'Afraid to Ask' implemented

This commit is contained in:
aodulov
2025-10-13 13:14:30 +03:00
parent 09269190c1
commit 5f8541a5f3
20 changed files with 2081 additions and 190 deletions

View File

@@ -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": {

View File

@@ -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"
}
}

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -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>
)}

View File

@@ -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();