Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions frontend/src/components/common/ConfirmationDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable react/prop-types */
/* eslint-disable no-unused-vars */
// frontend/src/components/common/ConfirmationDialog.jsx
import React from 'react';
Expand All @@ -17,7 +18,10 @@ const ConfirmationDialog = ({
message = "Are you sure you want to proceed? This action cannot be undone.", // Default message
confirmText = "Confirm", // Default confirm button text
cancelText = "Cancel", // Default cancel button text
isProcessing = false // Optional: disable buttons while processing confirm action
isProcessing = false, // Optional: disable buttons while processing confirm action
dialogTestId = "confirmation-dialog",
confirmButtonTestId = "confirmation-dialog-confirm-button",
cancelButtonTestId = "confirmation-dialog-cancel-button"
}) => {

return (
Expand All @@ -26,6 +30,7 @@ const ConfirmationDialog = ({
onClose={() => !isProcessing && onClose()} // Prevent closing while processing
aria-labelledby="confirmation-dialog-title"
aria-describedby="confirmation-dialog-description"
data-testid={dialogTestId} // <<< ADDED TEST ID
>
<DialogTitle id="confirmation-dialog-title">
{title}
Expand All @@ -36,7 +41,7 @@ const ConfirmationDialog = ({
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isProcessing}>
<Button onClick={onClose} disabled={isProcessing} data-testid={cancelButtonTestId}>
{cancelText}
</Button>
{/* Make confirm button stand out, often uses primary or error color */}
Expand All @@ -46,6 +51,7 @@ const ConfirmationDialog = ({
variant="contained" // Make it more prominent
autoFocus // Focus on confirm by default
disabled={isProcessing}
data-testid={confirmButtonTestId}
>
{isProcessing ? "Processing..." : confirmText}
</Button>
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/components/common/EditableField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';

const EditableField = ({
testIdPrefix, // Optional: Prefix for test IDs (e.g., "student-name")
label, // Label for the TextField in edit mode AND display mode now
value, // The current value to display
onSave, // Async function to call when saving (receives new value)
Expand Down Expand Up @@ -46,9 +47,12 @@ const EditableField = ({
}
};

// Helper function to generate test ID only if prefix is provided
const addTestId = (suffix) => (testIdPrefix ? { [`data-testid`]: `${testIdPrefix}-${suffix}` } : {});

return (
// Container - align items to the start (top) for label
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, minHeight: '40px', width: '100%', ...containerSx }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, minHeight: '40px', width: '100%', ...containerSx }} {...addTestId('container')}>
{!isEditing ? (
// --- MODIFIED Display Mode ---
<>
Expand All @@ -68,15 +72,15 @@ const EditableField = ({
color: !value ? 'text.secondary' : 'inherit',
// Apply word break for safety, although less likely needed for single-line fields
overflowWrap: 'break-word',
lineHeight: 1.4 // Adjust line height if needed
}}
lineHeight: 1.4}} // Adjust line height if needed
{...addTestId('display')} // <<< ADDED TEST ID FOR DISPLAY VALUE
>
{/* Use value, fallback to emptyText. Placeholder less relevant here */}
{value || emptyText}
</Typography>
</Box>
{/* Edit Button */}
<IconButton size="small" onClick={handleEditClick} aria-label={`Edit ${label}`} disabled={isSaving} sx={{mt: 0.5 /* Adjust vertical alignment if needed */}}>
<IconButton size="small" onClick={handleEditClick} aria-label={`Edit ${label}`} disabled={isSaving} sx={{mt: 0.5 /* Adjust vertical alignment if needed */}} {...addTestId('edit-button')}>
<EditIcon fontSize="small" />
</IconButton>
</>
Expand All @@ -95,11 +99,15 @@ const EditableField = ({
rows={multiline ? rows : 1}
{...textFieldProps}
disabled={isSaving}
// Add test ID to the TextField wrapper - Cypress can find input inside
{...addTestId('input-wrapper')} // <<< ADDED TEST ID FOR TEXTFIELD WRAPPER
// Alternatively, add directly to inputProps if needed, but wrapper is often easier
// inputProps={{ ...textFieldProps?.inputProps, ...addTestId('input') }}
/>
<IconButton size="small" onClick={handleSaveClick} color="success" aria-label={`Save ${label}`} disabled={isSaving}>
<IconButton size="small" onClick={handleSaveClick} color="success" aria-label={`Save ${label}`} disabled={isSaving} {...addTestId('save-button')}>
<CheckIcon />
</IconButton>
<IconButton size="small" onClick={handleCancelClick} color="error" aria-label={`Cancel ${label} edit`} disabled={isSaving}>
<IconButton size="small" onClick={handleCancelClick} color="error" aria-label={`Cancel ${label} edit`} disabled={isSaving} {...addTestId('cancel-button')}>
<CloseIcon />
</IconButton>
</>
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/components/common/EditableTextArea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';

const EditableTextArea = ({
testIdPrefix,
label, // Label for the TextField in edit mode AND display mode now
value, // The current value to display
onSave, // Async function to call when saving (receives new value)
Expand Down Expand Up @@ -45,9 +46,12 @@ const EditableTextArea = ({
}
};

// Helper function to generate test ID only if prefix is provided
const addTestId = (suffix) => (testIdPrefix ? { [`data-testid`]: `${testIdPrefix}-${suffix}` } : {});

return (
// Container remains largely the same
<Box sx={{ width: '100%', position: 'relative', ...containerSx }}>
<Box sx={{ width: '100%', position: 'relative', ...containerSx }} {...addTestId('container')}>
{!isEditing ? (
// Display Mode
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
Expand All @@ -67,11 +71,12 @@ const EditableTextArea = ({
color: !value ? 'text.secondary' : 'inherit',
minHeight: '20px'
}}
{...addTestId('display')}
>
{value?.trim() || placeholder || emptyText}
</Typography>
</Box>
<IconButton size="small" onClick={handleEditClick} /* ... other props ... */ sx={{ mt: 1 }}>
<IconButton size="small" onClick={handleEditClick} /* ... other props ... */ sx={{ mt: 1 }} {...addTestId('edit-button')}>
<EditIcon fontSize="small" />
</IconButton>
</Box>
Expand All @@ -90,12 +95,15 @@ const EditableTextArea = ({
autoFocus
{...textFieldProps}
disabled={isSaving}
{...addTestId('input-wrapper')} // <<< ADDED TEST ID FOR TEXTFIELD WRAPPER
// You might need to target the actual textarea element inside this wrapper in tests
// using .find('textarea')
/>
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
<IconButton size="small" onClick={handleSaveClick} color="success" aria-label={`Save ${label}`} disabled={isSaving}>
<IconButton size="small" onClick={handleSaveClick} color="success" aria-label={`Save ${label}`} disabled={isSaving} {...addTestId('save-button')}>
<CheckIcon />
</IconButton>
<IconButton size="small" onClick={handleCancelClick} color="error" aria-label={`Cancel ${label} edit`} disabled={isSaving}>
<IconButton size="small" onClick={handleCancelClick} color="error" aria-label={`Cancel ${label} edit`} disabled={isSaving} {...addTestId('cancel-button')}>
<CloseIcon />
</IconButton>
</Box>
Expand Down
170 changes: 105 additions & 65 deletions frontend/src/components/directory/FilterControls.jsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,113 @@
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
// frontend/src/components/directory/FilterControls.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import FilterControls from '../components/directory/FilterControls'; // make sure path is correct
import { Box, Grid, FormControl, InputLabel, Select, MenuItem } from '@mui/material';

describe('FilterControls Component', () => {
const mockOnRoleChange = jest.fn();
const mockOnDepartmentChange = jest.fn();
const mockOnMajorChange = jest.fn();
const mockOnYearChange = jest.fn();
const FilterControls = ({
// Filter values
selectedRole,
selectedDepartment,
selectedMajor,
selectedYear,
// Change handlers
onRoleChange,
onDepartmentChange,
onMajorChange,
onYearChange,
// Options for dropdowns
departmentsList,
majorsList,
yearsList,
}) => {
return (
<Box sx={{ mb: 2 }}> {/* Add some margin below filters */}
<Grid container spacing={2}>
{/* Role Filter */}
<Grid item xs={12} sm={6} md={3}>
<FormControl fullWidth size="small">
<InputLabel id="role-filter-label">Role</InputLabel>
<Select
labelId="role-filter-label"
id="role-filter-select"
value={selectedRole}
label="Role"
onChange={onRoleChange} // Pass event up
>
<MenuItem value=""><em>All Roles</em></MenuItem>
<MenuItem value="student">Student</MenuItem>
<MenuItem value="professor">Professor</MenuItem>
</Select>
</FormControl>
</Grid>

const departmentsList = ['Mathematics', 'Physics'];
const majorsList = ['Software Engineering', 'Computer Science'];
const yearsList = ['Freshman', 'Sophomore', 'Junior', 'Senior'];
{/* Department Filter (Show only if not filtering by Student) */}
{selectedRole !== 'student' && (
<Grid item xs={12} sm={6} md={3}>
<FormControl fullWidth size="small" disabled={selectedRole === 'student'}>
<InputLabel id="dept-filter-label">Department</InputLabel>
<Select
labelId="dept-filter-label"
id="dept-filter-select"
value={selectedDepartment}
label="Department"
onChange={onDepartmentChange}
>
<MenuItem value=""><em>All Departments</em></MenuItem>
{departmentsList.map((dept) => (
<MenuItem key={dept} value={dept}>{dept || '(No Dept.)'}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}

beforeEach(() => {
render(
<FilterControls
selectedRole=""
selectedDepartment=""
selectedMajor=""
selectedYear=""
onRoleChange={mockOnRoleChange}
onDepartmentChange={mockOnDepartmentChange}
onMajorChange={mockOnMajorChange}
onYearChange={mockOnYearChange}
departmentsList={departmentsList}
majorsList={majorsList}
yearsList={yearsList}
/>
);
});

it('calls correct handler when role changes', async () => {
const roleSelect = screen.getByLabelText('Role');
await userEvent.click(roleSelect);
{/* Major Filter (Show only if not filtering by Professor) */}
{selectedRole !== 'professor' && (
<Grid item xs={12} sm={6} md={3}>
<FormControl fullWidth size="small" disabled={selectedRole === 'professor'}>
<InputLabel id="major-filter-label">Major</InputLabel>
<Select
labelId="major-filter-label"
id="major-filter-select"
value={selectedMajor}
label="Major"
onChange={onMajorChange}
>
<MenuItem value=""><em>All Majors</em></MenuItem>
{majorsList.map((major) => (
<MenuItem key={major} value={major}>{major || '(No Major)'}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}

const studentOption = await screen.findByText('Student');
await userEvent.click(studentOption);
{/* Year Filter (Show only if not filtering by Professor) */}
{selectedRole !== 'professor' && (
<Grid item xs={12} sm={6} md={3}>
<FormControl fullWidth size="small" disabled={selectedRole === 'professor'}>
<InputLabel id="year-filter-label">Year</InputLabel>
<Select
labelId="year-filter-label"
id="year-filter-select"
value={selectedYear}
label="Year"
onChange={onYearChange}
>
<MenuItem value=""><em>All Years</em></MenuItem>
{yearsList.map((year) => (
<MenuItem key={year} value={year}>{year || '(No Year)'}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}

expect(mockOnRoleChange).toHaveBeenCalledTimes(1);
});
</Grid>
</Box>
);
};

it('calls correct handler when major changes', async () => {
const majorSelect = screen.getByLabelText('Major');
await userEvent.click(majorSelect);

const majorOption = await screen.findByText('Software Engineering');
await userEvent.click(majorOption);

expect(mockOnMajorChange).toHaveBeenCalledTimes(1);
});

it('calls correct handler when year changes', async () => {
const yearSelect = screen.getByLabelText('Year');
await userEvent.click(yearSelect);

const yearOption = await screen.findByText('Junior');
await userEvent.click(yearOption);

expect(mockOnYearChange).toHaveBeenCalledTimes(1);
});

it('calls correct handler when department changes', async () => {
const deptSelect = screen.getByLabelText('Department');
await userEvent.click(deptSelect);

const deptOption = await screen.findByText('Mathematics');
await userEvent.click(deptOption);

expect(mockOnDepartmentChange).toHaveBeenCalledTimes(1);
});
});
export default FilterControls;
12 changes: 8 additions & 4 deletions frontend/src/components/opportunities/AddOpportunityForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ const AddOpportunityForm = ({ open, onClose, onSave, initialData = null, isSavin
};

return (
<Dialog open={open} onClose={() => !isSaving && onClose()} maxWidth="sm" fullWidth> {/* Prevent closing while saving */}
<Dialog open={open} onClose={() => !isSaving && onClose()} maxWidth="sm" fullWidth data-testid="opportunity-form-dialog"> {/* Prevent closing while saving */}
<DialogTitle>{initialData ? 'Edit Opportunity Post' : 'Create New Opportunity Post'}</DialogTitle>
<DialogContent>
<TextField
data-testid="opportunity-title-input" // <<< ADDED
autoFocus
margin="dense"
name="title"
Expand All @@ -110,6 +111,7 @@ const AddOpportunityForm = ({ open, onClose, onSave, initialData = null, isSavin
disabled={isSaving}
/>
<TextField
data-testid="opportunity-description-input" // <<< ADDED
margin="dense"
name="description"
label="Description"
Expand All @@ -125,7 +127,7 @@ const AddOpportunityForm = ({ open, onClose, onSave, initialData = null, isSavin
helperText={formErrors.description}
disabled={isSaving}
/>
<FormControl fullWidth margin="dense" required error={!!formErrors.type} disabled={isSaving}>
<FormControl data-testid="opportunity-type-select" fullWidth margin="dense" required error={!!formErrors.type} disabled={isSaving}>
<InputLabel id="opportunity-type-label">Type</InputLabel>
<Select
labelId="opportunity-type-label"
Expand All @@ -145,6 +147,7 @@ const AddOpportunityForm = ({ open, onClose, onSave, initialData = null, isSavin
{formErrors.type && <FormHelperText>{formErrors.type}</FormHelperText>}
</FormControl>
<TextField
data-testid="opportunity-deadline-input"
margin="dense"
name="deadline"
label="Application Deadline (Optional)"
Expand All @@ -161,6 +164,7 @@ const AddOpportunityForm = ({ open, onClose, onSave, initialData = null, isSavin
<FormControlLabel
control={
<Checkbox
data-testid="opportunity-allow-interest-checkbox"
name="allowInterest"
checked={formData.allowInterest}
onChange={handleChange}
Expand All @@ -174,8 +178,8 @@ const AddOpportunityForm = ({ open, onClose, onSave, initialData = null, isSavin

</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isSaving}>Cancel</Button>
<Button onClick={handleSave} variant="contained" disabled={isSaving}>
<Button data-testid="opportunity-form-cancel-button" onClick={onClose} disabled={isSaving}>Cancel</Button>
<Button data-testid="opportunity-form-save-button" onClick={handleSave} variant="contained" disabled={isSaving}>
{isSaving ? <CircularProgress size={24} /> : (initialData ? 'Update Post' : 'Create Post')}
</Button>
</DialogActions>
Expand Down
Loading