From cb253af876c99460c6b56b6b58002588db4ca940 Mon Sep 17 00:00:00 2001 From: NesiciCoding Date: Wed, 12 Aug 2026 00:56:53 +0200 Subject: [PATCH 1/2] refactor(state): delete the useApp() merged view and migrate tests to domain hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useApp() merged every domain context into one value, so a single dispatch re-rendered all of its consumers — the exact whole-app re-render the domain split and selector store exist to prevent. App code was already migrated to the domain hooks (useRoster, useAuthoring, ...), leaving useApp() as a regression trap with no remaining production call sites. Delete the hook and its AppContextValue-only import, and migrate the test suite off it: - Context tests (AppContext, AppContext.extended, dyslexiaMode, themeBundleApply) now render each case against exactly the domain hooks it exercises, e.g. renderHook(() => useRoster()) or a two-hook composition like ({ ...useRoster(), ...useAuthoring() }) — which also makes each test's domain dependency explicit instead of silently subscribing to all seven. - All 57 shared vi.mock('.../AppContext') factories in page/component suites drop the now-nonexistent useApp export; their domain-hook mocks are unchanged. - Comments referencing useApp() updated to describe the domain hooks. The domain-hook imports in AppContext.tsx are narrowed to just the Provider components (the hooks are re-exported separately), removing the last unused imports. Verified: tsc --noEmit clean, eslint 0 errors, and all 2,863 tests pass with zero useApp references remaining anywhere in src. Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- .../__tests__/CommentBankManager.test.tsx | 1 - .../__tests__/CommentBankModal.test.tsx | 1 - .../Layout/__tests__/Sidebar.test.tsx | 1 - .../Layout/__tests__/Topbar.test.tsx | 1 - .../__tests__/CsvImportModal.test.tsx | 1 - .../__tests__/StudentDecksSection.test.tsx | 1 - .../QuestionBankItemEditorModal.test.tsx | 1 - .../__tests__/QuestionBankManager.test.tsx | 1 - .../QuestionBankSectionEditor.test.tsx | 1 - .../Tests/__tests__/QuestionEditor.test.tsx | 1 - .../__tests__/TestAssignmentModal.test.tsx | 1 - .../__tests__/GlobalSearch.test.tsx | 1 - .../__tests__/StandardsPickerModal.test.tsx | 1 - .../__tests__/components.smoke.test.tsx | 1 - .../__tests__/components.untested.test.tsx | 1 - src/context/AppContext.extended.test.tsx | 36 +++++++-------- src/context/AppContext.test.tsx | 46 +++++++++---------- src/context/AppContext.tsx | 31 +++---------- src/context/dyslexiaMode.test.tsx | 8 ++-- src/context/themeBundleApply.test.tsx | 8 ++-- src/hooks/__tests__/hooks.test.ts | 3 +- src/hooks/useNotificationFeed.test.ts | 1 - .../__tests__/ActivityDashboardPage.test.tsx | 1 - src/pages/__tests__/AttachmentsPage.test.tsx | 1 - .../__tests__/ComparativeGrading.test.tsx | 3 +- src/pages/__tests__/DocsPage.test.tsx | 1 - src/pages/__tests__/EssayBuilderPage.test.tsx | 1 - src/pages/__tests__/EssayListPage.test.tsx | 1 - src/pages/__tests__/ExportPage.test.tsx | 1 - src/pages/__tests__/GradeStudent.test.tsx | 3 +- src/pages/__tests__/LiveMonitorPage.test.tsx | 1 - src/pages/__tests__/MarketplacePage.test.tsx | 1 - src/pages/__tests__/MessagesPage.test.tsx | 1 - .../__tests__/ModerationQueuePage.test.tsx | 1 - .../__tests__/NotificationsPage.test.tsx | 3 +- .../PeerReviewAnalyticsPage.test.tsx | 1 - src/pages/__tests__/PeerReviewView.test.tsx | 1 - src/pages/__tests__/RubricBuilder.test.tsx | 1 - src/pages/__tests__/RubricList.test.tsx | 1 - src/pages/__tests__/SelfAssessPage.test.tsx | 1 - src/pages/__tests__/SettingsPage.test.tsx | 3 +- src/pages/__tests__/SpeakingSession.test.tsx | 1 - src/pages/__tests__/StatisticsPage.test.tsx | 3 +- .../StudentLearningPathPage.test.tsx | 1 - .../__tests__/StudentPortalPage.test.tsx | 1 - .../__tests__/StudentProfilePage.test.tsx | 1 - src/pages/__tests__/StudentsPage.test.tsx | 1 - src/pages/__tests__/TestBuilderPage.test.tsx | 1 - src/pages/__tests__/TestListPage.test.tsx | 1 - src/pages/__tests__/TestResultsPage.test.tsx | 1 - .../VocabularyDashboardPage.test.tsx | 1 - .../__tests__/pages-phase4.a11y.test.tsx | 1 - src/pages/__tests__/pages.a11y.test.tsx | 5 +- src/pages/__tests__/pages.admin.a11y.test.tsx | 3 +- .../__tests__/pages.deepcoverage.test.tsx | 1 - .../__tests__/pages.deepcoverage2.test.tsx | 1 - src/pages/__tests__/pages.functions.test.tsx | 1 - .../__tests__/pages.interaction.test.tsx | 1 - src/pages/__tests__/pages.missing.test.tsx | 1 - src/pages/__tests__/pages.smoke.test.tsx | 1 - .../__tests__/pages.student.a11y.test.tsx | 3 +- src/pages/__tests__/pages.unit.test.tsx | 1 - 62 files changed, 66 insertions(+), 140 deletions(-) diff --git a/src/components/Comments/__tests__/CommentBankManager.test.tsx b/src/components/Comments/__tests__/CommentBankManager.test.tsx index 7156899d..fd306531 100644 --- a/src/components/Comments/__tests__/CommentBankManager.test.tsx +++ b/src/components/Comments/__tests__/CommentBankManager.test.tsx @@ -22,7 +22,6 @@ const makeAppContextMock = () => ({ deleteCommentBankItem: vi.fn(), }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Comments/__tests__/CommentBankModal.test.tsx b/src/components/Comments/__tests__/CommentBankModal.test.tsx index b81cd5d1..8e48a4d0 100644 --- a/src/components/Comments/__tests__/CommentBankModal.test.tsx +++ b/src/components/Comments/__tests__/CommentBankModal.test.tsx @@ -21,7 +21,6 @@ const makeAppContextMock = () => ({ deleteCommentBankItem: vi.fn(), }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Layout/__tests__/Sidebar.test.tsx b/src/components/Layout/__tests__/Sidebar.test.tsx index e996ab93..38a314b0 100644 --- a/src/components/Layout/__tests__/Sidebar.test.tsx +++ b/src/components/Layout/__tests__/Sidebar.test.tsx @@ -14,7 +14,6 @@ const makeAppContextMock = () => ({ students: [], }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Layout/__tests__/Topbar.test.tsx b/src/components/Layout/__tests__/Topbar.test.tsx index 9b737616..2116037a 100644 --- a/src/components/Layout/__tests__/Topbar.test.tsx +++ b/src/components/Layout/__tests__/Topbar.test.tsx @@ -20,7 +20,6 @@ const makeAppContextMock = () => ({ essayAssignments: [], }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Students/__tests__/CsvImportModal.test.tsx b/src/components/Students/__tests__/CsvImportModal.test.tsx index 9974f308..e04a6d6c 100644 --- a/src/components/Students/__tests__/CsvImportModal.test.tsx +++ b/src/components/Students/__tests__/CsvImportModal.test.tsx @@ -54,7 +54,6 @@ const makeAppContextMock = () => ({ settings: {}, }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Students/__tests__/StudentDecksSection.test.tsx b/src/components/Students/__tests__/StudentDecksSection.test.tsx index bef121d7..7ae3e3bc 100644 --- a/src/components/Students/__tests__/StudentDecksSection.test.tsx +++ b/src/components/Students/__tests__/StudentDecksSection.test.tsx @@ -24,7 +24,6 @@ const makeAppContextMock = () => ({ fetchMyStudentFlashcardDecks, }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Tests/__tests__/QuestionBankItemEditorModal.test.tsx b/src/components/Tests/__tests__/QuestionBankItemEditorModal.test.tsx index ba77fff0..93d395fb 100644 --- a/src/components/Tests/__tests__/QuestionBankItemEditorModal.test.tsx +++ b/src/components/Tests/__tests__/QuestionBankItemEditorModal.test.tsx @@ -12,7 +12,6 @@ vi.mock('react-i18next', () => ({ const makeAppContextMock = () => ({ settings: {}, addQuestionBankItem: vi.fn() }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Tests/__tests__/QuestionBankManager.test.tsx b/src/components/Tests/__tests__/QuestionBankManager.test.tsx index 07466715..40734f64 100644 --- a/src/components/Tests/__tests__/QuestionBankManager.test.tsx +++ b/src/components/Tests/__tests__/QuestionBankManager.test.tsx @@ -79,7 +79,6 @@ const makeAppContextMock = () => ({ bulkUpdateQuestionBankItems, }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Tests/__tests__/QuestionBankSectionEditor.test.tsx b/src/components/Tests/__tests__/QuestionBankSectionEditor.test.tsx index 56c1f65a..6dc444c5 100644 --- a/src/components/Tests/__tests__/QuestionBankSectionEditor.test.tsx +++ b/src/components/Tests/__tests__/QuestionBankSectionEditor.test.tsx @@ -12,7 +12,6 @@ vi.mock('react-i18next', () => ({ const makeAppContextMock = () => ({ settings: {}, addQuestionBankItem: vi.fn() }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Tests/__tests__/QuestionEditor.test.tsx b/src/components/Tests/__tests__/QuestionEditor.test.tsx index a3c3965a..8e256be7 100644 --- a/src/components/Tests/__tests__/QuestionEditor.test.tsx +++ b/src/components/Tests/__tests__/QuestionEditor.test.tsx @@ -16,7 +16,6 @@ vi.mock('react-i18next', () => ({ const makeAppContextMock = () => ({ settings: {} }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/Tests/__tests__/TestAssignmentModal.test.tsx b/src/components/Tests/__tests__/TestAssignmentModal.test.tsx index 6bb39ede..cf533477 100644 --- a/src/components/Tests/__tests__/TestAssignmentModal.test.tsx +++ b/src/components/Tests/__tests__/TestAssignmentModal.test.tsx @@ -46,7 +46,6 @@ const makeAppContextMock = () => ({ saveTestAssignment: mockSaveTestAssignment, }); vi.mock('../../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/__tests__/GlobalSearch.test.tsx b/src/components/__tests__/GlobalSearch.test.tsx index 036911a1..687aa995 100644 --- a/src/components/__tests__/GlobalSearch.test.tsx +++ b/src/components/__tests__/GlobalSearch.test.tsx @@ -53,7 +53,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/components/__tests__/StandardsPickerModal.test.tsx b/src/components/__tests__/StandardsPickerModal.test.tsx index 1b799f46..9afb5af0 100644 --- a/src/components/__tests__/StandardsPickerModal.test.tsx +++ b/src/components/__tests__/StandardsPickerModal.test.tsx @@ -21,7 +21,6 @@ const makeAppContextMock = () => ({ isFavoriteStandard: mockIsFavorite, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/__tests__/components.smoke.test.tsx b/src/components/__tests__/components.smoke.test.tsx index ce26c778..6267a156 100644 --- a/src/components/__tests__/components.smoke.test.tsx +++ b/src/components/__tests__/components.smoke.test.tsx @@ -42,7 +42,6 @@ const makeAppContextMock = () => ({ isFavoriteStandard: vi.fn(() => false), }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/components/__tests__/components.untested.test.tsx b/src/components/__tests__/components.untested.test.tsx index 6a7f4f60..5b680e07 100644 --- a/src/components/__tests__/components.untested.test.tsx +++ b/src/components/__tests__/components.untested.test.tsx @@ -65,7 +65,6 @@ const makeAppContextMock = () => ({ dismissMigrationPrompt: mockDismissMigrationPrompt, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/context/AppContext.extended.test.tsx b/src/context/AppContext.extended.test.tsx index 3d4ba3e3..db02b82e 100644 --- a/src/context/AppContext.extended.test.tsx +++ b/src/context/AppContext.extended.test.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AppProvider, useApp } from './AppContext'; +import { AppProvider, useRoster, useAuthoring, useAssessment } from './AppContext'; import type { Rubric, RubricVersion } from '../types'; import { DEFAULT_FORMAT } from '../types'; @@ -117,7 +117,7 @@ describe('AppContext — extended actions', () => { // ─── Students & Classes ─────────────────────────────────────────────────── it('adds and deletes a student', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); let id: string; act(() => { const s = result.current.addStudent({ name: 'Bob', classId: 'c1' }); @@ -131,7 +131,7 @@ describe('AppContext — extended actions', () => { }); it('updates a student', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); act(() => { result.current.addStudent({ name: 'Bob', classId: 'c1' }); }); @@ -142,7 +142,7 @@ describe('AppContext — extended actions', () => { }); it('adds, updates, and deletes a class', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); act(() => { result.current.addClass({ name: 'Class A' }); }); @@ -158,7 +158,7 @@ describe('AppContext — extended actions', () => { }); it('deleteClass with deleteStudents=true removes students in that class', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); act(() => { result.current.addClass({ name: 'Class A' }); }); @@ -176,7 +176,7 @@ describe('AppContext — extended actions', () => { // ─── Self Assessments ───────────────────────────────────────────────────── it('saves and deletes a self assessment', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAssessment(), { wrapper }); const sa = { id: 'sa1', rubricId: 'r1', studentId: 's1', ratings: [], submittedAt: '2024-01-01' }; act(() => { result.current.saveSelfAssessment(sa); @@ -191,7 +191,7 @@ describe('AppContext — extended actions', () => { // ─── Speaking Sessions ──────────────────────────────────────────────────── it('saves and updates a speaking session', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAssessment(), { wrapper }); const session = { id: 'ss1', rubricId: 'r1', @@ -211,7 +211,7 @@ describe('AppContext — extended actions', () => { }); it('deletes a speaking session', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAssessment(), { wrapper }); const session = { id: 'ss1', rubricId: 'r1', @@ -232,7 +232,7 @@ describe('AppContext — extended actions', () => { // ─── Rubric Versioning ──────────────────────────────────────────────────── it('saves a rubric version', async () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -246,7 +246,7 @@ describe('AppContext — extended actions', () => { }); it('restores a rubric version', async () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -269,7 +269,7 @@ describe('AppContext — extended actions', () => { }); it('saveRubricVersion does nothing for unknown rubricId', async () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); await act(async () => { await result.current.saveRubricVersion('unknown'); }); @@ -278,7 +278,7 @@ describe('AppContext — extended actions', () => { }); it('restoreRubricVersion does nothing for unknown rubricId', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.restoreRubricVersion('unknown', makeRubric() as Rubric); }); @@ -288,7 +288,7 @@ describe('AppContext — extended actions', () => { // ─── Rubric Snapshot Sync ───────────────────────────────────────────────── it('syncRubricSnapshot updates student rubrics with new criteria', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring() }), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -319,7 +319,7 @@ describe('AppContext — extended actions', () => { // ─── Soft-delete grades (Phase 15.3) ───────────────────────────────────── it('deleteStudentRubric soft-deletes a solo grade and restoreStudentRubric brings it back', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring() }), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -347,7 +347,7 @@ describe('AppContext — extended actions', () => { }); it('deleteStudentRubric with scope "group" soft-deletes every sibling sharing groupId', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring() }), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -370,7 +370,7 @@ describe('AppContext — extended actions', () => { }); it('deleteStudentRubric with scope "student" only removes the target and detaches it from the group', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring() }), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -398,7 +398,7 @@ describe('AppContext — extended actions', () => { // ─── Vocabulary Items ───────────────────────────────────────────────────── it('adds, updates, and deletes vocabulary items', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addRubric(makeRubric()); }); @@ -425,7 +425,7 @@ describe('AppContext — extended actions', () => { // ─── Analysis Results ───────────────────────────────────────────────────── it('saves and deletes analysis results', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAssessment(), { wrapper }); const ar = { id: 'ar1', studentId: 's1', diff --git a/src/context/AppContext.test.tsx b/src/context/AppContext.test.tsx index 0cc7e40a..acdd8eb3 100644 --- a/src/context/AppContext.test.tsx +++ b/src/context/AppContext.test.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { renderHook, act, render } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AppProvider, useApp, useRoster, useSettings, useAuthoring } from './AppContext'; +import { AppProvider, useRoster, useSettings, useAuthoring, useAssessment } from './AppContext'; import * as storage from '../store/storage'; import { storageSync } from '../services/database'; import type { Rubric, GradeScale } from '../types'; @@ -131,7 +131,7 @@ describe('AppContext', () => { const wrapper = ({ children }: { children: ReactNode }) => {children}; it('should initialize with default loaded state', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring(), ...useSettings() }), { wrapper }); expect(result.current.rubrics).toEqual([]); expect(result.current.students).toEqual([]); expect(result.current.classes).toEqual([]); @@ -140,7 +140,7 @@ describe('AppContext', () => { }); it('should add a rubric', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useAuthoring(), ...useSettings() }), { wrapper }); const newRubric: Omit = { name: 'Test Rubric', @@ -166,7 +166,7 @@ describe('AppContext', () => { }); it('should update a rubric', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useAuthoring(), ...useSettings() }), { wrapper }); act(() => { result.current.addRubric({ @@ -197,7 +197,7 @@ describe('AppContext', () => { }); it('should delete a rubric', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useAuthoring(), ...useSettings() }), { wrapper }); let addedId = ''; act(() => { @@ -226,7 +226,7 @@ describe('AppContext', () => { }); it('should save and update student rubrics', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); const sr = { id: 'sr1', rubricId: 'r1', studentId: 's1', entries: [], overallComment: '', isPeerReview: false }; act(() => { @@ -242,7 +242,7 @@ describe('AppContext', () => { }); it('should create a student rubric with default entries', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring(), ...useSettings() }), { wrapper }); act(() => { result.current.addRubric({ name: 'R1', @@ -266,7 +266,7 @@ describe('AppContext', () => { }); it('should create group student rubrics sharing one groupId, and propagate scores on save', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring(), ...useSettings() }), { wrapper }); act(() => { result.current.addRubric({ name: 'Group Project', @@ -309,7 +309,7 @@ describe('AppContext', () => { }); it('should only fan out collaborative criteria in group grading, leaving individually-scoped criteria per student', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring(), ...useSettings() }), { wrapper }); act(() => { result.current.addRubric({ name: 'Group Project', @@ -360,7 +360,7 @@ describe('AppContext', () => { }); it('should still fan out overallComment/globalModifier for an all-individually-scoped rubric, while entries stay per-student', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring(), ...useSettings() }), { wrapper }); act(() => { result.current.addRubric({ name: 'Group Project', @@ -406,7 +406,7 @@ describe('AppContext', () => { }); it('should reuse an existing ungrouped StudentRubric instead of creating a duplicate', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => ({ ...useRoster(), ...useAuthoring(), ...useSettings() }), { wrapper }); act(() => { result.current.addRubric({ name: 'Group Project', @@ -440,7 +440,7 @@ describe('AppContext', () => { }); it('should merge classes', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); let c1 = '', c2 = ''; @@ -459,7 +459,7 @@ describe('AppContext', () => { }); it('should manage favorite standards', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); const std = { guid: 'std1', description: 'desc', standardSetTitle: '', jurisdictionTitle: '' }; act(() => { @@ -475,7 +475,7 @@ describe('AppContext', () => { }); it('should manage comment bank items', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addCommentBankItem('Good job', ['tag1']); @@ -495,7 +495,7 @@ describe('AppContext', () => { }); it('should track comment bank usage without requiring a full item replace', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addCommentBankItem('Good job', ['tag1']); @@ -516,7 +516,7 @@ describe('AppContext', () => { }); it('should mange grade scales', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addGradeScale({ name: 'New Scale', type: 'points', ranges: [] }); @@ -536,7 +536,7 @@ describe('AppContext', () => { }); it('should manage attachments', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useRoster(), { wrapper }); act(() => { result.current.addAttachment({ name: 'File 1', mimeType: 'docx', size: 100, dataUrl: 'data' }); @@ -551,7 +551,7 @@ describe('AppContext', () => { }); it('should manage export templates', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAuthoring(), { wrapper }); act(() => { result.current.addExportTemplate({ name: 'Template 1', dataUrl: 'data', levelHeaders: ['H1'], size: 100 }); @@ -566,7 +566,7 @@ describe('AppContext', () => { }); it('should manage peer reviews', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useAssessment(), { wrapper }); const pr = { id: 'pr1', rubricId: 'r1', @@ -589,7 +589,7 @@ describe('AppContext', () => { }); it('should retrieve active grade scale', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); let scale: GradeScale | undefined; act(() => { @@ -599,7 +599,7 @@ describe('AppContext', () => { }); it('should update settings', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); act(() => { result.current.updateSettings({ theme: 'dark' }); @@ -623,7 +623,7 @@ describe('AppContext', () => { it('shows the storage_full toast when the quota is exceeded while offline/disconnected', () => { vi.mocked(storageSync.isConnected).mockReturnValue(false); - renderHook(() => useApp(), { wrapper }); + renderHook(() => useSettings(), { wrapper }); triggerQuotaExceeded(); @@ -632,7 +632,7 @@ describe('AppContext', () => { it('stays silent when the quota is exceeded while connected — Supabase already has the real data', () => { vi.mocked(storageSync.isConnected).mockReturnValue(true); - renderHook(() => useApp(), { wrapper }); + renderHook(() => useSettings(), { wrapper }); triggerQuotaExceeded(); diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index f5907dec..cf314dee 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState, ReactNode } from 'react'; import { - AppContextValue, LOCAL_MODE_KEY, MIGRATION_DONE_KEY, flushToLocalStorage, @@ -10,13 +9,13 @@ import { } from './storeCore'; import { createSelectorStore, StoreProvider } from './useStore'; -import { useRoster, RosterProvider } from './domains/roster'; -import { useAuthoring, AuthoringProvider } from './domains/authoring'; -import { useAssessment, AssessmentProvider } from './domains/assessment'; -import { useEssays, EssaysProvider } from './domains/essays'; -import { useFlashcards, FlashcardsProvider } from './domains/flashcards'; -import { useSettings, SettingsProvider } from './domains/settings'; -import { usePlatform, PlatformProvider } from './domains/platform'; +import { RosterProvider } from './domains/roster'; +import { AuthoringProvider } from './domains/authoring'; +import { AssessmentProvider } from './domains/assessment'; +import { EssaysProvider } from './domains/essays'; +import { FlashcardsProvider } from './domains/flashcards'; +import { SettingsProvider } from './domains/settings'; +import { PlatformProvider } from './domains/platform'; import { useTranslation } from 'react-i18next'; import { useToast } from '../hooks/useToast'; import { loadDb, getDb } from '../services/database/lazyDb'; @@ -30,22 +29,6 @@ import { initClientLogger, setLoggerContext, STRESS_TEST_LOGGING_ENABLED } from import { initAuditLogger, clearAuditLogger } from '../services/database/AuditLogger'; import type { DatabaseConfig } from '../services/database'; -// The merged view for tests and object-form call sites — app code must use the -// domain hooks (enforced by the no-restricted-syntax ESLint rule). -export function useApp(): AppContextValue { - const roster = useRoster(); - const authoring = useAuthoring(); - const assessment = useAssessment(); - const essays = useEssays(); - const flashcards = useFlashcards(); - const settings = useSettings(); - const platform = usePlatform(); - return useMemo( - () => ({ ...settings, ...roster, ...authoring, ...assessment, ...essays, ...flashcards, ...platform }), - [roster, authoring, assessment, essays, flashcards, settings, platform] - ); -} - export { useRoster } from './domains/roster'; export { useAuthoring } from './domains/authoring'; export { useAssessment } from './domains/assessment'; diff --git a/src/context/dyslexiaMode.test.tsx b/src/context/dyslexiaMode.test.tsx index 64ad090b..56a583b4 100644 --- a/src/context/dyslexiaMode.test.tsx +++ b/src/context/dyslexiaMode.test.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AppProvider, useApp } from './AppContext'; +import { AppProvider, useSettings } from './AppContext'; import * as storage from '../store/storage'; vi.mock('../store/storage', () => ({ @@ -91,7 +91,7 @@ describe('Dyslexia-friendly reading mode', () => { const wrapper = ({ children }: { children: ReactNode }) => {children}; it('sets line-height/letter-spacing CSS vars and persists the setting when enabled', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); act(() => { result.current.updateSettings({ dyslexiaFriendlyMode: true }); @@ -104,7 +104,7 @@ describe('Dyslexia-friendly reading mode', () => { }); it('removes the CSS vars when disabled again', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); act(() => { result.current.updateSettings({ dyslexiaFriendlyMode: true }); @@ -118,7 +118,7 @@ describe('Dyslexia-friendly reading mode', () => { }); it('sets dir="rtl" when the active language is Arabic, and "ltr" otherwise', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); expect(document.documentElement.dir).toBe('ltr'); diff --git a/src/context/themeBundleApply.test.tsx b/src/context/themeBundleApply.test.tsx index df81ba72..256bff05 100644 --- a/src/context/themeBundleApply.test.tsx +++ b/src/context/themeBundleApply.test.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AppProvider, useApp } from './AppContext'; +import { AppProvider, useSettings } from './AppContext'; import { THEME_BUNDLES } from '../data/themes'; import * as storage from '../store/storage'; import type { AppSettings } from '../types'; @@ -111,7 +111,7 @@ describe('Theme bundle application', () => { const wrapper = ({ children }: { children: ReactNode }) => {children}; it.each(THEME_BUNDLES)('applying the "$id" bundle updates settings to match the bundle', (theme) => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); act(() => { applyTheme(result.current.updateSettings, result.current.settings, theme); @@ -126,7 +126,7 @@ describe('Theme bundle application', () => { }); it('preserves all other defaultFormat fields when applying a theme (spread regression)', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); const before = result.current.settings.defaultFormat; const theme = THEME_BUNDLES[0]; @@ -155,7 +155,7 @@ describe('Theme bundle application', () => { }); it('switching between two bundles updates settings to the latest bundle', () => { - const { result } = renderHook(() => useApp(), { wrapper }); + const { result } = renderHook(() => useSettings(), { wrapper }); const [first, second] = THEME_BUNDLES; act(() => { diff --git a/src/hooks/__tests__/hooks.test.ts b/src/hooks/__tests__/hooks.test.ts index 191d7811..93aef62d 100644 --- a/src/hooks/__tests__/hooks.test.ts +++ b/src/hooks/__tests__/hooks.test.ts @@ -7,7 +7,7 @@ import { useDbStatus } from '../useDbStatus'; // ─── Mocks ──────────────────────────────────────────────────────────────────── -// Mutable state consumed by useApp mock (useOverdueStudents) +// Mutable state consumed by the AppContext mock (useOverdueStudents) let mockStudents: Student[] = []; let mockStudentRubrics: StudentRubric[] = []; let mockThreshold = 7; @@ -18,7 +18,6 @@ const makeAppContextMock = () => ({ settings: { overdueReminderThreshold: mockThreshold }, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/hooks/useNotificationFeed.test.ts b/src/hooks/useNotificationFeed.test.ts index b95faaef..0dbfbc96 100644 --- a/src/hooks/useNotificationFeed.test.ts +++ b/src/hooks/useNotificationFeed.test.ts @@ -29,7 +29,6 @@ const makeAppContextMock = () => ({ markMessageReadByTeacher: mockMarkMessageReadByTeacher, }); vi.mock('../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/ActivityDashboardPage.test.tsx b/src/pages/__tests__/ActivityDashboardPage.test.tsx index 7f845f97..a7961811 100644 --- a/src/pages/__tests__/ActivityDashboardPage.test.tsx +++ b/src/pages/__tests__/ActivityDashboardPage.test.tsx @@ -62,7 +62,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/AttachmentsPage.test.tsx b/src/pages/__tests__/AttachmentsPage.test.tsx index 7cf61b27..98ad1f7d 100644 --- a/src/pages/__tests__/AttachmentsPage.test.tsx +++ b/src/pages/__tests__/AttachmentsPage.test.tsx @@ -62,7 +62,6 @@ const mockAppValue: Record = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/ComparativeGrading.test.tsx b/src/pages/__tests__/ComparativeGrading.test.tsx index 5647fd50..470a8426 100644 --- a/src/pages/__tests__/ComparativeGrading.test.tsx +++ b/src/pages/__tests__/ComparativeGrading.test.tsx @@ -47,7 +47,7 @@ const mockSettings: AppSettings = { const mockSaveStudentRubric = vi.fn(); const mockNavigate = vi.fn(); -// Stable references — a useApp() mock that builds new array literals on every call +// Stable references — a domain-hook mock that builds new array literals on every call // defeats this page's useMemo/useEffect deps and causes infinite render loops. const mockRubricsArr = [mockRubric]; const mockStudentsArr = [mockStudentA, mockStudentB]; @@ -67,7 +67,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/DocsPage.test.tsx b/src/pages/__tests__/DocsPage.test.tsx index 8f6fd05a..6d10b344 100644 --- a/src/pages/__tests__/DocsPage.test.tsx +++ b/src/pages/__tests__/DocsPage.test.tsx @@ -22,7 +22,6 @@ const makeAppContextMock = () => ({ studentRubrics: [], }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/EssayBuilderPage.test.tsx b/src/pages/__tests__/EssayBuilderPage.test.tsx index 39d4fb94..4f20aa96 100644 --- a/src/pages/__tests__/EssayBuilderPage.test.tsx +++ b/src/pages/__tests__/EssayBuilderPage.test.tsx @@ -125,7 +125,6 @@ const makeAppContextMock = () => ({ ...appOverrides, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/EssayListPage.test.tsx b/src/pages/__tests__/EssayListPage.test.tsx index 088ecf0c..1c8ba023 100644 --- a/src/pages/__tests__/EssayListPage.test.tsx +++ b/src/pages/__tests__/EssayListPage.test.tsx @@ -74,7 +74,6 @@ const makeAppContextMock = () => ({ ...appOverrides, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/ExportPage.test.tsx b/src/pages/__tests__/ExportPage.test.tsx index 278b2659..ced8695b 100644 --- a/src/pages/__tests__/ExportPage.test.tsx +++ b/src/pages/__tests__/ExportPage.test.tsx @@ -108,7 +108,6 @@ const makeAppContextMock = () => ({ ...appOverrides, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/GradeStudent.test.tsx b/src/pages/__tests__/GradeStudent.test.tsx index f5b6f6ca..e6fa15fd 100644 --- a/src/pages/__tests__/GradeStudent.test.tsx +++ b/src/pages/__tests__/GradeStudent.test.tsx @@ -54,7 +54,7 @@ const mockSaveStudentRubric = vi.fn(); const mockUpdateSettings = vi.fn(); // Stable references — see ComparativeGrading/StatisticsPage tests for why this matters: -// fresh array/object literals on every useApp() call defeat memo/effect deps and can +// fresh array/object literals on every domain-hook call defeat memo/effect deps and can // cause infinite render loops. const mockRubricsArr = [mockRubric]; const mockStudentsArr = [mockStudent, mockStudentBob]; @@ -92,7 +92,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/LiveMonitorPage.test.tsx b/src/pages/__tests__/LiveMonitorPage.test.tsx index cd1cca88..6e721a06 100644 --- a/src/pages/__tests__/LiveMonitorPage.test.tsx +++ b/src/pages/__tests__/LiveMonitorPage.test.tsx @@ -33,7 +33,6 @@ vi.mock('../../services/database', () => ({ let mockUseApp: any; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/MarketplacePage.test.tsx b/src/pages/__tests__/MarketplacePage.test.tsx index b58a0faf..61daf4fd 100644 --- a/src/pages/__tests__/MarketplacePage.test.tsx +++ b/src/pages/__tests__/MarketplacePage.test.tsx @@ -49,7 +49,6 @@ const mockAppValue: Record = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/MessagesPage.test.tsx b/src/pages/__tests__/MessagesPage.test.tsx index 9186d0cf..a8fab029 100644 --- a/src/pages/__tests__/MessagesPage.test.tsx +++ b/src/pages/__tests__/MessagesPage.test.tsx @@ -48,7 +48,6 @@ const makeAppContextMock = () => ({ ...appOverrides, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/ModerationQueuePage.test.tsx b/src/pages/__tests__/ModerationQueuePage.test.tsx index a2bb2b15..f3ad964a 100644 --- a/src/pages/__tests__/ModerationQueuePage.test.tsx +++ b/src/pages/__tests__/ModerationQueuePage.test.tsx @@ -80,7 +80,6 @@ const makeAppContextMock = () => ({ ...appOverrides, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/NotificationsPage.test.tsx b/src/pages/__tests__/NotificationsPage.test.tsx index 76e1d859..fd518ad8 100644 --- a/src/pages/__tests__/NotificationsPage.test.tsx +++ b/src/pages/__tests__/NotificationsPage.test.tsx @@ -84,14 +84,13 @@ vi.mock('../../hooks/useNotificationFeed', () => ({ }), })); -// Topbar (rendered by NotificationsPage) reads settings/classes off useApp() directly. +// Topbar (rendered by NotificationsPage) reads settings/classes off the domain hooks directly. const makeAppContextMock = () => ({ settings: { theme: 'dark', language: 'en' }, updateSettings: vi.fn(), classes: [], }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/PeerReviewAnalyticsPage.test.tsx b/src/pages/__tests__/PeerReviewAnalyticsPage.test.tsx index 7627ca09..1d8f9731 100644 --- a/src/pages/__tests__/PeerReviewAnalyticsPage.test.tsx +++ b/src/pages/__tests__/PeerReviewAnalyticsPage.test.tsx @@ -74,7 +74,6 @@ const peerReview: StudentRubric = { let mockUseApp: any; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/PeerReviewView.test.tsx b/src/pages/__tests__/PeerReviewView.test.tsx index 097efe08..d429ef4e 100644 --- a/src/pages/__tests__/PeerReviewView.test.tsx +++ b/src/pages/__tests__/PeerReviewView.test.tsx @@ -62,7 +62,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/RubricBuilder.test.tsx b/src/pages/__tests__/RubricBuilder.test.tsx index e7740202..0304a270 100644 --- a/src/pages/__tests__/RubricBuilder.test.tsx +++ b/src/pages/__tests__/RubricBuilder.test.tsx @@ -137,7 +137,6 @@ const makeAppContextMock = () => ({ ...appOverrides, }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/RubricList.test.tsx b/src/pages/__tests__/RubricList.test.tsx index ad8c8bec..6ef6f44a 100644 --- a/src/pages/__tests__/RubricList.test.tsx +++ b/src/pages/__tests__/RubricList.test.tsx @@ -56,7 +56,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/SelfAssessPage.test.tsx b/src/pages/__tests__/SelfAssessPage.test.tsx index da8cafe3..849ab7d3 100644 --- a/src/pages/__tests__/SelfAssessPage.test.tsx +++ b/src/pages/__tests__/SelfAssessPage.test.tsx @@ -83,7 +83,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/SettingsPage.test.tsx b/src/pages/__tests__/SettingsPage.test.tsx index a06be56c..f97aca4c 100644 --- a/src/pages/__tests__/SettingsPage.test.tsx +++ b/src/pages/__tests__/SettingsPage.test.tsx @@ -26,7 +26,7 @@ const mockAddGradeScale = vi.fn(() => ({ ...mockGradeScale, id: 'gs2' })); const mockShowToast = vi.fn(); // Stable references — see other page tests in this directory for why fresh array/object -// literals on every useApp() call can cause infinite render loops via memo/effect deps. +// literals on every domain-hook call can cause infinite render loops via memo/effect deps. const mockGradeScalesArr = [mockGradeScale]; const mockCommentBankArr: never[] = []; const mockExportTemplatesArr: never[] = []; @@ -59,7 +59,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/SpeakingSession.test.tsx b/src/pages/__tests__/SpeakingSession.test.tsx index 75e84db9..6fa0c26e 100644 --- a/src/pages/__tests__/SpeakingSession.test.tsx +++ b/src/pages/__tests__/SpeakingSession.test.tsx @@ -71,7 +71,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/StatisticsPage.test.tsx b/src/pages/__tests__/StatisticsPage.test.tsx index 15b862de..4682a130 100644 --- a/src/pages/__tests__/StatisticsPage.test.tsx +++ b/src/pages/__tests__/StatisticsPage.test.tsx @@ -77,7 +77,7 @@ const mockSettings: AppSettings = { const mockUpdateSettings = vi.fn(); // Stable references: StatisticsPage has effects/memos keyed on `classes` etc. — a mock -// that builds new array literals on every useApp() call defeats those memo deps and +// that builds new array literals on every domain-hook call defeats those memo deps and // causes an infinite render loop (each render sees a "new" classes array). const mockRubricsArr = [mockRubric]; const mockStudentsArr = [mockStudentA, mockStudentB]; @@ -99,7 +99,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/StudentLearningPathPage.test.tsx b/src/pages/__tests__/StudentLearningPathPage.test.tsx index 6e7ab1ce..dc8ce973 100644 --- a/src/pages/__tests__/StudentLearningPathPage.test.tsx +++ b/src/pages/__tests__/StudentLearningPathPage.test.tsx @@ -37,7 +37,6 @@ const makeAppContextMock = () => ({ flashcardDecks: [], }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/StudentPortalPage.test.tsx b/src/pages/__tests__/StudentPortalPage.test.tsx index 161d5ae7..d03571f2 100644 --- a/src/pages/__tests__/StudentPortalPage.test.tsx +++ b/src/pages/__tests__/StudentPortalPage.test.tsx @@ -200,7 +200,6 @@ const mockAppValue: Record = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/StudentProfilePage.test.tsx b/src/pages/__tests__/StudentProfilePage.test.tsx index a6c6679a..67a50efb 100644 --- a/src/pages/__tests__/StudentProfilePage.test.tsx +++ b/src/pages/__tests__/StudentProfilePage.test.tsx @@ -96,7 +96,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/StudentsPage.test.tsx b/src/pages/__tests__/StudentsPage.test.tsx index 9a161aa9..71831579 100644 --- a/src/pages/__tests__/StudentsPage.test.tsx +++ b/src/pages/__tests__/StudentsPage.test.tsx @@ -82,7 +82,6 @@ const mockAppValue = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockAppValue, useRoster: () => mockAppValue, useAuthoring: () => mockAppValue, useAssessment: () => mockAppValue, diff --git a/src/pages/__tests__/TestBuilderPage.test.tsx b/src/pages/__tests__/TestBuilderPage.test.tsx index 27f04ee7..6c13820d 100644 --- a/src/pages/__tests__/TestBuilderPage.test.tsx +++ b/src/pages/__tests__/TestBuilderPage.test.tsx @@ -74,7 +74,6 @@ const mockUseApp = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/TestListPage.test.tsx b/src/pages/__tests__/TestListPage.test.tsx index b22c923d..33716bbb 100644 --- a/src/pages/__tests__/TestListPage.test.tsx +++ b/src/pages/__tests__/TestListPage.test.tsx @@ -80,7 +80,6 @@ const mockUseApp = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/TestResultsPage.test.tsx b/src/pages/__tests__/TestResultsPage.test.tsx index b637dcf2..9c6cdbc8 100644 --- a/src/pages/__tests__/TestResultsPage.test.tsx +++ b/src/pages/__tests__/TestResultsPage.test.tsx @@ -84,7 +84,6 @@ const mockUseApp = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/VocabularyDashboardPage.test.tsx b/src/pages/__tests__/VocabularyDashboardPage.test.tsx index b10d0f22..0340cebc 100644 --- a/src/pages/__tests__/VocabularyDashboardPage.test.tsx +++ b/src/pages/__tests__/VocabularyDashboardPage.test.tsx @@ -66,7 +66,6 @@ const makeAppContextMock = () => ({ updateSettings: vi.fn(), }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/pages-phase4.a11y.test.tsx b/src/pages/__tests__/pages-phase4.a11y.test.tsx index 2867b486..31a77392 100644 --- a/src/pages/__tests__/pages-phase4.a11y.test.tsx +++ b/src/pages/__tests__/pages-phase4.a11y.test.tsx @@ -70,7 +70,6 @@ const makeAppContextMock = () => ({ deleteGradingTask: vi.fn(), }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/pages.a11y.test.tsx b/src/pages/__tests__/pages.a11y.test.tsx index 91e00f67..0ecadcd2 100644 --- a/src/pages/__tests__/pages.a11y.test.tsx +++ b/src/pages/__tests__/pages.a11y.test.tsx @@ -89,7 +89,7 @@ const mockFlashcardDeck: FlashcardDeck = { updatedAt: '2024-01-01T00:00:00Z', }; -// Per-test overrides merged into the mocked useApp() return value. Default empty so +// Per-test overrides merged into the mocked domain-hook return value. Default empty so // the shared mock stays as the other suites expect; a suite that needs seeded data // (e.g. TestResultsPage's graded-submission view) sets this in beforeEach and resets // it in afterEach. @@ -97,7 +97,7 @@ let appStateOverride: Record = {}; // ─── Module mocks ────────────────────────────────────────────────────────────── -// Build the context value ONCE so useApp() returns stable array references across +// Build the context value ONCE so the domain hooks return stable array references across // renders — a fresh object/array each call makes any page whose effect depends on a // context array re-run that effect forever (setState → re-render → new array ref → // effect → …), which OOMs the worker. `appStateOverride` is spread on top so seeded @@ -216,7 +216,6 @@ const base = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => ({ ...base, ...appStateOverride }), useRoster: () => ({ ...base, ...appStateOverride }), useAuthoring: () => ({ ...base, ...appStateOverride }), useAssessment: () => ({ ...base, ...appStateOverride }), diff --git a/src/pages/__tests__/pages.admin.a11y.test.tsx b/src/pages/__tests__/pages.admin.a11y.test.tsx index 395fa9a2..33bbaa15 100644 --- a/src/pages/__tests__/pages.admin.a11y.test.tsx +++ b/src/pages/__tests__/pages.admin.a11y.test.tsx @@ -45,7 +45,7 @@ const mockRubric: Rubric = { // ─── Module mocks ────────────────────────────────────────────────────────────── -// Build the context value ONCE so useApp() returns stable array references across +// Build the context value ONCE so the domain hooks return stable array references across // renders — a fresh object/array each call makes any page whose effect depends on a // context array re-run that effect forever (setState → re-render → new array ref → // effect → …), which OOMs the worker. In the real app these references are stable. @@ -179,7 +179,6 @@ vi.mock('../../context/AppContext', () => { signOutFromDatabase: vi.fn(() => Promise.resolve()), }; return { - useApp: () => base, useRoster: () => base, useAuthoring: () => base, useAssessment: () => base, diff --git a/src/pages/__tests__/pages.deepcoverage.test.tsx b/src/pages/__tests__/pages.deepcoverage.test.tsx index 6baf29c7..c384f5b8 100644 --- a/src/pages/__tests__/pages.deepcoverage.test.tsx +++ b/src/pages/__tests__/pages.deepcoverage.test.tsx @@ -161,7 +161,6 @@ const mockUseApp = { }; vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/pages.deepcoverage2.test.tsx b/src/pages/__tests__/pages.deepcoverage2.test.tsx index 1844a011..7c9f7e98 100644 --- a/src/pages/__tests__/pages.deepcoverage2.test.tsx +++ b/src/pages/__tests__/pages.deepcoverage2.test.tsx @@ -172,7 +172,6 @@ Object.defineProperty(navigator, 'clipboard', { // ─── Module mocks ───────────────────────────────────────────────────────────── vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/pages.functions.test.tsx b/src/pages/__tests__/pages.functions.test.tsx index 449e3bab..98c73405 100644 --- a/src/pages/__tests__/pages.functions.test.tsx +++ b/src/pages/__tests__/pages.functions.test.tsx @@ -178,7 +178,6 @@ function makeApp(overrides = {}) { let currentApp = makeApp(); vi.mock('../../context/AppContext', () => ({ - useApp: () => currentApp, useRoster: () => currentApp, useAuthoring: () => currentApp, useAssessment: () => currentApp, diff --git a/src/pages/__tests__/pages.interaction.test.tsx b/src/pages/__tests__/pages.interaction.test.tsx index 1cb799ec..874da44c 100644 --- a/src/pages/__tests__/pages.interaction.test.tsx +++ b/src/pages/__tests__/pages.interaction.test.tsx @@ -160,7 +160,6 @@ const mockUseApp = { // ─── Module mocks ───────────────────────────────────────────────────────────── vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/pages.missing.test.tsx b/src/pages/__tests__/pages.missing.test.tsx index 2da64d2c..6fdc81d3 100644 --- a/src/pages/__tests__/pages.missing.test.tsx +++ b/src/pages/__tests__/pages.missing.test.tsx @@ -56,7 +56,6 @@ const makeAppContextMock = () => ({ getActiveGradeScale: vi.fn(), }); vi.mock('../../context/AppContext', () => ({ - useApp: () => makeAppContextMock(), useRoster: () => makeAppContextMock(), useAuthoring: () => makeAppContextMock(), useAssessment: () => makeAppContextMock(), diff --git a/src/pages/__tests__/pages.smoke.test.tsx b/src/pages/__tests__/pages.smoke.test.tsx index c8444108..b9f7b3a9 100644 --- a/src/pages/__tests__/pages.smoke.test.tsx +++ b/src/pages/__tests__/pages.smoke.test.tsx @@ -153,7 +153,6 @@ const mockUseApp = { // ─── Module mocks ────────────────────────────────────────────────────────────── vi.mock('../../context/AppContext', () => ({ - useApp: () => mockUseApp, useRoster: () => mockUseApp, useAuthoring: () => mockUseApp, useAssessment: () => mockUseApp, diff --git a/src/pages/__tests__/pages.student.a11y.test.tsx b/src/pages/__tests__/pages.student.a11y.test.tsx index ab7cd42b..b8afe498 100644 --- a/src/pages/__tests__/pages.student.a11y.test.tsx +++ b/src/pages/__tests__/pages.student.a11y.test.tsx @@ -45,7 +45,7 @@ const mockRubric: Rubric = { // ─── Module mocks ────────────────────────────────────────────────────────────── -// Build the context value ONCE so useApp() returns a stable object (and stable +// Build the context value ONCE so the domain hooks return a stable object (and stable // array references) across renders. A fresh object/array on every call makes any // page whose effect depends on a context array re-run that effect every render — // e.g. PeerReviewView setEntry()s a new object keyed on `peerReviews`, which loops @@ -198,7 +198,6 @@ vi.mock('../../context/AppContext', () => { fetchMyFlashcardReview: vi.fn(() => Promise.resolve(null)), }; return { - useApp: () => base, useRoster: () => base, useAuthoring: () => base, useAssessment: () => base, diff --git a/src/pages/__tests__/pages.unit.test.tsx b/src/pages/__tests__/pages.unit.test.tsx index 80bff03d..e33809d1 100644 --- a/src/pages/__tests__/pages.unit.test.tsx +++ b/src/pages/__tests__/pages.unit.test.tsx @@ -146,7 +146,6 @@ function makeApp(overrides = {}) { let currentApp = makeApp(); vi.mock('../../context/AppContext', () => ({ - useApp: () => currentApp, useRoster: () => currentApp, useAuthoring: () => currentApp, useAssessment: () => currentApp, From cc188e28f241a801b8c21d9c358f1f423dd9e690 Mon Sep 17 00:00:00 2001 From: NesiciCoding Date: Wed, 12 Aug 2026 21:57:24 +0200 Subject: [PATCH 2/2] test: describe currentStateRef sync as render-phase in regression comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ref is written during render, not in a layout effect — the first layout-effect run dispatches an update and the second run reads the state produced by the resulting render. --- src/context/AppContext.test.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/context/AppContext.test.tsx b/src/context/AppContext.test.tsx index f99bb207..ade2ecd0 100644 --- a/src/context/AppContext.test.tsx +++ b/src/context/AppContext.test.tsx @@ -773,10 +773,11 @@ describe('AppContext', () => { }); it('actions invoked from a layout effect see the latest rubric state', () => { - // Regression for currentStateRef sync timing: the provider must refresh the ref in the - // layout phase (parent layout effects fire before descendants'), so an action called from - // a descendant's useLayoutEffect — e.g. pre-filling a just-created rubric — reads the - // fresh snapshot instead of the previous render's. + // Regression for currentStateRef sync timing: the ref is written during render (not in an + // effect), so the first layout-effect run below dispatches an update and the second run — + // which fires after the re-render that dispatch produced — reads the ref's fresh snapshot + // instead of the previous render's. An effect-based sync (layout or passive) would lag + // behind by a commit and observe stale state. const entryCounts: number[] = []; const rubricIdRef: { current: string } = { current: '' }; @@ -846,8 +847,9 @@ describe('AppContext', () => { renderWithRouter(, { withAppProvider: true }); act(() => {}); // flush the re-render scheduled by the first layout effect - // With the fix the ref is refreshed in the layout phase, so the action sees the scale - // added in run 1; with a passive-effect sync it would still resolve the old default. + // The ref is written during render, so run 2 — which fires after the re-render caused by + // run 1's dispatch — sees the scale added in run 1; with an effect-based sync it would lag + // by a commit and still resolve the old default. expect(observedNames).toEqual(['Layout Scale']); }); });