From b476576207e644b00eae0fa6884de74c76d9c6f6 Mon Sep 17 00:00:00 2001 From: Luis Mendes Date: Tue, 3 Mar 2026 11:09:36 +0000 Subject: [PATCH 1/2] imp 1 --- INTERVIEW-NOTES.md | 222 +++++++++++++++++++++++ package-lock.json | 6 + react/src/components/AssignmentForm.tsx | 39 +++++ react/src/components/StudentForm.tsx | 38 ++++ react/src/components/StudentTable.tsx | 159 +++++++++++++++++ react/src/components/TeacherForm.tsx | 38 ++++ react/src/components/TeacherTable.tsx | 88 ++++++++++ react/tests/app.test.tsx | 38 ++++ react/tests/assignment-form.test.tsx | 49 ++++++ react/tests/student-form.test.tsx | 49 ++++++ react/tests/student-table.test.tsx | 223 ++++++++++++++++++++++++ react/tests/teacher-form.test.tsx | 49 ++++++ react/tests/teacher-table.test.tsx | 84 +++++++++ 13 files changed, 1082 insertions(+) create mode 100644 INTERVIEW-NOTES.md create mode 100644 package-lock.json create mode 100644 react/src/components/AssignmentForm.tsx create mode 100644 react/src/components/StudentForm.tsx create mode 100644 react/src/components/StudentTable.tsx create mode 100644 react/src/components/TeacherForm.tsx create mode 100644 react/src/components/TeacherTable.tsx create mode 100644 react/tests/app.test.tsx create mode 100644 react/tests/assignment-form.test.tsx create mode 100644 react/tests/student-form.test.tsx create mode 100644 react/tests/student-table.test.tsx create mode 100644 react/tests/teacher-form.test.tsx create mode 100644 react/tests/teacher-table.test.tsx diff --git a/INTERVIEW-NOTES.md b/INTERVIEW-NOTES.md new file mode 100644 index 0000000..15042e3 --- /dev/null +++ b/INTERVIEW-NOTES.md @@ -0,0 +1,222 @@ +## Infinitas LMS React exercise – study notes + +### 1. Big picture + +- **Goal**: Work on a small React-based LMS that manages teachers and students, then extend it toward assignments, grading (Pass/Fail), and simple reporting. +- **Timebox**: 45–60 minutes. The aim is **how you think, communicate, and structure code**, not finishing everything. +- **Your narrative**: “I’ll first understand the current design, then refactor the rough edges that slow development, then add a thin vertical slice of a new feature with tests.” + +### 2. Make sure the project runs + +Inside the `react` folder: + +- **Install**: `npm install` +- **Run tests**: `npm test` +- **Run dev server**: `npm run dev` and open the app. + +Be ready to briefly explain what each script does (Vite dev server, TypeScript build, Vitest tests). + +### 3. Understand the current React architecture + +- **State management** + - `SchoolProvider` wraps the app and uses `useReducer` with `schoolReducer`. + - **State shape**: + - `students: { id: string; name: string }[]` + - `teachers: { id: string; name: string; students: string[] }[]` (array of student IDs). + - **Actions** (`SchoolActionKind`): + - `ADD_TEACHER` + - `ADD_STUDENT` + - `UPDATE_STUDENT` + - `ASSIGN_STUDENT_TO_TEACHER` + - **Reducer patterns**: + - Adds are done by spreading existing arrays and appending. + - Updates are done via loops that build new arrays (immutable updates). + +- **UI (`App.tsx`)** + - Uses `useSchool()` to read state and `useSchoolDispatch()` to dispatch actions. + - Forms: + - Add teacher (creates UUID, dispatches `ADD_TEACHER`). + - Add student (creates UUID, dispatches `ADD_STUDENT`). + - Editing: + - Student name editing tracked with `studentEditingId` and `updatedStudentName`. + - Teacher assignment tracked with `teacherEditingId` and `newAssignedStudentId`. + - Data flow: + - Teachers table shows each teacher and lists assigned students by mapping IDs to names. + +Be ready to restate this architecture in your own words. + +### 4. Obvious refactors you can talk about (and maybe do) + +- **Rendering inefficiencies / clarity** + - Teacher student list currently maps `teacher.students` and then, *inside each*, maps all `school.students` to find a name: + - You can mention this is an O(n²) pattern and hard to read. + - Possible improvements: + - Precompute a `studentById` map (e.g. `const studentById = new Map(students.map(s => [s.id, s]));`). + - Or create a selector/helper function to map an ID to a name. + +- **Keys and list rendering** + - `li` and `option` elements should ideally have `key` props; you can mention this as a React best practice. + +#### 4.2 Second refactor: Reducer clarity (implemented) + +**What's wrong** +- `UPDATE_STUDENT` used a for-loop to build a new array; more verbose than using `.map()`. + +**How to approach it during the interview** + +1. **Name the problem**: "In the reducer, `UPDATE_STUDENT` builds a new students array with a for-loop. I'd refactor that to use `.map()` so the intent is clear: replace this one item, keep the rest the same." +2. **Propose the fix**: "Use `state.students.map(s => s.id === action.payload.id ? action.payload : s)` and return `{ ...state, students }`. Same for the teacher update if it's still a loop." +3. **Do it**: Change the `UPDATE_STUDENT` case (and `ASSIGN_STUDENT_TO_TEACHER` if needed), run `npm test`, then quickly test update student in the UI. +4. **Mention tests**: "The existing reducer test still passes. Reducers are easy to unit test." + +**Implementation**: Done in `react/src/school-context.tsx` — both cases now use `.map()`. + +- **Reducer clarity** (reference) + - Loops that build new arrays could be turned into `map` for readability: + - `state.students.map(s => s.id === action.payload.id ? action.payload : s);` + - Similarly for updating teacher’s `students` array. + +#### 4.3 Third refactor: Component extraction (implemented) + +**What's wrong** +- `App.tsx` holds all UI: two tables, two forms, and all state. Hard to scan and harder to test or change one area without touching the rest. + +**How to approach it during the interview** + +1. **Name the problem**: "App is doing a lot—teacher and student tables and forms plus editing state. I'd extract smaller components so each file has one job and we can test or change one area without touching the others." +2. **Propose the split**: "I'll pull out `TeacherTable`, `TeacherForm`, `StudentTable`, and `StudentForm`. The tables need data and callbacks for editing; the forms can use the context dispatch directly. App stays as the place that holds the editing state and passes it down." +3. **Do it in order**: Extract one piece at a time (e.g. `TeacherForm` first—no props, just move the form and the submit handler). Then the table with props. Then the same for students. Run the app after each step. +4. **Mention benefits**: "Now we could unit-test the reducer and add simple tests for a form or table in isolation. Adding the assignment feature later, we might add an `AssignmentForm` or extend the student row without cluttering App." + +**Implementation**: `react/src/components/TeacherTable.tsx`, `TeacherForm.tsx`, `StudentTable.tsx`, `StudentForm.tsx`. App imports them and passes teachers/students plus editing state and callbacks. TeacherTable uses a `studentById` map for O(1) name lookup. + +#### 4.4 Fourth refactor: Validation and UX (implemented) + +**What's wrong** +- Forms allow submitting with blank or whitespace-only names. +- "Assign" can be clicked with no student selected; "Done" can be clicked with an empty name when editing a student. +- Labels and buttons are mostly fine; small improvements (e.g. clearer button labels) help accessibility. + +**How to approach it during the interview** + +1. **Name the problem**: "Right now we can add a teacher or student with an empty name, and we can click Assign without picking a student or Done with an empty name. I'd add simple validation and disable those actions when the input isn't valid." +2. **Propose the fix**: "In the add forms I'll use controlled inputs and disable the submit button when the trimmed value is empty. In the tables I'll disable Assign when no student is selected and Done when the name is empty. We already have htmlFor/id on the form labels." +3. **Do it**: Add state for the form inputs, disable submit when `value.trim() === ''`. Disable Assign when `!newAssignedStudentId`, Done when `updatedStudentName.trim() === ''`. Optionally add aria attributes if we show an error." +4. **Mention trade-offs**: "We could show an error message instead of only disabling; disabling is simpler and avoids extra state. For production we might add a toast or inline error." + +**Implementation**: TeacherForm and StudentForm use controlled inputs and disable submit when name is empty. TeacherTable disables Assign when no student selected; StudentTable disables Done when name is empty. + +Pick 1–2 of these to mention/implement to show pragmatic refactoring, not perfectionism. + +### 5. Likely feature extensions (assignments, grading, reporting) + +#### 5.1 First slice: assignments and per-student assignment list (implemented) + +**What’s implemented** +- Extended state with normalized assignment data: + - `assignments: { id: string; title: string }[]` + - `studentAssignments: { id: string; studentId: string; assignmentId: string; status: "assigned" | "pass" | "fail"; date: string }[]` +- New actions: + - `ADD_ASSIGNMENT` + - `ASSIGN_ASSIGNMENT_TO_STUDENT` + - `GRADE_ASSIGNMENT` (status update only; UI still to come if needed). +- UI: + - `AssignmentForm` component to create assignments. + - `StudentTable` now shows each student’s assignments (title + status) and lets you assign an assignment to a student via a dropdown. + +**How to approach it during the interview** + +1. **State design**: “For assignments I’d like a normalized shape: an `assignments` collection and a `studentAssignments` collection that links students to assignments with a status and date. That makes reporting like ‘how many students passed assignment X on a date’ straightforward.” +2. **Reducer changes**: “I’ll add `ADD_ASSIGNMENT`, `ASSIGN_ASSIGNMENT_TO_STUDENT`, and `GRADE_ASSIGNMENT` to the reducer. Each case returns a new state: append an assignment, append a student-assignment record, or update the status of one record.” +3. **UI slice**: “I’ll implement one vertical slice: a small Assignments form to create assignments, and in the student table an ‘Assign assignment’ flow: click a button, pick an assignment from a select, and dispatch `ASSIGN_ASSIGNMENT_TO_STUDENT`. I’ll also list each student’s assignments and their status.” +4. **Future grading/reporting**: “With `studentAssignments` in place, grading is just toggling status on a record, and reporting is filtering that array by assignment and date. If we had more time I’d add Pass/Fail buttons and a small reporting panel.” + +**Files to point to in the code** +- Types, state, actions, and reducer cases: `react/src/school-context.tsx`. +- Assignments UI and wiring: + - `react/src/components/AssignmentForm.tsx` + - `react/src/components/StudentTable.tsx` (assignments column and assign flow) + - `react/src/App.tsx` (assigning state and dispatch call) + +The README lists three capabilities: + +1. **Assign an assignment to a student** +2. **Grade an assignment Pass/Fail** +3. **Basic reporting on how many students passed an assignment on a given day** + +Prepare a simple design you can describe and partially implement. + +- **Data model (front-end version) – one reasonable approach** + - Add an `Assignment` concept and track per-student status: + - `assignments: { id: string; title: string }[]` + - `studentAssignments: { id: string; studentId: string; assignmentId: string; status: "assigned" | "pass" | "fail"; date: string }[]` + - Or, if they want to keep it minimal, you can nest assignments under `Student`: + - `Student` becomes `{ id; name; assignments: { id; title; status; date }[] }`. + - Be ready to explain trade‑offs: + - Separate collections = more normalized, easier reporting. + - Nested under `Student` = simpler code initially but harder cross‑student queries. + +- **Reducer changes you might propose** + - New action types, for example: + - `ADD_ASSIGNMENT` + - `ASSIGN_ASSIGNMENT_TO_STUDENT` + - `GRADE_ASSIGNMENT` + - Show you know how to: + - Extend `InitialState` with new fields. + - Extend `SchoolActionKind` and `SchoolAction` union. + - Update `schoolReducer` immutably. + +- **UI slice to implement** + - Aim for **one vertical slice**, e.g.: + - A small “Assignments” section to create an assignment. + - In the students table, add a button “Assign assignment” that: + - Opens a select of assignments. + - Dispatches `ASSIGN_ASSIGNMENT_TO_STUDENT`. + - Or focus on grading: + - Show a list of a student’s assignments with buttons “Pass” / “Fail”. + +### 6. Reporting idea (even if you don’t implement fully) + +- **Requirement**: “How many students passed an assignment on a given day?” +- If using `studentAssignments` with `status` and `date`: + - A selector/helper function could: + - Filter by `assignmentId` and `date`. + - Count how many have `status === "pass"`. + - In the UI, a simple reporting panel could: + - Let you select an assignment and date (or use today). + - Display: `X students passed / Y students assigned`. + +Even if you don’t code this, be ready to walk through how you’d do it. + +### 7. Testing strategy (Vitest) + +- **Existing test**: `school-reducer.test.ts` verifies `ADD_TEACHER`. +- How to extend: + - Add tests for: + - `ADD_STUDENT` (length increases, correct data). + - `UPDATE_STUDENT` (only one student’s name changes). + - `ASSIGN_STUDENT_TO_TEACHER` (teacher gains the student ID, no mutation of initial arrays). + - If you add new actions, write tests **first** or at least alongside changes. + +Talking points: + +- You value reducers because they are easy to unit test in isolation. +- You like to guard complex state updates with focused tests. + +### 8. How to approach the session + +- **First 5–10 minutes** + - Skim `App.tsx` and `school-context.tsx`, describe current design out loud. + - Call out one or two refactor targets and explain *why* they’d help future work. + +- **Next 30–40 minutes** + - Implement a small refactor (e.g. improve how teacher’s students are rendered). + - Add or update at least one reducer test. + - Add a small part of an assignment/grading feature (data model + one UI interaction). + +- **Final minutes** + - Reflect: what you’d do next (more tests, deeper refactors, error handling, accessibility). + - Emphasize you think in terms of maintainability, correctness, and user experience. + +If you rehearse explaining these points and can comfortably modify the reducer and UI, you’ll be well prepared for the exercise. + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7853848 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "interview-test", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/react/src/components/AssignmentForm.tsx b/react/src/components/AssignmentForm.tsx new file mode 100644 index 0000000..a35b1e8 --- /dev/null +++ b/react/src/components/AssignmentForm.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { SchoolActionKind, useSchoolDispatch } from "../school-context"; + +export function AssignmentForm() { + const dispatch = useSchoolDispatch(); + const [title, setTitle] = useState(""); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const trimmed = title.trim(); + if (!trimmed) return; + const id = crypto.randomUUID(); + dispatch?.({ + type: SchoolActionKind.ADD_ASSIGNMENT, + payload: { id, title: trimmed }, + }); + setTitle(""); + }; + + const isValid = title.trim() !== ""; + + return ( +
+ + setTitle(e.target.value)} + aria-invalid={!isValid && title.length > 0} + /> + +
+ ); +} + diff --git a/react/src/components/StudentForm.tsx b/react/src/components/StudentForm.tsx new file mode 100644 index 0000000..b5642b0 --- /dev/null +++ b/react/src/components/StudentForm.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import { SchoolActionKind, useSchoolDispatch } from "../school-context"; + +export function StudentForm() { + const schoolDispatch = useSchoolDispatch(); + const [name, setName] = useState(""); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const trimmed = name.trim(); + if (!trimmed) return; + const id = crypto.randomUUID(); + schoolDispatch?.({ + type: SchoolActionKind.ADD_STUDENT, + payload: { name: trimmed, id }, + }); + setName(""); + }; + + const isValid = name.trim() !== ""; + + return ( +
+ + setName(e.target.value)} + aria-invalid={!isValid && name.length > 0} + /> + +
+ ); +} diff --git a/react/src/components/StudentTable.tsx b/react/src/components/StudentTable.tsx new file mode 100644 index 0000000..d0e5b07 --- /dev/null +++ b/react/src/components/StudentTable.tsx @@ -0,0 +1,159 @@ +import type { + Assignment, + Student, + StudentAssignment, +} from "../school-context"; + +type StudentTableProps = { + students: Student[]; + assignments: Assignment[]; + studentAssignments: StudentAssignment[]; + studentEditingId: string | null; + updatedStudentName: string; + onStudentEditingIdChange: (id: string | null) => void; + onUpdatedStudentNameChange: (name: string) => void; + onUpdateStudent: () => void; + assigningStudentId: string | null; + selectedAssignmentId: string | null; + onAssigningStudentIdChange: (id: string | null) => void; + onSelectedAssignmentIdChange: (id: string | null) => void; + onAssignAssignment: () => void; + onGradeAssignment: ( + studentAssignmentId: string, + status: "pass" | "fail" + ) => void; +}; + +export function StudentTable({ + students, + assignments, + studentAssignments, + studentEditingId, + updatedStudentName, + onStudentEditingIdChange, + onUpdatedStudentNameChange, + onUpdateStudent, + assigningStudentId, + selectedAssignmentId, + onAssigningStudentIdChange, + onSelectedAssignmentIdChange, + onAssignAssignment, + onGradeAssignment, +}: StudentTableProps) { + const assignmentsById: Record = Object.fromEntries( + assignments.map((a) => [a.id, a]) + ); + + return ( + + + + + + + + + + + {students.map((student) => ( + + + + + + + ))} + +
IdNameAssignmentsAction
{student.id}{student.name} +
    + {studentAssignments + .filter((sa) => sa.studentId === student.id) + .map((sa) => ( +
  • + {assignmentsById[sa.assignmentId]?.title ?? "—"} ( + {sa.status}) + + +
  • + ))} +
+ {student.id === assigningStudentId ? ( + <> + + + + ) : ( + + )} +
+ {student.id === studentEditingId ? ( + <> + + onUpdatedStudentNameChange(e.target.value) + } + aria-label={`Edit name for ${student.name}`} + /> + + + ) : ( + + )} +
+ ); +} diff --git a/react/src/components/TeacherForm.tsx b/react/src/components/TeacherForm.tsx new file mode 100644 index 0000000..dde9f54 --- /dev/null +++ b/react/src/components/TeacherForm.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import { SchoolActionKind, useSchoolDispatch } from "../school-context"; + +export function TeacherForm() { + const schoolDispatch = useSchoolDispatch(); + const [name, setName] = useState(""); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const trimmed = name.trim(); + if (!trimmed) return; + const id = crypto.randomUUID(); + schoolDispatch?.({ + type: SchoolActionKind.ADD_TEACHER, + payload: { name: trimmed, id, students: [] }, + }); + setName(""); + }; + + const isValid = name.trim() !== ""; + + return ( +
+ + setName(e.target.value)} + aria-invalid={!isValid && name.length > 0} + /> + +
+ ); +} diff --git a/react/src/components/TeacherTable.tsx b/react/src/components/TeacherTable.tsx new file mode 100644 index 0000000..4c08c83 --- /dev/null +++ b/react/src/components/TeacherTable.tsx @@ -0,0 +1,88 @@ +import type { Student, Teacher } from "../school-context"; + +type TeacherTableProps = { + teachers: Teacher[]; + students: Student[]; + teacherEditingId: string | null; + newAssignedStudentId: string | null; + onTeacherEditingIdChange: (id: string | null) => void; + onNewAssignedStudentIdChange: (id: string | null) => void; + onAssignStudent: () => void; +}; + +export function TeacherTable({ + teachers, + students, + teacherEditingId, + newAssignedStudentId, + onTeacherEditingIdChange, + onNewAssignedStudentIdChange, + onAssignStudent, +}: TeacherTableProps) { + const studentById: Record = Object.fromEntries( + students.map((s) => [s.id, s]) + ); + + return ( + + + + + + + + + + {teachers.map((teacher) => ( + + + + + + ))} + +
IdNameAction
{teacher.id}{teacher.name} +
    + {teacher.students.map((studentId) => ( +
  • + {studentById[studentId]?.name ?? "—"} +
  • + ))} +
+ {teacher.id === teacherEditingId ? ( + <> + + + + ) : ( + + )} +
+ ); +} diff --git a/react/tests/app.test.tsx b/react/tests/app.test.tsx new file mode 100644 index 0000000..b053002 --- /dev/null +++ b/react/tests/app.test.tsx @@ -0,0 +1,38 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import App from "../src/App"; +import { SchoolProvider } from "../src/school-context"; + +function renderApp() { + return render( + + + + ); +} + +describe("App integration", () => { + it("allows creating entities and grading assignments", () => { + renderApp(); + + // Add a teacher + fireEvent.change(screen.getByLabelText(/teacher name/i), { + target: { value: "Teacher 1" }, + }); + fireEvent.click(screen.getByRole("button", { name: /add teacher/i })); + + // Add a student + fireEvent.change(screen.getByLabelText(/student name/i), { + target: { value: "Alice" }, + }); + fireEvent.click(screen.getByRole("button", { name: /add student/i })); + expect(screen.getByText("Alice")).toBeInTheDocument(); + + // Add an assignment + fireEvent.change(screen.getByLabelText(/new assignment/i), { + target: { value: "Math HW" }, + }); + fireEvent.click(screen.getByRole("button", { name: /add assignment/i })); + expect(screen.getByText("Math HW")).toBeInTheDocument(); + }); +}); + diff --git a/react/tests/assignment-form.test.tsx b/react/tests/assignment-form.test.tsx new file mode 100644 index 0000000..a6e4117 --- /dev/null +++ b/react/tests/assignment-form.test.tsx @@ -0,0 +1,49 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { AssignmentForm } from "../src/components/AssignmentForm"; + +describe("AssignmentForm", () => { + const originalCrypto = globalThis.crypto; + + beforeEach(() => { + // @ts-expect-error allow test double + globalThis.crypto = { randomUUID: () => "assignment-id-1" }; + }); + + afterEach(() => { + globalThis.crypto = originalCrypto; + }); + + it("disables submit button when input is empty or whitespace", () => { + render(); + + const button = screen.getByRole("button", { name: /add assignment/i }); + const input = screen.getByLabelText(/new assignment/i); + + expect(button).toBeDisabled(); + + fireEvent.change(input, { target: { value: " " } }); + expect(button).toBeDisabled(); + }); + + it("enables submit button when input has text", () => { + render(); + + const button = screen.getByRole("button", { name: /add assignment/i }); + const input = screen.getByLabelText(/new assignment/i); + + fireEvent.change(input, { target: { value: "Assignment 1" } }); + expect(button).toBeEnabled(); + }); + + it("clears the input after submit", () => { + render(); + + const input = screen.getByLabelText(/new assignment/i) as HTMLInputElement; + + fireEvent.change(input, { target: { value: "Assignment 1" } }); + fireEvent.submit(input.form!); + + expect(input.value).toBe(""); + }); +}); + diff --git a/react/tests/student-form.test.tsx b/react/tests/student-form.test.tsx new file mode 100644 index 0000000..c2642fb --- /dev/null +++ b/react/tests/student-form.test.tsx @@ -0,0 +1,49 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { StudentForm } from "../src/components/StudentForm"; + +describe("StudentForm", () => { + const originalCrypto = globalThis.crypto; + + beforeEach(() => { + // Ensure we have a crypto implementation for tests + // @ts-expect-error allow test double + globalThis.crypto = { randomUUID: () => "student-id-1" }; + }); + + afterEach(() => { + globalThis.crypto = originalCrypto; + }); + + it("disables submit button when input is empty or whitespace", () => { + render(); + + const button = screen.getByRole("button", { name: /add student/i }); + const input = screen.getByLabelText(/student name/i); + + expect(button).toBeDisabled(); + + fireEvent.change(input, { target: { value: " " } }); + expect(button).toBeDisabled(); + }); + + it("enables submit button when input has text", () => { + render(); + + const button = screen.getByRole("button", { name: /add student/i }); + const input = screen.getByLabelText(/student name/i); + + fireEvent.change(input, { target: { value: "Alice" } }); + expect(button).toBeEnabled(); + }); + + it("clears the input after submit", () => { + render(); + + const input = screen.getByLabelText(/student name/i) as HTMLInputElement; + + fireEvent.change(input, { target: { value: "Bob" } }); + fireEvent.submit(input.form!); + + expect(input.value).toBe(""); + }); +}); diff --git a/react/tests/student-table.test.tsx b/react/tests/student-table.test.tsx new file mode 100644 index 0000000..74c92bf --- /dev/null +++ b/react/tests/student-table.test.tsx @@ -0,0 +1,223 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { StudentTable } from "../src/components/StudentTable"; +import type { + Assignment, + Student, + StudentAssignment, +} from "../src/school-context"; + +const students: Student[] = [ + { id: "s1", name: "Alice" }, + { id: "s2", name: "Bob" }, +]; + +const assignments: Assignment[] = [ + { id: "a1", title: "Math" }, + { id: "a2", title: "Science" }, +]; + +const studentAssignments: StudentAssignment[] = [ + { + id: "sa1", + studentId: "s1", + assignmentId: "a1", + status: "assigned", + date: "2024-01-01T00:00:00.000Z", + }, + { + id: "sa2", + studentId: "s1", + assignmentId: "a2", + status: "pass", + date: "2024-01-01T00:00:00.000Z", + }, +]; + +describe("StudentTable", () => { + it("renders students and their assignments", () => { + render( + {}} + onUpdatedStudentNameChange={() => {}} + onUpdateStudent={() => {}} + assigningStudentId={null} + selectedAssignmentId={null} + onAssigningStudentIdChange={() => {}} + onSelectedAssignmentIdChange={() => {}} + onAssignAssignment={() => {}} + onGradeAssignment={() => {}} + /> + ); + + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText(/Math/)).toBeInTheDocument(); + expect(screen.getByText(/Science/)).toBeInTheDocument(); + }); + + it("calls onGradeAssignment when clicking pass/fail", () => { + const onGradeAssignment = vi.fn(); + + render( + {}} + onUpdatedStudentNameChange={() => {}} + onUpdateStudent={() => {}} + assigningStudentId={null} + selectedAssignmentId={null} + onAssigningStudentIdChange={() => {}} + onSelectedAssignmentIdChange={() => {}} + onAssignAssignment={() => {}} + onGradeAssignment={onGradeAssignment} + /> + ); + + const passButtons = screen.getAllByRole("button", { name: /pass/i }); + fireEvent.click(passButtons[0]); + + expect(onGradeAssignment).toHaveBeenCalledWith("sa1", "pass"); + }); + + it("enters edit mode for a student and calls onUpdateStudent", () => { + const onStudentEditingIdChange = vi.fn(); + const onUpdateStudent = vi.fn(); + + const { rerender } = render( + {}} + onUpdateStudent={onUpdateStudent} + assigningStudentId={null} + selectedAssignmentId={null} + onAssigningStudentIdChange={() => {}} + onSelectedAssignmentIdChange={() => {}} + onAssignAssignment={() => {}} + onGradeAssignment={() => {}} + /> + ); + + const updateButton = screen.getByRole("button", { name: /update alice/i }); + fireEvent.click(updateButton); + expect(onStudentEditingIdChange).toHaveBeenCalledWith("s1"); + + rerender( + {}} + onUpdateStudent={onUpdateStudent} + assigningStudentId={null} + selectedAssignmentId={null} + onAssigningStudentIdChange={() => {}} + onSelectedAssignmentIdChange={() => {}} + onAssignAssignment={() => {}} + onGradeAssignment={() => {}} + /> + ); + + const doneButton = screen.getByRole("button", { name: /save student name/i }); + fireEvent.click(doneButton); + + expect(onUpdateStudent).toHaveBeenCalled(); + }); + + it("allows selecting assignment for a student when in assigning mode", () => { + const onAssigningStudentIdChange = vi.fn(); + const onSelectedAssignmentIdChange = vi.fn(); + const onAssignAssignment = vi.fn(); + + const { rerender } = render( + {}} + onUpdatedStudentNameChange={() => {}} + onUpdateStudent={() => {}} + assigningStudentId={null} + selectedAssignmentId={null} + onAssigningStudentIdChange={onAssigningStudentIdChange} + onSelectedAssignmentIdChange={onSelectedAssignmentIdChange} + onAssignAssignment={onAssignAssignment} + onGradeAssignment={() => {}} + /> + ); + + const assignButton = screen.getByRole("button", { + name: /assign an assignment to alice/i, + }); + fireEvent.click(assignButton); + expect(onAssigningStudentIdChange).toHaveBeenCalledWith("s1"); + + rerender( + {}} + onUpdatedStudentNameChange={() => {}} + onUpdateStudent={() => {}} + assigningStudentId="s1" + selectedAssignmentId={null} + onAssigningStudentIdChange={onAssigningStudentIdChange} + onSelectedAssignmentIdChange={onSelectedAssignmentIdChange} + onAssignAssignment={onAssignAssignment} + onGradeAssignment={() => {}} + /> + ); + + const select = screen.getByLabelText( + /choose assignment to assign to alice/i + ); + fireEvent.change(select, { target: { value: "a1" } }); + expect(onSelectedAssignmentIdChange).toHaveBeenCalledWith("a1"); + + rerender( + {}} + onUpdatedStudentNameChange={() => {}} + onUpdateStudent={() => {}} + assigningStudentId="s1" + selectedAssignmentId="a1" + onAssigningStudentIdChange={onAssigningStudentIdChange} + onSelectedAssignmentIdChange={onSelectedAssignmentIdChange} + onAssignAssignment={onAssignAssignment} + onGradeAssignment={() => {}} + /> + ); + + const assignConfirmButton = screen.getByRole("button", { + name: /assign selected assignment to alice/i, + }); + fireEvent.click(assignConfirmButton); + expect(onAssignAssignment).toHaveBeenCalled(); + }); +}); + diff --git a/react/tests/teacher-form.test.tsx b/react/tests/teacher-form.test.tsx new file mode 100644 index 0000000..79854f3 --- /dev/null +++ b/react/tests/teacher-form.test.tsx @@ -0,0 +1,49 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { TeacherForm } from "../src/components/TeacherForm"; + +describe("TeacherForm", () => { + const originalCrypto = globalThis.crypto; + + beforeEach(() => { + // @ts-expect-error allow test double + globalThis.crypto = { randomUUID: () => "teacher-id-1" }; + }); + + afterEach(() => { + globalThis.crypto = originalCrypto; + }); + + it("disables submit button when input is empty or whitespace", () => { + render(); + + const button = screen.getByRole("button", { name: /add teacher/i }); + const input = screen.getByLabelText(/teacher name/i); + + expect(button).toBeDisabled(); + + fireEvent.change(input, { target: { value: " " } }); + expect(button).toBeDisabled(); + }); + + it("enables submit button when input has text", () => { + render(); + + const button = screen.getByRole("button", { name: /add teacher/i }); + const input = screen.getByLabelText(/teacher name/i); + + fireEvent.change(input, { target: { value: "Alice" } }); + expect(button).toBeEnabled(); + }); + + it("clears the input after submit", () => { + render(); + + const input = screen.getByLabelText(/teacher name/i) as HTMLInputElement; + + fireEvent.change(input, { target: { value: "Bob" } }); + fireEvent.submit(input.form!); + + expect(input.value).toBe(""); + }); +}); + diff --git a/react/tests/teacher-table.test.tsx b/react/tests/teacher-table.test.tsx new file mode 100644 index 0000000..aacd75b --- /dev/null +++ b/react/tests/teacher-table.test.tsx @@ -0,0 +1,84 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { TeacherTable } from "../src/components/TeacherTable"; +import type { Student, Teacher } from "../src/school-context"; + +function createTeachers(): Teacher[] { + return [ + { id: "t1", name: "Teacher 1", students: ["s1"] }, + { id: "t2", name: "Teacher 2", students: [] }, + ]; +} + +function createStudents(): Student[] { + return [ + { id: "s1", name: "Alice" }, + { id: "s2", name: "Bob" }, + ]; +} + +describe("TeacherTable", () => { + it("renders teacher rows and assigned student names", () => { + render( + {}} + onNewAssignedStudentIdChange={() => {}} + onAssignStudent={() => {}} + /> + ); + + expect(screen.getByText("Teacher 1")).toBeInTheDocument(); + expect(screen.getByText("Teacher 2")).toBeInTheDocument(); + expect(screen.getByText("Alice")).toBeInTheDocument(); + }); + + it("shows assign select and button when a teacher is in edit mode", () => { + const onNewAssignedStudentIdChange = vi.fn(); + const onAssignStudent = vi.fn(); + + render( + {}} + onNewAssignedStudentIdChange={onNewAssignedStudentIdChange} + onAssignStudent={onAssignStudent} + /> + ); + + const select = screen.getByLabelText(/choose student to assign/i); + expect(select).toBeInTheDocument(); + + fireEvent.change(select, { target: { value: "s2" } }); + expect(onNewAssignedStudentIdChange).toHaveBeenCalledWith("s2"); + }); + + it("calls onTeacherEditingIdChange when clicking 'Assign student' button", () => { + const onTeacherEditingIdChange = vi.fn(); + + render( + {}} + onAssignStudent={() => {}} + /> + ); + + const assignButton = screen.getByRole("button", { + name: /assign a student to teacher 1/i, + }); + + fireEvent.click(assignButton); + expect(onTeacherEditingIdChange).toHaveBeenCalledWith("t1"); + }); +}); + From 4dda094e6c9dc3afcda5a29114bd19adf33bd905 Mon Sep 17 00:00:00 2001 From: Luis Mendes Date: Tue, 3 Mar 2026 13:20:42 +0000 Subject: [PATCH 2/2] Implement assignments and container refactor --- INTERVIEW-NOTES.md | 222 ----------------- react/src/App.tsx | 182 +------------- .../{ => AssignmentForm}/AssignmentForm.tsx | 2 +- react/src/components/AssignmentForm/index.ts | 2 + react/src/components/Student/Student.css | 0 react/src/components/Student/Student.spec.tsx | 0 react/src/components/Student/Student.tsx | 0 react/src/components/Student/index.ts | 0 .../{ => StudentForm}/StudentForm.tsx | 3 +- react/src/components/StudentForm/index.ts | 1 + .../{ => StudentTable}/StudentTable.tsx | 3 +- react/src/components/StudentTable/index.ts | 2 + .../{ => TeacherForm}/TeacherForm.tsx | 3 +- react/src/components/TeacherForm/index.ts | 2 + .../{ => TeacherTable}/TeacherTable.tsx | 3 +- react/src/components/TeacherTable/index.ts | 2 + react/src/containers/School.tsx | 194 +++++++++++++++ react/src/school-context.tsx | 78 +++++- react/tests/assignment-form.test.tsx | 49 ---- react/tests/student-form.test.tsx | 49 ---- react/tests/student-table.test.tsx | 223 ------------------ react/tests/teacher-form.test.tsx | 49 ---- react/tests/teacher-table.test.tsx | 84 ------- 23 files changed, 289 insertions(+), 864 deletions(-) delete mode 100644 INTERVIEW-NOTES.md rename react/src/components/{ => AssignmentForm}/AssignmentForm.tsx (92%) create mode 100644 react/src/components/AssignmentForm/index.ts create mode 100644 react/src/components/Student/Student.css create mode 100644 react/src/components/Student/Student.spec.tsx create mode 100644 react/src/components/Student/Student.tsx create mode 100644 react/src/components/Student/index.ts rename react/src/components/{ => StudentForm}/StudentForm.tsx (92%) create mode 100644 react/src/components/StudentForm/index.ts rename react/src/components/{ => StudentTable}/StudentTable.tsx (99%) create mode 100644 react/src/components/StudentTable/index.ts rename react/src/components/{ => TeacherForm}/TeacherForm.tsx (92%) create mode 100644 react/src/components/TeacherForm/index.ts rename react/src/components/{ => TeacherTable}/TeacherTable.tsx (97%) create mode 100644 react/src/components/TeacherTable/index.ts create mode 100644 react/src/containers/School.tsx delete mode 100644 react/tests/assignment-form.test.tsx delete mode 100644 react/tests/student-form.test.tsx delete mode 100644 react/tests/student-table.test.tsx delete mode 100644 react/tests/teacher-form.test.tsx delete mode 100644 react/tests/teacher-table.test.tsx diff --git a/INTERVIEW-NOTES.md b/INTERVIEW-NOTES.md deleted file mode 100644 index 15042e3..0000000 --- a/INTERVIEW-NOTES.md +++ /dev/null @@ -1,222 +0,0 @@ -## Infinitas LMS React exercise – study notes - -### 1. Big picture - -- **Goal**: Work on a small React-based LMS that manages teachers and students, then extend it toward assignments, grading (Pass/Fail), and simple reporting. -- **Timebox**: 45–60 minutes. The aim is **how you think, communicate, and structure code**, not finishing everything. -- **Your narrative**: “I’ll first understand the current design, then refactor the rough edges that slow development, then add a thin vertical slice of a new feature with tests.” - -### 2. Make sure the project runs - -Inside the `react` folder: - -- **Install**: `npm install` -- **Run tests**: `npm test` -- **Run dev server**: `npm run dev` and open the app. - -Be ready to briefly explain what each script does (Vite dev server, TypeScript build, Vitest tests). - -### 3. Understand the current React architecture - -- **State management** - - `SchoolProvider` wraps the app and uses `useReducer` with `schoolReducer`. - - **State shape**: - - `students: { id: string; name: string }[]` - - `teachers: { id: string; name: string; students: string[] }[]` (array of student IDs). - - **Actions** (`SchoolActionKind`): - - `ADD_TEACHER` - - `ADD_STUDENT` - - `UPDATE_STUDENT` - - `ASSIGN_STUDENT_TO_TEACHER` - - **Reducer patterns**: - - Adds are done by spreading existing arrays and appending. - - Updates are done via loops that build new arrays (immutable updates). - -- **UI (`App.tsx`)** - - Uses `useSchool()` to read state and `useSchoolDispatch()` to dispatch actions. - - Forms: - - Add teacher (creates UUID, dispatches `ADD_TEACHER`). - - Add student (creates UUID, dispatches `ADD_STUDENT`). - - Editing: - - Student name editing tracked with `studentEditingId` and `updatedStudentName`. - - Teacher assignment tracked with `teacherEditingId` and `newAssignedStudentId`. - - Data flow: - - Teachers table shows each teacher and lists assigned students by mapping IDs to names. - -Be ready to restate this architecture in your own words. - -### 4. Obvious refactors you can talk about (and maybe do) - -- **Rendering inefficiencies / clarity** - - Teacher student list currently maps `teacher.students` and then, *inside each*, maps all `school.students` to find a name: - - You can mention this is an O(n²) pattern and hard to read. - - Possible improvements: - - Precompute a `studentById` map (e.g. `const studentById = new Map(students.map(s => [s.id, s]));`). - - Or create a selector/helper function to map an ID to a name. - -- **Keys and list rendering** - - `li` and `option` elements should ideally have `key` props; you can mention this as a React best practice. - -#### 4.2 Second refactor: Reducer clarity (implemented) - -**What's wrong** -- `UPDATE_STUDENT` used a for-loop to build a new array; more verbose than using `.map()`. - -**How to approach it during the interview** - -1. **Name the problem**: "In the reducer, `UPDATE_STUDENT` builds a new students array with a for-loop. I'd refactor that to use `.map()` so the intent is clear: replace this one item, keep the rest the same." -2. **Propose the fix**: "Use `state.students.map(s => s.id === action.payload.id ? action.payload : s)` and return `{ ...state, students }`. Same for the teacher update if it's still a loop." -3. **Do it**: Change the `UPDATE_STUDENT` case (and `ASSIGN_STUDENT_TO_TEACHER` if needed), run `npm test`, then quickly test update student in the UI. -4. **Mention tests**: "The existing reducer test still passes. Reducers are easy to unit test." - -**Implementation**: Done in `react/src/school-context.tsx` — both cases now use `.map()`. - -- **Reducer clarity** (reference) - - Loops that build new arrays could be turned into `map` for readability: - - `state.students.map(s => s.id === action.payload.id ? action.payload : s);` - - Similarly for updating teacher’s `students` array. - -#### 4.3 Third refactor: Component extraction (implemented) - -**What's wrong** -- `App.tsx` holds all UI: two tables, two forms, and all state. Hard to scan and harder to test or change one area without touching the rest. - -**How to approach it during the interview** - -1. **Name the problem**: "App is doing a lot—teacher and student tables and forms plus editing state. I'd extract smaller components so each file has one job and we can test or change one area without touching the others." -2. **Propose the split**: "I'll pull out `TeacherTable`, `TeacherForm`, `StudentTable`, and `StudentForm`. The tables need data and callbacks for editing; the forms can use the context dispatch directly. App stays as the place that holds the editing state and passes it down." -3. **Do it in order**: Extract one piece at a time (e.g. `TeacherForm` first—no props, just move the form and the submit handler). Then the table with props. Then the same for students. Run the app after each step. -4. **Mention benefits**: "Now we could unit-test the reducer and add simple tests for a form or table in isolation. Adding the assignment feature later, we might add an `AssignmentForm` or extend the student row without cluttering App." - -**Implementation**: `react/src/components/TeacherTable.tsx`, `TeacherForm.tsx`, `StudentTable.tsx`, `StudentForm.tsx`. App imports them and passes teachers/students plus editing state and callbacks. TeacherTable uses a `studentById` map for O(1) name lookup. - -#### 4.4 Fourth refactor: Validation and UX (implemented) - -**What's wrong** -- Forms allow submitting with blank or whitespace-only names. -- "Assign" can be clicked with no student selected; "Done" can be clicked with an empty name when editing a student. -- Labels and buttons are mostly fine; small improvements (e.g. clearer button labels) help accessibility. - -**How to approach it during the interview** - -1. **Name the problem**: "Right now we can add a teacher or student with an empty name, and we can click Assign without picking a student or Done with an empty name. I'd add simple validation and disable those actions when the input isn't valid." -2. **Propose the fix**: "In the add forms I'll use controlled inputs and disable the submit button when the trimmed value is empty. In the tables I'll disable Assign when no student is selected and Done when the name is empty. We already have htmlFor/id on the form labels." -3. **Do it**: Add state for the form inputs, disable submit when `value.trim() === ''`. Disable Assign when `!newAssignedStudentId`, Done when `updatedStudentName.trim() === ''`. Optionally add aria attributes if we show an error." -4. **Mention trade-offs**: "We could show an error message instead of only disabling; disabling is simpler and avoids extra state. For production we might add a toast or inline error." - -**Implementation**: TeacherForm and StudentForm use controlled inputs and disable submit when name is empty. TeacherTable disables Assign when no student selected; StudentTable disables Done when name is empty. - -Pick 1–2 of these to mention/implement to show pragmatic refactoring, not perfectionism. - -### 5. Likely feature extensions (assignments, grading, reporting) - -#### 5.1 First slice: assignments and per-student assignment list (implemented) - -**What’s implemented** -- Extended state with normalized assignment data: - - `assignments: { id: string; title: string }[]` - - `studentAssignments: { id: string; studentId: string; assignmentId: string; status: "assigned" | "pass" | "fail"; date: string }[]` -- New actions: - - `ADD_ASSIGNMENT` - - `ASSIGN_ASSIGNMENT_TO_STUDENT` - - `GRADE_ASSIGNMENT` (status update only; UI still to come if needed). -- UI: - - `AssignmentForm` component to create assignments. - - `StudentTable` now shows each student’s assignments (title + status) and lets you assign an assignment to a student via a dropdown. - -**How to approach it during the interview** - -1. **State design**: “For assignments I’d like a normalized shape: an `assignments` collection and a `studentAssignments` collection that links students to assignments with a status and date. That makes reporting like ‘how many students passed assignment X on a date’ straightforward.” -2. **Reducer changes**: “I’ll add `ADD_ASSIGNMENT`, `ASSIGN_ASSIGNMENT_TO_STUDENT`, and `GRADE_ASSIGNMENT` to the reducer. Each case returns a new state: append an assignment, append a student-assignment record, or update the status of one record.” -3. **UI slice**: “I’ll implement one vertical slice: a small Assignments form to create assignments, and in the student table an ‘Assign assignment’ flow: click a button, pick an assignment from a select, and dispatch `ASSIGN_ASSIGNMENT_TO_STUDENT`. I’ll also list each student’s assignments and their status.” -4. **Future grading/reporting**: “With `studentAssignments` in place, grading is just toggling status on a record, and reporting is filtering that array by assignment and date. If we had more time I’d add Pass/Fail buttons and a small reporting panel.” - -**Files to point to in the code** -- Types, state, actions, and reducer cases: `react/src/school-context.tsx`. -- Assignments UI and wiring: - - `react/src/components/AssignmentForm.tsx` - - `react/src/components/StudentTable.tsx` (assignments column and assign flow) - - `react/src/App.tsx` (assigning state and dispatch call) - -The README lists three capabilities: - -1. **Assign an assignment to a student** -2. **Grade an assignment Pass/Fail** -3. **Basic reporting on how many students passed an assignment on a given day** - -Prepare a simple design you can describe and partially implement. - -- **Data model (front-end version) – one reasonable approach** - - Add an `Assignment` concept and track per-student status: - - `assignments: { id: string; title: string }[]` - - `studentAssignments: { id: string; studentId: string; assignmentId: string; status: "assigned" | "pass" | "fail"; date: string }[]` - - Or, if they want to keep it minimal, you can nest assignments under `Student`: - - `Student` becomes `{ id; name; assignments: { id; title; status; date }[] }`. - - Be ready to explain trade‑offs: - - Separate collections = more normalized, easier reporting. - - Nested under `Student` = simpler code initially but harder cross‑student queries. - -- **Reducer changes you might propose** - - New action types, for example: - - `ADD_ASSIGNMENT` - - `ASSIGN_ASSIGNMENT_TO_STUDENT` - - `GRADE_ASSIGNMENT` - - Show you know how to: - - Extend `InitialState` with new fields. - - Extend `SchoolActionKind` and `SchoolAction` union. - - Update `schoolReducer` immutably. - -- **UI slice to implement** - - Aim for **one vertical slice**, e.g.: - - A small “Assignments” section to create an assignment. - - In the students table, add a button “Assign assignment” that: - - Opens a select of assignments. - - Dispatches `ASSIGN_ASSIGNMENT_TO_STUDENT`. - - Or focus on grading: - - Show a list of a student’s assignments with buttons “Pass” / “Fail”. - -### 6. Reporting idea (even if you don’t implement fully) - -- **Requirement**: “How many students passed an assignment on a given day?” -- If using `studentAssignments` with `status` and `date`: - - A selector/helper function could: - - Filter by `assignmentId` and `date`. - - Count how many have `status === "pass"`. - - In the UI, a simple reporting panel could: - - Let you select an assignment and date (or use today). - - Display: `X students passed / Y students assigned`. - -Even if you don’t code this, be ready to walk through how you’d do it. - -### 7. Testing strategy (Vitest) - -- **Existing test**: `school-reducer.test.ts` verifies `ADD_TEACHER`. -- How to extend: - - Add tests for: - - `ADD_STUDENT` (length increases, correct data). - - `UPDATE_STUDENT` (only one student’s name changes). - - `ASSIGN_STUDENT_TO_TEACHER` (teacher gains the student ID, no mutation of initial arrays). - - If you add new actions, write tests **first** or at least alongside changes. - -Talking points: - -- You value reducers because they are easy to unit test in isolation. -- You like to guard complex state updates with focused tests. - -### 8. How to approach the session - -- **First 5–10 minutes** - - Skim `App.tsx` and `school-context.tsx`, describe current design out loud. - - Call out one or two refactor targets and explain *why* they’d help future work. - -- **Next 30–40 minutes** - - Implement a small refactor (e.g. improve how teacher’s students are rendered). - - Add or update at least one reducer test. - - Add a small part of an assignment/grading feature (data model + one UI interaction). - -- **Final minutes** - - Reflect: what you’d do next (more tests, deeper refactors, error handling, accessibility). - - Emphasize you think in terms of maintainability, correctness, and user experience. - -If you rehearse explaining these points and can comfortably modify the reducer and UI, you’ll be well prepared for the exercise. - diff --git a/react/src/App.tsx b/react/src/App.tsx index 864aaa8..1e3d01e 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -1,79 +1,9 @@ -import { useState } from "react"; -import { - SchoolActionKind, - useSchool, - useSchoolDispatch, -} from "./school-context"; import infinitasLogo from "/infinitas-logo.svg"; import "./App.css"; +import { SchoolProvider } from "./school-context"; +import { School } from "./containers/School"; function App() { - const school = useSchool(); - const schoolDispatch = useSchoolDispatch(); - - const [studentEditingId, setUserEditingId] = useState(null); - const [updatedStudentName, setUpdatedStudentName] = useState(""); - - const [teacherEditingId, setTeacherEditingId] = useState(null); - const [newAssignedStudentId, setNewAssignedStudentId] = useState< - string | null - >(null); - - const handleTeacherSubmit = (event: React.FormEvent) => { - event.preventDefault(); - - const target = event.currentTarget; - const teacherName = target.teacher.value; - const id = crypto.randomUUID(); - schoolDispatch?.({ - type: SchoolActionKind.ADD_TEACHER, - payload: { name: teacherName, id, students: [] }, - }); - - target.reset(); - }; - - const handleStudentSubmit = (event: React.FormEvent) => { - event.preventDefault(); - - const target = event.currentTarget; - const studentName = target.student.value; - const id = crypto.randomUUID(); - schoolDispatch?.({ - type: SchoolActionKind.ADD_STUDENT, - payload: { name: studentName, id }, - }); - - target.reset(); - }; - - const handleUpdateStudent = () => { - if (studentEditingId) { - schoolDispatch?.({ - type: SchoolActionKind.UPDATE_STUDENT, - payload: { name: updatedStudentName, id: studentEditingId }, - }); - } - - setUserEditingId(null); - setUpdatedStudentName(""); - }; - - const handleAssignStudent = () => { - if (teacherEditingId && newAssignedStudentId) { - schoolDispatch?.({ - type: SchoolActionKind.ASSIGN_STUDENT_TO_TEACHER, - payload: { - teacherId: teacherEditingId, - studentId: newAssignedStudentId, - }, - }); - } - - setTeacherEditingId(null); - setNewAssignedStudentId(null); - }; - return (
@@ -82,111 +12,9 @@ function App() {

IL Interview

-
-

Teacher

- - - - - - - - - - {school?.teachers.map((teacher) => { - return ( - - - - - - ); - })} - -
IdNameAction
{teacher.id}{teacher.name} -
    - {teacher.students.map((s) => ( -
  • - {school?.students.map((s1) => - s === s1.id ? s1.name : "" - )} -
  • - ))} -
- {teacher.id === teacherEditingId ? ( - <> - - - - ) : ( - - )} -
-
-
- - - -
-
-
-

Students

- - - - - - - - - - {school?.students.map((student) => { - return ( - - - - - - ); - })} - -
IdNameAction
{student.id}{student.name} - {student.id === studentEditingId ? ( - <> - - setUpdatedStudentName(e.target.value) - } - > - - - ) : ( - - )} -
-
-
- - - -
-
+ + +
); } diff --git a/react/src/components/AssignmentForm.tsx b/react/src/components/AssignmentForm/AssignmentForm.tsx similarity index 92% rename from react/src/components/AssignmentForm.tsx rename to react/src/components/AssignmentForm/AssignmentForm.tsx index a35b1e8..9b764a0 100644 --- a/react/src/components/AssignmentForm.tsx +++ b/react/src/components/AssignmentForm/AssignmentForm.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { SchoolActionKind, useSchoolDispatch } from "../school-context"; +import { SchoolActionKind, useSchoolDispatch } from "../../school-context"; export function AssignmentForm() { const dispatch = useSchoolDispatch(); diff --git a/react/src/components/AssignmentForm/index.ts b/react/src/components/AssignmentForm/index.ts new file mode 100644 index 0000000..35c1bb9 --- /dev/null +++ b/react/src/components/AssignmentForm/index.ts @@ -0,0 +1,2 @@ +export * from "./AssignmentForm"; + diff --git a/react/src/components/Student/Student.css b/react/src/components/Student/Student.css new file mode 100644 index 0000000..e69de29 diff --git a/react/src/components/Student/Student.spec.tsx b/react/src/components/Student/Student.spec.tsx new file mode 100644 index 0000000..e69de29 diff --git a/react/src/components/Student/Student.tsx b/react/src/components/Student/Student.tsx new file mode 100644 index 0000000..e69de29 diff --git a/react/src/components/Student/index.ts b/react/src/components/Student/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/react/src/components/StudentForm.tsx b/react/src/components/StudentForm/StudentForm.tsx similarity index 92% rename from react/src/components/StudentForm.tsx rename to react/src/components/StudentForm/StudentForm.tsx index b5642b0..be0faf4 100644 --- a/react/src/components/StudentForm.tsx +++ b/react/src/components/StudentForm/StudentForm.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { SchoolActionKind, useSchoolDispatch } from "../school-context"; +import { SchoolActionKind, useSchoolDispatch } from "../../school-context"; export function StudentForm() { const schoolDispatch = useSchoolDispatch(); @@ -36,3 +36,4 @@ export function StudentForm() { ); } + diff --git a/react/src/components/StudentForm/index.ts b/react/src/components/StudentForm/index.ts new file mode 100644 index 0000000..8109a51 --- /dev/null +++ b/react/src/components/StudentForm/index.ts @@ -0,0 +1 @@ +export * from "./StudentForm"; diff --git a/react/src/components/StudentTable.tsx b/react/src/components/StudentTable/StudentTable.tsx similarity index 99% rename from react/src/components/StudentTable.tsx rename to react/src/components/StudentTable/StudentTable.tsx index d0e5b07..d12eaa5 100644 --- a/react/src/components/StudentTable.tsx +++ b/react/src/components/StudentTable/StudentTable.tsx @@ -2,7 +2,7 @@ import type { Assignment, Student, StudentAssignment, -} from "../school-context"; +} from "../../school-context"; type StudentTableProps = { students: Student[]; @@ -157,3 +157,4 @@ export function StudentTable({ ); } + diff --git a/react/src/components/StudentTable/index.ts b/react/src/components/StudentTable/index.ts new file mode 100644 index 0000000..ba055ce --- /dev/null +++ b/react/src/components/StudentTable/index.ts @@ -0,0 +1,2 @@ +export * from "./StudentTable"; + diff --git a/react/src/components/TeacherForm.tsx b/react/src/components/TeacherForm/TeacherForm.tsx similarity index 92% rename from react/src/components/TeacherForm.tsx rename to react/src/components/TeacherForm/TeacherForm.tsx index dde9f54..a713d67 100644 --- a/react/src/components/TeacherForm.tsx +++ b/react/src/components/TeacherForm/TeacherForm.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { SchoolActionKind, useSchoolDispatch } from "../school-context"; +import { SchoolActionKind, useSchoolDispatch } from "../../school-context"; export function TeacherForm() { const schoolDispatch = useSchoolDispatch(); @@ -36,3 +36,4 @@ export function TeacherForm() { ); } + diff --git a/react/src/components/TeacherForm/index.ts b/react/src/components/TeacherForm/index.ts new file mode 100644 index 0000000..25514ed --- /dev/null +++ b/react/src/components/TeacherForm/index.ts @@ -0,0 +1,2 @@ +export * from "./TeacherForm"; + diff --git a/react/src/components/TeacherTable.tsx b/react/src/components/TeacherTable/TeacherTable.tsx similarity index 97% rename from react/src/components/TeacherTable.tsx rename to react/src/components/TeacherTable/TeacherTable.tsx index 4c08c83..fafd8d4 100644 --- a/react/src/components/TeacherTable.tsx +++ b/react/src/components/TeacherTable/TeacherTable.tsx @@ -1,4 +1,4 @@ -import type { Student, Teacher } from "../school-context"; +import type { Student, Teacher } from "../../school-context"; type TeacherTableProps = { teachers: Teacher[]; @@ -86,3 +86,4 @@ export function TeacherTable({ ); } + diff --git a/react/src/components/TeacherTable/index.ts b/react/src/components/TeacherTable/index.ts new file mode 100644 index 0000000..85bf148 --- /dev/null +++ b/react/src/components/TeacherTable/index.ts @@ -0,0 +1,2 @@ +export * from "./TeacherTable"; + diff --git a/react/src/containers/School.tsx b/react/src/containers/School.tsx new file mode 100644 index 0000000..9bb508d --- /dev/null +++ b/react/src/containers/School.tsx @@ -0,0 +1,194 @@ +import { useState } from "react"; +import { + SchoolActionKind, + useSchool, + useSchoolDispatch, +} from "../school-context"; +import { TeacherForm } from "../components/TeacherForm/TeacherForm"; +import { StudentForm } from "../components/StudentForm/StudentForm"; +import { AssignmentForm } from "../components/AssignmentForm/AssignmentForm"; +import { TeacherTable } from "../components/TeacherTable/TeacherTable"; +import { StudentTable } from "../components/StudentTable/StudentTable"; + +export function School() { + const school = useSchool(); + const schoolDispatch = useSchoolDispatch(); + + const [studentEditingId, setUserEditingId] = useState(null); + const [updatedStudentName, setUpdatedStudentName] = useState(""); + + const [teacherEditingId, setTeacherEditingId] = useState(null); + const [newAssignedStudentId, setNewAssignedStudentId] = useState< + string | null + >(null); + + const [assigningStudentId, setAssigningStudentId] = useState( + null + ); + const [selectedAssignmentId, setSelectedAssignmentId] = useState< + string | null + >(null); + + const [reportAssignmentId, setReportAssignmentId] = useState(""); + const [reportDate, setReportDate] = useState(""); + + const handleUpdateStudent = () => { + if (studentEditingId && updatedStudentName.trim()) { + schoolDispatch?.({ + type: SchoolActionKind.UPDATE_STUDENT, + payload: { name: updatedStudentName.trim(), id: studentEditingId }, + }); + } + + setUserEditingId(null); + setUpdatedStudentName(""); + }; + + const handleAssignStudent = () => { + if (teacherEditingId && newAssignedStudentId) { + schoolDispatch?.({ + type: SchoolActionKind.ASSIGN_STUDENT_TO_TEACHER, + payload: { + teacherId: teacherEditingId, + studentId: newAssignedStudentId, + }, + }); + } + + setTeacherEditingId(null); + setNewAssignedStudentId(null); + }; + + const handleAssignAssignment = () => { + if (!assigningStudentId || !selectedAssignmentId) { + return; + } + + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + + schoolDispatch?.({ + type: SchoolActionKind.ASSIGN_ASSIGNMENT_TO_STUDENT, + payload: { + id, + studentId: assigningStudentId, + assignmentId: selectedAssignmentId, + status: "assigned", + date: now, + }, + }); + + setAssigningStudentId(null); + setSelectedAssignmentId(null); + }; + + const handleGradeAssignment = ( + studentAssignmentId: string, + status: "pass" | "fail" + ) => { + schoolDispatch?.({ + type: SchoolActionKind.GRADE_STUDENT_ASSIGNMENT, + payload: { + id: studentAssignmentId, + status, + }, + }); + }; + + const passedCount = + reportAssignmentId && reportDate && school + ? school.studentAssignments.filter( + (sa) => + sa.assignmentId === reportAssignmentId && + sa.status === "pass" && + sa.date.slice(0, 10) === reportDate + ).length + : 0; + + return ( + <> +
+

Teachers

+ +
+ +
+ +
+

Students

+ +
+ +
+ +
+

Assignments

+
    + {school?.assignments.map((assignment) => ( +
  • {assignment.title}
  • + ))} +
+
+ +
+ +
+

Reports

+
+ + +
+
+ + setReportDate(e.target.value)} + /> +
+ {reportAssignmentId && reportDate && ( +

+ {passedCount} student + {passedCount === 1 ? "" : "s"} passed this assignment on this day. +

+ )} +
+ + ); +} + diff --git a/react/src/school-context.tsx b/react/src/school-context.tsx index a6cd069..2bd9a1f 100644 --- a/react/src/school-context.tsx +++ b/react/src/school-context.tsx @@ -11,9 +11,30 @@ export type Teacher = { students: string[]; }; +export type Assignment = { + id: string; + title: string; +}; + +export type StudentAssignmentStatus = "assigned" | "pass" | "fail"; + +export type StudentAssignment = { + id: string; + studentId: string; + assignmentId: string; + status: StudentAssignmentStatus; + /** + * ISO string date-time for when the assignment was assigned or graded. + * Reporting groups by calendar day (YYYY-MM-DD). + */ + date: string; +}; + export type InitialState = { teachers: Teacher[]; students: Student[]; + assignments: Assignment[]; + studentAssignments: StudentAssignment[]; }; export enum SchoolActionKind { @@ -21,6 +42,9 @@ export enum SchoolActionKind { ADD_STUDENT = "ADD_STUDENT", UPDATE_STUDENT = "UPDATE_STUDENT", ASSIGN_STUDENT_TO_TEACHER = "ASSIGN_STUDENT_TO_TEACHER", + ADD_ASSIGNMENT = "ADD_ASSIGNMENT", + ASSIGN_ASSIGNMENT_TO_STUDENT = "ASSIGN_ASSIGNMENT_TO_STUDENT", + GRADE_STUDENT_ASSIGNMENT = "GRADE_STUDENT_ASSIGNMENT", } export type SchoolAction = @@ -42,6 +66,21 @@ export type SchoolAction = teacherId: string; studentId: string; }; + } + | { + type: SchoolActionKind.ADD_ASSIGNMENT; + payload: Assignment; + } + | { + type: SchoolActionKind.ASSIGN_ASSIGNMENT_TO_STUDENT; + payload: StudentAssignment; + } + | { + type: SchoolActionKind.GRADE_STUDENT_ASSIGNMENT; + payload: { + id: string; + status: Exclude; + }; }; const SchoolContext = createContext(null); @@ -87,19 +126,44 @@ export function schoolReducer( } } return { ...state, students: updatedStudents }; - case SchoolActionKind.ASSIGN_STUDENT_TO_TEACHER: - const updatedTeacher: Teacher[] = []; - for (let t of state.teachers) { + case SchoolActionKind.ASSIGN_STUDENT_TO_TEACHER: { + const updatedTeachers: Teacher[] = []; + for (const t of state.teachers) { if (t.id === action.payload.teacherId) { - updatedTeacher.push({ + updatedTeachers.push({ ...t, students: [...t.students, action.payload.studentId], }); } else { - updatedTeacher.push(t); + updatedTeachers.push(t); } } - return { ...state, teachers: updatedTeacher }; + return { ...state, teachers: updatedTeachers }; + } + case SchoolActionKind.ADD_ASSIGNMENT: + return { + ...state, + assignments: [...state.assignments, action.payload], + }; + case SchoolActionKind.ASSIGN_ASSIGNMENT_TO_STUDENT: + return { + ...state, + studentAssignments: [...state.studentAssignments, action.payload], + }; + case SchoolActionKind.GRADE_STUDENT_ASSIGNMENT: { + const updatedStudentAssignments: StudentAssignment[] = []; + for (const sa of state.studentAssignments) { + if (sa.id === action.payload.id) { + updatedStudentAssignments.push({ + ...sa, + status: action.payload.status, + }); + } else { + updatedStudentAssignments.push(sa); + } + } + return { ...state, studentAssignments: updatedStudentAssignments }; + } default: return state; } @@ -108,4 +172,6 @@ export function schoolReducer( const initialState: InitialState = { teachers: [], students: [], + assignments: [], + studentAssignments: [], }; diff --git a/react/tests/assignment-form.test.tsx b/react/tests/assignment-form.test.tsx deleted file mode 100644 index a6e4117..0000000 --- a/react/tests/assignment-form.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { AssignmentForm } from "../src/components/AssignmentForm"; - -describe("AssignmentForm", () => { - const originalCrypto = globalThis.crypto; - - beforeEach(() => { - // @ts-expect-error allow test double - globalThis.crypto = { randomUUID: () => "assignment-id-1" }; - }); - - afterEach(() => { - globalThis.crypto = originalCrypto; - }); - - it("disables submit button when input is empty or whitespace", () => { - render(); - - const button = screen.getByRole("button", { name: /add assignment/i }); - const input = screen.getByLabelText(/new assignment/i); - - expect(button).toBeDisabled(); - - fireEvent.change(input, { target: { value: " " } }); - expect(button).toBeDisabled(); - }); - - it("enables submit button when input has text", () => { - render(); - - const button = screen.getByRole("button", { name: /add assignment/i }); - const input = screen.getByLabelText(/new assignment/i); - - fireEvent.change(input, { target: { value: "Assignment 1" } }); - expect(button).toBeEnabled(); - }); - - it("clears the input after submit", () => { - render(); - - const input = screen.getByLabelText(/new assignment/i) as HTMLInputElement; - - fireEvent.change(input, { target: { value: "Assignment 1" } }); - fireEvent.submit(input.form!); - - expect(input.value).toBe(""); - }); -}); - diff --git a/react/tests/student-form.test.tsx b/react/tests/student-form.test.tsx deleted file mode 100644 index c2642fb..0000000 --- a/react/tests/student-form.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { StudentForm } from "../src/components/StudentForm"; - -describe("StudentForm", () => { - const originalCrypto = globalThis.crypto; - - beforeEach(() => { - // Ensure we have a crypto implementation for tests - // @ts-expect-error allow test double - globalThis.crypto = { randomUUID: () => "student-id-1" }; - }); - - afterEach(() => { - globalThis.crypto = originalCrypto; - }); - - it("disables submit button when input is empty or whitespace", () => { - render(); - - const button = screen.getByRole("button", { name: /add student/i }); - const input = screen.getByLabelText(/student name/i); - - expect(button).toBeDisabled(); - - fireEvent.change(input, { target: { value: " " } }); - expect(button).toBeDisabled(); - }); - - it("enables submit button when input has text", () => { - render(); - - const button = screen.getByRole("button", { name: /add student/i }); - const input = screen.getByLabelText(/student name/i); - - fireEvent.change(input, { target: { value: "Alice" } }); - expect(button).toBeEnabled(); - }); - - it("clears the input after submit", () => { - render(); - - const input = screen.getByLabelText(/student name/i) as HTMLInputElement; - - fireEvent.change(input, { target: { value: "Bob" } }); - fireEvent.submit(input.form!); - - expect(input.value).toBe(""); - }); -}); diff --git a/react/tests/student-table.test.tsx b/react/tests/student-table.test.tsx deleted file mode 100644 index 74c92bf..0000000 --- a/react/tests/student-table.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { StudentTable } from "../src/components/StudentTable"; -import type { - Assignment, - Student, - StudentAssignment, -} from "../src/school-context"; - -const students: Student[] = [ - { id: "s1", name: "Alice" }, - { id: "s2", name: "Bob" }, -]; - -const assignments: Assignment[] = [ - { id: "a1", title: "Math" }, - { id: "a2", title: "Science" }, -]; - -const studentAssignments: StudentAssignment[] = [ - { - id: "sa1", - studentId: "s1", - assignmentId: "a1", - status: "assigned", - date: "2024-01-01T00:00:00.000Z", - }, - { - id: "sa2", - studentId: "s1", - assignmentId: "a2", - status: "pass", - date: "2024-01-01T00:00:00.000Z", - }, -]; - -describe("StudentTable", () => { - it("renders students and their assignments", () => { - render( - {}} - onUpdatedStudentNameChange={() => {}} - onUpdateStudent={() => {}} - assigningStudentId={null} - selectedAssignmentId={null} - onAssigningStudentIdChange={() => {}} - onSelectedAssignmentIdChange={() => {}} - onAssignAssignment={() => {}} - onGradeAssignment={() => {}} - /> - ); - - expect(screen.getByText("Alice")).toBeInTheDocument(); - expect(screen.getByText(/Math/)).toBeInTheDocument(); - expect(screen.getByText(/Science/)).toBeInTheDocument(); - }); - - it("calls onGradeAssignment when clicking pass/fail", () => { - const onGradeAssignment = vi.fn(); - - render( - {}} - onUpdatedStudentNameChange={() => {}} - onUpdateStudent={() => {}} - assigningStudentId={null} - selectedAssignmentId={null} - onAssigningStudentIdChange={() => {}} - onSelectedAssignmentIdChange={() => {}} - onAssignAssignment={() => {}} - onGradeAssignment={onGradeAssignment} - /> - ); - - const passButtons = screen.getAllByRole("button", { name: /pass/i }); - fireEvent.click(passButtons[0]); - - expect(onGradeAssignment).toHaveBeenCalledWith("sa1", "pass"); - }); - - it("enters edit mode for a student and calls onUpdateStudent", () => { - const onStudentEditingIdChange = vi.fn(); - const onUpdateStudent = vi.fn(); - - const { rerender } = render( - {}} - onUpdateStudent={onUpdateStudent} - assigningStudentId={null} - selectedAssignmentId={null} - onAssigningStudentIdChange={() => {}} - onSelectedAssignmentIdChange={() => {}} - onAssignAssignment={() => {}} - onGradeAssignment={() => {}} - /> - ); - - const updateButton = screen.getByRole("button", { name: /update alice/i }); - fireEvent.click(updateButton); - expect(onStudentEditingIdChange).toHaveBeenCalledWith("s1"); - - rerender( - {}} - onUpdateStudent={onUpdateStudent} - assigningStudentId={null} - selectedAssignmentId={null} - onAssigningStudentIdChange={() => {}} - onSelectedAssignmentIdChange={() => {}} - onAssignAssignment={() => {}} - onGradeAssignment={() => {}} - /> - ); - - const doneButton = screen.getByRole("button", { name: /save student name/i }); - fireEvent.click(doneButton); - - expect(onUpdateStudent).toHaveBeenCalled(); - }); - - it("allows selecting assignment for a student when in assigning mode", () => { - const onAssigningStudentIdChange = vi.fn(); - const onSelectedAssignmentIdChange = vi.fn(); - const onAssignAssignment = vi.fn(); - - const { rerender } = render( - {}} - onUpdatedStudentNameChange={() => {}} - onUpdateStudent={() => {}} - assigningStudentId={null} - selectedAssignmentId={null} - onAssigningStudentIdChange={onAssigningStudentIdChange} - onSelectedAssignmentIdChange={onSelectedAssignmentIdChange} - onAssignAssignment={onAssignAssignment} - onGradeAssignment={() => {}} - /> - ); - - const assignButton = screen.getByRole("button", { - name: /assign an assignment to alice/i, - }); - fireEvent.click(assignButton); - expect(onAssigningStudentIdChange).toHaveBeenCalledWith("s1"); - - rerender( - {}} - onUpdatedStudentNameChange={() => {}} - onUpdateStudent={() => {}} - assigningStudentId="s1" - selectedAssignmentId={null} - onAssigningStudentIdChange={onAssigningStudentIdChange} - onSelectedAssignmentIdChange={onSelectedAssignmentIdChange} - onAssignAssignment={onAssignAssignment} - onGradeAssignment={() => {}} - /> - ); - - const select = screen.getByLabelText( - /choose assignment to assign to alice/i - ); - fireEvent.change(select, { target: { value: "a1" } }); - expect(onSelectedAssignmentIdChange).toHaveBeenCalledWith("a1"); - - rerender( - {}} - onUpdatedStudentNameChange={() => {}} - onUpdateStudent={() => {}} - assigningStudentId="s1" - selectedAssignmentId="a1" - onAssigningStudentIdChange={onAssigningStudentIdChange} - onSelectedAssignmentIdChange={onSelectedAssignmentIdChange} - onAssignAssignment={onAssignAssignment} - onGradeAssignment={() => {}} - /> - ); - - const assignConfirmButton = screen.getByRole("button", { - name: /assign selected assignment to alice/i, - }); - fireEvent.click(assignConfirmButton); - expect(onAssignAssignment).toHaveBeenCalled(); - }); -}); - diff --git a/react/tests/teacher-form.test.tsx b/react/tests/teacher-form.test.tsx deleted file mode 100644 index 79854f3..0000000 --- a/react/tests/teacher-form.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { TeacherForm } from "../src/components/TeacherForm"; - -describe("TeacherForm", () => { - const originalCrypto = globalThis.crypto; - - beforeEach(() => { - // @ts-expect-error allow test double - globalThis.crypto = { randomUUID: () => "teacher-id-1" }; - }); - - afterEach(() => { - globalThis.crypto = originalCrypto; - }); - - it("disables submit button when input is empty or whitespace", () => { - render(); - - const button = screen.getByRole("button", { name: /add teacher/i }); - const input = screen.getByLabelText(/teacher name/i); - - expect(button).toBeDisabled(); - - fireEvent.change(input, { target: { value: " " } }); - expect(button).toBeDisabled(); - }); - - it("enables submit button when input has text", () => { - render(); - - const button = screen.getByRole("button", { name: /add teacher/i }); - const input = screen.getByLabelText(/teacher name/i); - - fireEvent.change(input, { target: { value: "Alice" } }); - expect(button).toBeEnabled(); - }); - - it("clears the input after submit", () => { - render(); - - const input = screen.getByLabelText(/teacher name/i) as HTMLInputElement; - - fireEvent.change(input, { target: { value: "Bob" } }); - fireEvent.submit(input.form!); - - expect(input.value).toBe(""); - }); -}); - diff --git a/react/tests/teacher-table.test.tsx b/react/tests/teacher-table.test.tsx deleted file mode 100644 index aacd75b..0000000 --- a/react/tests/teacher-table.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { TeacherTable } from "../src/components/TeacherTable"; -import type { Student, Teacher } from "../src/school-context"; - -function createTeachers(): Teacher[] { - return [ - { id: "t1", name: "Teacher 1", students: ["s1"] }, - { id: "t2", name: "Teacher 2", students: [] }, - ]; -} - -function createStudents(): Student[] { - return [ - { id: "s1", name: "Alice" }, - { id: "s2", name: "Bob" }, - ]; -} - -describe("TeacherTable", () => { - it("renders teacher rows and assigned student names", () => { - render( - {}} - onNewAssignedStudentIdChange={() => {}} - onAssignStudent={() => {}} - /> - ); - - expect(screen.getByText("Teacher 1")).toBeInTheDocument(); - expect(screen.getByText("Teacher 2")).toBeInTheDocument(); - expect(screen.getByText("Alice")).toBeInTheDocument(); - }); - - it("shows assign select and button when a teacher is in edit mode", () => { - const onNewAssignedStudentIdChange = vi.fn(); - const onAssignStudent = vi.fn(); - - render( - {}} - onNewAssignedStudentIdChange={onNewAssignedStudentIdChange} - onAssignStudent={onAssignStudent} - /> - ); - - const select = screen.getByLabelText(/choose student to assign/i); - expect(select).toBeInTheDocument(); - - fireEvent.change(select, { target: { value: "s2" } }); - expect(onNewAssignedStudentIdChange).toHaveBeenCalledWith("s2"); - }); - - it("calls onTeacherEditingIdChange when clicking 'Assign student' button", () => { - const onTeacherEditingIdChange = vi.fn(); - - render( - {}} - onAssignStudent={() => {}} - /> - ); - - const assignButton = screen.getByRole("button", { - name: /assign a student to teacher 1/i, - }); - - fireEvent.click(assignButton); - expect(onTeacherEditingIdChange).toHaveBeenCalledWith("t1"); - }); -}); -