'Afraid to Ask' implemented
This commit is contained in:
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@@ -29,7 +29,11 @@
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.7"
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint src/**/*.{js,jsx,ts,tsx}"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -48,6 +49,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.7"
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,60 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import DesireForm from './DesireForm';
|
||||
|
||||
test('renders desire submission form with input fields and submit button', () => {
|
||||
render(<DesireForm onSubmit={() => {}} />);
|
||||
|
||||
// Check for headings/labels for each category
|
||||
expect(screen.getByLabelText(/What you WANT/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/What you ACCEPT/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/What you DO NOT WANT/i)).toBeInTheDocument();
|
||||
describe('DesireForm', () => {
|
||||
test('renders desire submission form with all input fields and submit button', () => {
|
||||
render(<DesireForm onSubmit={() => {}} />);
|
||||
|
||||
// Check for headings/labels for each category
|
||||
expect(screen.getByText(/What You Want/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Afraid to Ask \(Private\)/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/What You Accept/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/What You Do Not Want/i)).toBeInTheDocument();
|
||||
|
||||
// Check for the submit button
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
// Check for the submit button
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('submits correct data including afraidToAsk field', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
render(<DesireForm onSubmit={handleSubmit} />);
|
||||
|
||||
const wantsInput = screen.getByLabelText(/Enter items you want/i);
|
||||
const acceptsInput = screen.getByLabelText(/Enter items you accept/i);
|
||||
const noGoesInput = screen.getByLabelText(/Enter items you absolutely do not want/i);
|
||||
const afraidToAskInput = screen.getByLabelText(/Enter sensitive ideas privately/i);
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
|
||||
await userEvent.type(wantsInput, 'More vacation');
|
||||
await userEvent.type(acceptsInput, 'Flexible hours');
|
||||
await userEvent.type(noGoesInput, 'Mandatory overtime');
|
||||
await userEvent.type(afraidToAskInput, 'A raise');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalledWith({
|
||||
wants: ['More vacation'],
|
||||
accepts: ['Flexible hours'],
|
||||
noGoes: ['Mandatory overtime'],
|
||||
afraidToAsk: 'A raise',
|
||||
});
|
||||
});
|
||||
|
||||
test('shows alert if no desires are entered, including afraidToAsk', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
render(<DesireForm onSubmit={handleSubmit} />);
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
|
||||
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(alertMock).toHaveBeenCalledWith('Please enter at least one desire in any category.');
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
|
||||
alertMock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,13 +2,14 @@ import React, { useState } from 'react';
|
||||
import { TextField, Button, Box, Typography } from '@mui/material';
|
||||
|
||||
interface DesireFormProps {
|
||||
onSubmit: (desires: { wants: string[], accepts: string[], noGoes: string[] }) => void;
|
||||
onSubmit: (desires: { wants: string[], accepts: string[], noGoes: string[], afraidToAsk: string }) => void;
|
||||
}
|
||||
|
||||
const DesireForm: React.FC<DesireFormProps> = ({ onSubmit }) => {
|
||||
const [wants, setWants] = useState('');
|
||||
const [accepts, setAccepts] = useState('');
|
||||
const [noGoes, setNoGoes] = useState('');
|
||||
const [afraidToAsk, setAfraidToAsk] = useState('');
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -17,7 +18,7 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit }) => {
|
||||
const parsedNoGoes = noGoes.split('\n').map(s => s.trim()).filter(s => s);
|
||||
|
||||
// FR-020: The system MUST require a user to enter at least one desire in at least one of the three categories
|
||||
if (parsedWants.length === 0 && parsedAccepts.length === 0 && parsedNoGoes.length === 0) {
|
||||
if (parsedWants.length === 0 && parsedAccepts.length === 0 && parsedNoGoes.length === 0 && afraidToAsk.length === 0) {
|
||||
alert('Please enter at least one desire in any category.');
|
||||
return;
|
||||
}
|
||||
@@ -34,6 +35,7 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit }) => {
|
||||
wants: parsedWants,
|
||||
accepts: parsedAccepts,
|
||||
noGoes: parsedNoGoes,
|
||||
afraidToAsk: afraidToAsk,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -51,6 +53,18 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit }) => {
|
||||
helperText={`Enter items you want, one per line. Max 500 characters per item. ${wants.length}/500`}
|
||||
/>
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 4 }}>Afraid to Ask (Private)</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={afraidToAsk}
|
||||
onChange={(e) => setAfraidToAsk(e.target.value)}
|
||||
margin="normal"
|
||||
inputProps={{ maxLength: 500 }}
|
||||
helperText={`Enter sensitive ideas privately. Max 500 characters. ${afraidToAsk.length}/500`}
|
||||
/>
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 4 }}>What You Accept</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
|
||||
@@ -30,4 +30,22 @@ describe('ResultsDisplay Refactor', () => {
|
||||
expect(screen.getByText('Needs discussion')).toBeInTheDocument();
|
||||
expect(screen.getByText('There is a conflict regarding Tacos.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should correctly display harmonized results including matched Afraid to Ask ideas', () => {
|
||||
const decisionWithAfraidToAsk: Decision = {
|
||||
goTo: 'Everyone wants Pizza and a secret desire for a raise.', // Matched Afraid to Ask
|
||||
alsoGood: 'Many people are okay with Pasta.',
|
||||
considerable: 'Burgers are an option for some.',
|
||||
noGoes: 'No one wants Salad.',
|
||||
needsDiscussion: 'There is a conflict regarding Tacos.',
|
||||
};
|
||||
|
||||
render(<ResultsDisplay decision={decisionWithAfraidToAsk} />);
|
||||
|
||||
expect(screen.getByText('Go-to')).toBeInTheDocument();
|
||||
expect(screen.getByText('Everyone wants Pizza and a secret desire for a raise.')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Also good')).toBeInTheDocument();
|
||||
expect(screen.getByText('Many people are okay with Pasta.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface DesireSet {
|
||||
wants: string[];
|
||||
accepts: string[];
|
||||
noGoes: string[];
|
||||
afraidToAsk: string; // Add afraidToAsk to DesireSet
|
||||
}
|
||||
|
||||
export interface Decision {
|
||||
@@ -39,7 +40,7 @@ export interface Session {
|
||||
state: SessionState;
|
||||
expectedResponses: number;
|
||||
submittedCount: number;
|
||||
responses: { [clientId: string]: boolean }; // Map of clientId to a boolean indicating if they submitted
|
||||
responses: { [clientId: string]: DesireSet }; // Map of clientId to their submitted DesireSet
|
||||
finalResult: Decision | null;
|
||||
topic?: string; // This might be part of the initial setup payload
|
||||
}
|
||||
@@ -89,9 +90,21 @@ export const useSession = (sessionId: string): [Session | null, Dispatch<SetStat
|
||||
|
||||
webSocketService.onMessage(handleMessage);
|
||||
|
||||
const handleSessionTerminated = () => {
|
||||
setSession(prevSession => {
|
||||
if (prevSession) {
|
||||
return { ...prevSession, state: SessionState.FINAL, finalResult: null }; // Or a specific TERMINATED state
|
||||
}
|
||||
return null;
|
||||
});
|
||||
setError('Session terminated by server.');
|
||||
};
|
||||
webSocketService.onSessionTerminated(handleSessionTerminated);
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
webSocketService.removeMessageHandler(handleMessage);
|
||||
webSocketService.removeSessionTerminatedHandler(handleSessionTerminated);
|
||||
webSocketService.disconnect();
|
||||
};
|
||||
}, [sessionId, clientId]); // Re-run effect if sessionId or clientId changes
|
||||
|
||||
@@ -16,7 +16,7 @@ const SessionPage = () => {
|
||||
sendMessage({ type: 'SETUP_SESSION', payload: { expectedResponses, topic } });
|
||||
};
|
||||
|
||||
const handleSubmitDesires = (desires: { wants: string[], accepts: string[], noGoes: string[] }) => {
|
||||
const handleSubmitDesires = (desires: { wants: string[], accepts: string[], noGoes: string[], afraidToAsk: string }) => {
|
||||
if (!session || !clientId) return;
|
||||
|
||||
const desireSet: DesireSet = {
|
||||
@@ -24,6 +24,7 @@ const SessionPage = () => {
|
||||
wants: desires.wants,
|
||||
accepts: desires.accepts,
|
||||
noGoes: desires.noGoes,
|
||||
afraidToAsk: desires.afraidToAsk,
|
||||
};
|
||||
|
||||
sendMessage({
|
||||
@@ -104,9 +105,24 @@ const SessionPage = () => {
|
||||
<Typography variant="h6" component="p">
|
||||
Waiting for {remainingResponses} more responses...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Your desires have been submitted. The results will be calculated once all participants have responded.
|
||||
</Typography>
|
||||
<Box sx={{ textAlign: 'left', mt: 2 }}>
|
||||
<Typography variant="subtitle1">Your Submitted Desires:</Typography>
|
||||
{session.responses[clientId]?.wants.length > 0 && (
|
||||
<Typography variant="body2"><strong>Wants:</strong> {session.responses[clientId]?.wants.join(', ')}</Typography>
|
||||
)}
|
||||
{session.responses[clientId]?.afraidToAsk && (
|
||||
<Typography variant="body2"><strong>Afraid to Ask:</strong> {session.responses[clientId]?.afraidToAsk}</Typography>
|
||||
)}
|
||||
{session.responses[clientId]?.accepts.length > 0 && (
|
||||
<Typography variant="body2"><strong>Accepts:</strong> {session.responses[clientId]?.accepts.join(', ')}</Typography>
|
||||
)}
|
||||
{session.responses[clientId]?.noGoes.length > 0 && (
|
||||
<Typography variant="body2"><strong>No-Goes:</strong> {session.responses[clientId]?.noGoes.join(', ')}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ class WebSocketService {
|
||||
private ws: WebSocket | null = null;
|
||||
private messageHandlers: ((message: any) => void)[] = [];
|
||||
private errorHandlers: ((error: Event) => void)[] = [];
|
||||
private sessionTerminatedHandlers: (() => void)[] = [];
|
||||
private currentSessionId: string | null = null;
|
||||
private currentClientId: string | null = null;
|
||||
|
||||
@@ -34,6 +35,7 @@ class WebSocketService {
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
this.sessionTerminatedHandlers.forEach(handler => handler());
|
||||
this.ws = null;
|
||||
this.currentSessionId = null;
|
||||
this.currentClientId = null;
|
||||
@@ -80,6 +82,14 @@ class WebSocketService {
|
||||
removeErrorHandler(handler: (error: Event) => void) {
|
||||
this.errorHandlers = this.errorHandlers.filter(h => h !== handler);
|
||||
}
|
||||
|
||||
onSessionTerminated(handler: () => void) {
|
||||
this.sessionTerminatedHandlers.push(handler);
|
||||
}
|
||||
|
||||
removeSessionTerminatedHandler(handler: () => void) {
|
||||
this.sessionTerminatedHandlers = this.sessionTerminatedHandlers.filter(h => h !== handler);
|
||||
}
|
||||
}
|
||||
|
||||
export const webSocketService = new WebSocketService();
|
||||
|
||||
Reference in New Issue
Block a user