From 93be6d6a2ffc69c6b28b3c7a79d13b561fd8aeda Mon Sep 17 00:00:00 2001 From: NesiciCoding Date: Thu, 13 Aug 2026 15:30:43 +0200 Subject: [PATCH 1/2] perf(render): migrate SpeakingSession to the selector store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five data slices now come from one useStoreSelector and saveSpeakingSession from the stable useStoreActions context, replacing four whole-domain hook subscriptions that re-rendered the session on unrelated updates. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/pages/SpeakingSession.tsx | 15 +++++++++------ .../__tests__/SpeakingSession.extended.test.tsx | 5 +++++ src/pages/__tests__/SpeakingSession.test.tsx | 5 +++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/pages/SpeakingSession.tsx b/src/pages/SpeakingSession.tsx index c5d3fae0..9f1b9076 100644 --- a/src/pages/SpeakingSession.tsx +++ b/src/pages/SpeakingSession.tsx @@ -6,7 +6,7 @@ import type { EventData } from 'react-joyride'; import { getSpeakingTourSteps } from '../data/TutorialSteps'; import { ArrowLeft, Play, Pause, Square, Save, Mic, X, Trash2 } from 'lucide-react'; import Topbar from '../components/Layout/Topbar'; -import { useAssessment, useAuthoring, useSettings, useStudents } from '../context/AppContext'; +import { useStoreActions, useStoreSelector } from '../context/useStore'; import { calcEntryPoints, calcGradeSummary, criterionMaxPoints } from '../utils/gradeCalc'; import { nanoid } from '../utils/nanoid'; import RecordingControls from '../components/Recordings/RecordingControls'; @@ -32,11 +32,14 @@ export default function SpeakingSession() { const { rubricId, studentId } = useParams<{ rubricId: string; studentId: string }>(); const navigate = useNavigate(); const { t } = useTranslation(); - const { students } = useStudents(); - - const { rubrics, gradeScales } = useAuthoring(); - const { speakingSessions, saveSpeakingSession } = useAssessment(); - const { settings } = useSettings(); + const { students, rubrics, gradeScales, speakingSessions, settings } = useStoreSelector((s) => ({ + students: s.students, + rubrics: s.rubrics, + gradeScales: s.gradeScales, + speakingSessions: s.speakingSessions, + settings: s.settings, + })); + const { saveSpeakingSession } = useStoreActions(); const rubric = rubrics.find((r) => r.id === rubricId); const student = students.find((s) => s.id === studentId); diff --git a/src/pages/__tests__/SpeakingSession.extended.test.tsx b/src/pages/__tests__/SpeakingSession.extended.test.tsx index 08d0e3fd..bcc16200 100644 --- a/src/pages/__tests__/SpeakingSession.extended.test.tsx +++ b/src/pages/__tests__/SpeakingSession.extended.test.tsx @@ -102,6 +102,11 @@ vi.mock('../../context/AppContext', () => ({ usePlatform: () => mockAppValue, })); +vi.mock('../../context/useStore', () => ({ + useStoreSelector: (selector: (state: any) => any) => selector(mockAppValue), + useStoreActions: () => mockAppValue, +})); + vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate }; diff --git a/src/pages/__tests__/SpeakingSession.test.tsx b/src/pages/__tests__/SpeakingSession.test.tsx index 4dd9c127..3a71bc15 100644 --- a/src/pages/__tests__/SpeakingSession.test.tsx +++ b/src/pages/__tests__/SpeakingSession.test.tsx @@ -83,6 +83,11 @@ vi.mock('../../context/AppContext', () => ({ usePlatform: () => mockAppValue, })); +vi.mock('../../context/useStore', () => ({ + useStoreSelector: (selector: (state: any) => any) => selector(mockAppValue), + useStoreActions: () => mockAppValue, +})); + vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate }; From dc6ad47b2f63e94552b6446882478d73eeba51b2 Mon Sep 17 00:00:00 2001 From: NesiciCoding Date: Tue, 18 Aug 2026 09:07:47 +0200 Subject: [PATCH 2/2] fix(render): exclude archived students from speaking sessions; type test mocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The roster domain hooks filtered soft-deleted rows; the selector-store migration read the raw slice. Restore the active-student filter and type the useStoreSelector mock against StoreData with a narrow StoreActions fixture for saveSpeakingSession. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/pages/SpeakingSession.tsx | 11 ++++++++++- src/pages/__tests__/SpeakingSession.extended.test.tsx | 9 +++++---- src/pages/__tests__/SpeakingSession.test.tsx | 9 +++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/SpeakingSession.tsx b/src/pages/SpeakingSession.tsx index 9f1b9076..aba973de 100644 --- a/src/pages/SpeakingSession.tsx +++ b/src/pages/SpeakingSession.tsx @@ -32,13 +32,22 @@ export default function SpeakingSession() { const { rubricId, studentId } = useParams<{ rubricId: string; studentId: string }>(); const navigate = useNavigate(); const { t } = useTranslation(); - const { students, rubrics, gradeScales, speakingSessions, settings } = useStoreSelector((s) => ({ + const { + students: allStudents, + rubrics, + gradeScales, + speakingSessions, + settings, + } = useStoreSelector((s) => ({ students: s.students, rubrics: s.rubrics, gradeScales: s.gradeScales, speakingSessions: s.speakingSessions, settings: s.settings, })); + // The roster domain hooks filtered soft-deleted rows; archived students must not + // be reachable from a speaking-session route. + const students = React.useMemo(() => allStudents.filter((s) => !s.archivedAt), [allStudents]); const { saveSpeakingSession } = useStoreActions(); const rubric = rubrics.find((r) => r.id === rubricId); diff --git a/src/pages/__tests__/SpeakingSession.extended.test.tsx b/src/pages/__tests__/SpeakingSession.extended.test.tsx index bcc16200..70bd3458 100644 --- a/src/pages/__tests__/SpeakingSession.extended.test.tsx +++ b/src/pages/__tests__/SpeakingSession.extended.test.tsx @@ -3,6 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { DEFAULT_FORMAT } from '../../types'; +import type { StoreData } from '../../store/storage'; import type { AppSettings, Class, @@ -77,14 +78,13 @@ const mockClassesArr = [mockClass]; const mockGradeScalesArr = [mockGradeScale]; const emptyArr: never[] = []; -const mockAppValue: Record = { +const mockAppValue: Partial = { rubrics: mockRubricsArr, students: mockStudentsArr, classes: mockClassesArr, gradeScales: mockGradeScalesArr, settings: mockSettings, speakingSessions: emptyArr, - saveSpeakingSession: mockSaveSpeakingSession, studentRubrics: emptyArr, }; @@ -103,8 +103,9 @@ vi.mock('../../context/AppContext', () => ({ })); vi.mock('../../context/useStore', () => ({ - useStoreSelector: (selector: (state: any) => any) => selector(mockAppValue), - useStoreActions: () => mockAppValue, + useStoreSelector: (selector: (state: StoreData) => T): T => selector(mockAppValue as StoreData), + // SpeakingSession only triggers saveSpeakingSession; keep the action mock narrow. + useStoreActions: () => ({ saveSpeakingSession: mockSaveSpeakingSession }), })); vi.mock('react-router-dom', async () => { diff --git a/src/pages/__tests__/SpeakingSession.test.tsx b/src/pages/__tests__/SpeakingSession.test.tsx index 3a71bc15..1709eeb5 100644 --- a/src/pages/__tests__/SpeakingSession.test.tsx +++ b/src/pages/__tests__/SpeakingSession.test.tsx @@ -4,6 +4,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { DEFAULT_FORMAT } from '../../types'; import type { AppSettings, Class, GradeScale, Rubric, Student } from '../../types'; +import type { StoreData } from '../../store/storage'; const mockGradeScale: GradeScale = { id: 'gs1', @@ -59,14 +60,13 @@ const mockClassesArr = [mockClass]; const mockGradeScalesArr = [mockGradeScale]; const emptyArr: never[] = []; -const mockAppValue = { +const mockAppValue: Partial = { rubrics: mockRubricsArr, students: mockStudentsArr, classes: mockClassesArr, gradeScales: mockGradeScalesArr, settings: mockSettings, speakingSessions: emptyArr, - saveSpeakingSession: mockSaveSpeakingSession, studentRubrics: emptyArr, }; @@ -84,8 +84,9 @@ vi.mock('../../context/AppContext', () => ({ })); vi.mock('../../context/useStore', () => ({ - useStoreSelector: (selector: (state: any) => any) => selector(mockAppValue), - useStoreActions: () => mockAppValue, + useStoreSelector: (selector: (state: StoreData) => T): T => selector(mockAppValue as StoreData), + // SpeakingSession only triggers saveSpeakingSession; keep the action mock narrow. + useStoreActions: () => ({ saveSpeakingSession: mockSaveSpeakingSession }), })); vi.mock('react-router-dom', async () => {