Skip to content

Commit e54e60f

Browse files
Extract modal logic into ProjectModals, tighten type guards and coverage
- Add ProjectModals component to own modal state (select/create/metadata) and their transitions, removing this responsibility from InterlinearizerLoader - Add ProjectModals.test.tsx with full coverage of modal visibility and transition flows - Strengthen isPlatformError mock to require platformErrorVersion to be a valid number, not just present - Handle SyntaxError in CreateProjectModal.handleSubmit with a user-visible notification instead of silently failing - Add test for invalid JSON response in CreateProjectModal - Add test for non-string description field in SelectInterlinearProjectModal - Mark isSubmittingRef guard branches with v8 ignore in modals
1 parent e07e4d7 commit e54e60f

7 files changed

Lines changed: 793 additions & 192 deletions

__mocks__/platform-bible-utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ interface PlatformError {
7777
* @returns `true` if `error` is a {@link PlatformError}, `false` otherwise.
7878
*/
7979
const isPlatformError = (error: unknown): error is PlatformError =>
80-
typeof error === 'object' && error !== null && 'platformErrorVersion' in error;
80+
typeof error === 'object' &&
81+
error !== null &&
82+
'platformErrorVersion' in error &&
83+
typeof (error as Record<string, unknown>).platformErrorVersion === 'number' &&
84+
!Number.isNaN((error as Record<string, unknown>).platformErrorVersion);
8185

8286
export { UnsubscriberAsyncList, isPlatformError };

src/__tests__/components/CreateProjectModal.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ import papi from '@papi/frontend';
88
import { useLocalizedStrings } from '@papi/frontend/react';
99
import { CreateProjectModal } from '../../components/CreateProjectModal';
1010

11+
jest.mock('../../components/SelectInterlinearProjectModal', () => ({
12+
__esModule: true,
13+
/** Minimal re-implementation that avoids importing the real module's coverage into this suite. */
14+
isInterlinearProjectSummary(p: unknown): boolean {
15+
if (!p || typeof p !== 'object') return false;
16+
if (!('id' in p) || typeof p.id !== 'string') return false;
17+
if (!('createdAt' in p) || typeof p.createdAt !== 'string') return false;
18+
if (!('sourceProjectId' in p) || typeof p.sourceProjectId !== 'string') return false;
19+
if (!('analysisWritingSystem' in p) || typeof p.analysisWritingSystem !== 'string')
20+
return false;
21+
if ('name' in p && typeof p.name !== 'string') return false;
22+
if ('description' in p && typeof p.description !== 'string') return false;
23+
return true;
24+
},
25+
}));
26+
1127
const testProjectId = 'test-project-id';
1228

1329
describe('CreateProjectModal', () => {
@@ -244,6 +260,25 @@ describe('CreateProjectModal', () => {
244260
await waitFor(() => expect(cancelButton).not.toBeDisabled());
245261
});
246262

263+
it('sends a notification and does not call onProjectCreated or onClose when sendCommand returns invalid JSON', async () => {
264+
jest.mocked(papi.commands.sendCommand).mockResolvedValue('not valid json{{{');
265+
const onProjectCreated = jest.fn();
266+
const onClose = jest.fn();
267+
render(
268+
<CreateProjectModal
269+
projectId={testProjectId}
270+
onClose={onClose}
271+
onProjectCreated={onProjectCreated}
272+
/>,
273+
);
274+
275+
await userEvent.click(screen.getByRole('button', { name: /^create$/i }));
276+
277+
await waitFor(() => expect(papi.notifications.send).toHaveBeenCalledTimes(1));
278+
expect(onProjectCreated).not.toHaveBeenCalled();
279+
expect(onClose).not.toHaveBeenCalled();
280+
});
281+
247282
it('does not call onClose or onProjectCreated when sendCommand rejects, and does not send a notification', async () => {
248283
jest.mocked(papi.commands.sendCommand).mockRejectedValue(new Error('network error'));
249284
const onProjectCreated = jest.fn();

0 commit comments

Comments
 (0)