session start works
This commit is contained in:
16
frontend/src/components/DesireForm.test.tsx
Normal file
16
frontend/src/components/DesireForm.test.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
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();
|
||||
|
||||
// Check for the submit button
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
});
|
||||
87
frontend/src/components/DesireForm.tsx
Normal file
87
frontend/src/components/DesireForm.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextField, Button, Box, Typography } from '@mui/material';
|
||||
|
||||
interface DesireFormProps {
|
||||
onSubmit: (desires: { wants: string[], accepts: string[], noGoes: string[] }) => void;
|
||||
}
|
||||
|
||||
const DesireForm: React.FC<DesireFormProps> = ({ onSubmit }) => {
|
||||
const [wants, setWants] = useState('');
|
||||
const [accepts, setAccepts] = useState('');
|
||||
const [noGoes, setNoGoes] = useState('');
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const parsedWants = wants.split('\n').map(s => s.trim()).filter(s => s);
|
||||
const parsedAccepts = accepts.split('\n').map(s => s.trim()).filter(s => s);
|
||||
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) {
|
||||
alert('Please enter at least one desire in any category.');
|
||||
return;
|
||||
}
|
||||
|
||||
// FR-016: System MUST validate a user's submission to prevent the same item from appearing in conflicting categories
|
||||
const allItems = [...parsedWants, ...parsedAccepts, ...parsedNoGoes];
|
||||
const uniqueItems = new Set(allItems);
|
||||
if (allItems.length !== uniqueItems.size) {
|
||||
alert('You have conflicting desires (same item in different categories). Please resolve.');
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
wants: parsedWants,
|
||||
accepts: parsedAccepts,
|
||||
noGoes: parsedNoGoes,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>What you WANT</Typography>
|
||||
<TextField
|
||||
label="List items you want (one per line)"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={wants}
|
||||
onChange={(e) => setWants(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 4 }}>What you ACCEPT</Typography>
|
||||
<TextField
|
||||
label="List items you accept (one per line)"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={accepts}
|
||||
onChange={(e) => setAccepts(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 4 }}>What you DO NOT WANT</Typography>
|
||||
<TextField
|
||||
label="List items you absolutely do not want (one per line)"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={noGoes}
|
||||
onChange={(e) => setNoGoes(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
>
|
||||
Submit Desires
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesireForm;
|
||||
29
frontend/src/components/ResultsDisplay.test.tsx
Normal file
29
frontend/src/components/ResultsDisplay.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ResultsDisplay from './ResultsDisplay';
|
||||
import { Decision } from '../hooks/useSession';
|
||||
|
||||
const mockDecision: Decision = {
|
||||
goTos: [{ title: 'Go to the beach', rawInputs: ['beach'] }],
|
||||
alsoGoods: [{ title: 'Eat pizza', rawInputs: ['pizza'] }],
|
||||
considerables: [{ title: 'Watch a movie', rawInputs: ['movie'] }],
|
||||
noGoes: [{ title: 'Stay home', rawInputs: ['home'] }],
|
||||
};
|
||||
|
||||
describe('ResultsDisplay', () => {
|
||||
it('renders all categories correctly', () => {
|
||||
render(<ResultsDisplay decision={mockDecision} />);
|
||||
|
||||
expect(screen.getByText('Go-to')).toBeInTheDocument();
|
||||
expect(screen.getByText('Go to the beach')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Also good')).toBeInTheDocument();
|
||||
expect(screen.getByText('Eat pizza')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Considerable')).toBeInTheDocument();
|
||||
expect(screen.getByText('Watch a movie')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('No-goes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Stay home')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
61
frontend/src/components/ResultsDisplay.tsx
Normal file
61
frontend/src/components/ResultsDisplay.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, List, ListItem, ListItemText, Collapse, IconButton } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { Decision, SemanticDesire } from '../hooks/useSession';
|
||||
|
||||
interface ResultsDisplayProps {
|
||||
decision: Decision;
|
||||
}
|
||||
|
||||
const CategorySection: React.FC<{ title: string; desires: SemanticDesire[]; defaultExpanded?: boolean }>
|
||||
= ({ title, desires, defaultExpanded = true }) => {
|
||||
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
||||
|
||||
if (!desires || desires.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 3, border: '1px solid #e0e0e0', borderRadius: '4px', p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton onClick={() => setExpanded(!expanded)} size="small">
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<List dense>
|
||||
{desires.map((desire, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText primary={desire.title} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ResultsDisplay: React.FC<ResultsDisplayProps> = ({ decision }) => {
|
||||
if (!decision) {
|
||||
return <Typography>No decision available yet.</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h5" component="h1" gutterBottom>
|
||||
Cooperative Decision
|
||||
</Typography>
|
||||
|
||||
<CategorySection title="Go-to" desires={decision.goTos} />
|
||||
<CategorySection title="Also good" desires={decision.alsoGoods} />
|
||||
<CategorySection title="Considerable" desires={decision.considerables} defaultExpanded={false} />
|
||||
<CategorySection title="No-goes" desires={decision.noGoes} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultsDisplay;
|
||||
Reference in New Issue
Block a user