Skip to content

Commit eb96dac

Browse files
Fix Tailwind classes, clean up test files, ignore branches that don't need testing, reintroduce createSourceIsSelect to reopen SelectProjectModal if user cancels project creation
1 parent 9a7acb6 commit eb96dac

14 files changed

Lines changed: 124 additions & 367 deletions

src/__tests__/components/CreateProjectModal.test.tsx

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,6 @@ describe('CreateProjectModal', () => {
4848
false,
4949
]);
5050
});
51-
it('renders the heading and analysis language input', () => {
52-
render(<CreateProjectModal projectId={testProjectId} onClose={() => {}} />);
53-
54-
expect(
55-
screen.getByRole('heading', { name: /create interlinear project/i }),
56-
).toBeInTheDocument();
57-
expect(screen.getByLabelText(/analysis language/i)).toBeInTheDocument();
58-
});
59-
6051
it('calls onClose when cancel is clicked', async () => {
6152
const onClose = jest.fn();
6253
render(<CreateProjectModal projectId={testProjectId} onClose={onClose} />);
@@ -159,14 +150,6 @@ describe('CreateProjectModal', () => {
159150
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
160151
});
161152

162-
it('renders nothing while strings are loading', () => {
163-
jest.mocked(useLocalizedStrings).mockReturnValue([{}, true]);
164-
const { container } = render(
165-
<CreateProjectModal projectId={testProjectId} onClose={() => {}} />,
166-
);
167-
expect(container.firstChild).toBeNull();
168-
});
169-
170153
it('calls onProjectCreated with the parsed project when sendCommand returns a project JSON', async () => {
171154
const persistedProject = {
172155
id: 'new-project-id',

src/__tests__/components/InterlinearizerLoader.test.tsx

Lines changed: 8 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Book } from 'interlinearizer';
1010
import useInterlinearizerBookData from '../../hooks/useInterlinearizerBookData';
1111
import useOptimisticBooleanSetting from '../../hooks/useOptimisticBooleanSetting';
1212
import InterlinearizerLoader from '../../components/InterlinearizerLoader';
13+
import { makeWebViewState } from '../test-helpers';
1314

1415
jest.mock('../../hooks/useInterlinearizerBookData');
1516
jest.mock('../../hooks/useOptimisticBooleanSetting');
@@ -220,55 +221,14 @@ const GEN_1_1_BOOK: Book = {
220221
],
221222
};
222223

223-
/**
224-
* Returns a `useWebViewScrollGroupScrRef` hook stub bound to the given reference and setter.
225-
*
226-
* @param scrRef - Scripture reference to expose; defaults to GEN 1:1
227-
* @param setScrRef - Setter callback; defaults to a no-op
228-
*/
229-
function makeScrollGroupHook(
230-
scrRef: SerializedVerseRef = defaultScrRef,
231-
setScrRef: (r: SerializedVerseRef) => void = () => {},
232-
) {
224+
/** Returns a `useWebViewScrollGroupScrRef` hook stub fixed to GEN 1:1. */
225+
function makeScrollGroupHook() {
233226
return (): [
234227
SerializedVerseRef,
235228
(r: SerializedVerseRef) => void,
236229
number | undefined,
237230
(id: number | undefined) => void,
238-
] => [scrRef, setScrRef, undefined, () => {}];
239-
}
240-
241-
/** Typed read/write pair stored per key in {@link makeWebViewState}. */
242-
type StateSlot<T> = { get: () => T; set: (v: T) => void };
243-
244-
/**
245-
* Returns a `useWebViewState` hook stub that stores values in typed per-key closures so state
246-
* persists across re-renders within the same test without requiring any type assertions.
247-
*/
248-
function makeWebViewState() {
249-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
250-
const slots = new Map<string, StateSlot<any>>();
251-
return <T,>(key: string, defaultValue: T): [T, (v: T) => void, () => void] => {
252-
let slot: StateSlot<T> | undefined = slots.get(key);
253-
if (slot === undefined) {
254-
let stored = defaultValue;
255-
slot = {
256-
get: () => stored,
257-
set: (v) => {
258-
stored = v;
259-
},
260-
};
261-
slots.set(key, slot);
262-
}
263-
const resolvedSlot = slot;
264-
return [
265-
resolvedSlot.get(),
266-
(v: T) => resolvedSlot.set(v),
267-
() => {
268-
slots.delete(key);
269-
},
270-
];
271-
};
231+
] => [defaultScrRef, () => {}, undefined, () => {}];
272232
}
273233

274234
/**
@@ -365,24 +325,7 @@ describe('InterlinearizerLoader', () => {
365325
expect(screen.getByText('Loading…')).toBeInTheDocument();
366326
});
367327

368-
it('shows an error when no USJ book is available for the project', () => {
369-
mockBookData({
370-
book: undefined,
371-
bookError: 'No USJ book available for GEN in project test-project-id',
372-
});
373-
render(
374-
<InterlinearizerLoader
375-
projectId={testProjectId}
376-
useWebViewScrollGroupScrRef={makeScrollGroupHook()}
377-
useWebViewState={makeWebViewState()}
378-
/>,
379-
);
380-
381-
expect(screen.getByRole('heading', { name: /error loading book/i })).toBeInTheDocument();
382-
expect(screen.getByText(/no usj book available for gen in project/i)).toBeInTheDocument();
383-
});
384-
385-
it('shows an error heading and message when book data is a PlatformError', () => {
328+
it('shows an error heading and message when bookError is set', () => {
386329
mockBookData({ book: undefined, bookError: 'Project not found' });
387330
render(
388331
<InterlinearizerLoader
@@ -430,48 +373,6 @@ describe('InterlinearizerLoader', () => {
430373
expect(screen.getByText('unexpected string error')).toBeInTheDocument();
431374
});
432375

433-
it('passes a book-stable ref to BookUSJ so chapter and verse changes do not re-fetch the book', () => {
434-
// The book-stable ref logic lives in useInterlinearizerBookData, which is tested in its own
435-
// test file. This test verifies that InterlinearizerLoader passes scrRef into the hook and that
436-
// re-rendering with a new verse does not cause the hook to receive a new book ref.
437-
const webViewState = makeWebViewState();
438-
const { rerender } = render(
439-
<InterlinearizerLoader
440-
projectId={testProjectId}
441-
useWebViewScrollGroupScrRef={makeScrollGroupHook()}
442-
useWebViewState={webViewState}
443-
/>,
444-
);
445-
rerender(
446-
<InterlinearizerLoader
447-
projectId={testProjectId}
448-
useWebViewScrollGroupScrRef={makeScrollGroupHook({
449-
book: 'GEN',
450-
chapterNum: 2,
451-
verseNum: 5,
452-
})}
453-
useWebViewState={webViewState}
454-
/>,
455-
);
456-
457-
const { calls } = jest.mocked(useInterlinearizerBookData).mock;
458-
expect(calls.length).toBeGreaterThanOrEqual(2);
459-
calls.forEach((args) => expect(args[0].projectId).toBe(testProjectId));
460-
});
461-
462-
it('passes continuousScroll from useOptimisticBooleanSetting to Interlinearizer', () => {
463-
mockOptimisticSetting(true);
464-
render(
465-
<InterlinearizerLoader
466-
projectId={testProjectId}
467-
useWebViewScrollGroupScrRef={makeScrollGroupHook()}
468-
useWebViewState={makeWebViewState()}
469-
/>,
470-
);
471-
472-
expect(screen.getByTestId('continuous-scroll-toggle')).toBeInTheDocument();
473-
});
474-
475376
it('passes checked and disabled from useOptimisticBooleanSetting to ContinuousScrollToggle', () => {
476377
mockOptimisticSetting(true, jest.fn(), true);
477378
render(
@@ -502,22 +403,6 @@ describe('InterlinearizerLoader', () => {
502403
expect(mockOnChange).toHaveBeenCalledWith(true);
503404
});
504405

505-
it('clicking the continuous scroll toggle calls onChange with the toggled value', async () => {
506-
const mockOnChange = jest.fn();
507-
mockOptimisticSetting(true, mockOnChange);
508-
render(
509-
<InterlinearizerLoader
510-
projectId={testProjectId}
511-
useWebViewScrollGroupScrRef={makeScrollGroupHook()}
512-
useWebViewState={makeWebViewState()}
513-
/>,
514-
);
515-
516-
await userEvent.click(screen.getByTestId('continuous-scroll-toggle'));
517-
518-
expect(mockOnChange).toHaveBeenCalledWith(false);
519-
});
520-
521406
it('passes continuousScroll=true to Interlinearizer when the setting is true', () => {
522407
mockOptimisticSetting(true);
523408
render(
@@ -620,23 +505,6 @@ describe('InterlinearizerLoader', () => {
620505
expect(screen.getByTestId('create-modal')).toBeInTheDocument();
621506
});
622507

623-
it('closes the create modal and sets the active project when a project is created', async () => {
624-
const state = makeWebViewState();
625-
render(
626-
<InterlinearizerLoader
627-
projectId={testProjectId}
628-
useWebViewScrollGroupScrRef={makeScrollGroupHook()}
629-
useWebViewState={state}
630-
/>,
631-
);
632-
633-
await userEvent.click(screen.getByTestId('tab-toolbar-project-menu'));
634-
await userEvent.click(screen.getByTestId('select-modal-create-new'));
635-
await userEvent.click(screen.getByTestId('create-modal-created'));
636-
637-
expect(screen.queryByTestId('create-modal')).not.toBeInTheDocument();
638-
});
639-
640508
it('closes all modals after a project is created from the select modal', async () => {
641509
render(
642510
<InterlinearizerLoader
@@ -667,6 +535,9 @@ describe('InterlinearizerLoader', () => {
667535
await userEvent.click(screen.getByTestId('select-modal-select'));
668536

669537
expect(screen.queryByTestId('select-modal')).not.toBeInTheDocument();
538+
// After selection the view-project-info button becomes available, confirming activeProject is set
539+
await userEvent.click(screen.getByTestId('tab-toolbar-view-project-info'));
540+
expect(screen.getByTestId('metadata-modal')).toBeInTheDocument();
670541
});
671542

672543
it('opens the metadata modal from the openProjectInfoModal menu item when a project is active', async () => {
@@ -753,33 +624,6 @@ describe('InterlinearizerLoader', () => {
753624
expect(screen.queryByTestId('metadata-modal')).not.toBeInTheDocument();
754625
});
755626

756-
it('handles topMenu with no items array without throwing', () => {
757-
jest.mocked(useData).mockReturnValue(
758-
new Proxy(
759-
{},
760-
{
761-
get: () =>
762-
jest
763-
.fn()
764-
.mockReturnValue([
765-
{ topMenu: { label: 'top' }, includeDefaults: true, contextMenu: undefined },
766-
jest.fn(),
767-
false,
768-
]),
769-
},
770-
),
771-
);
772-
render(
773-
<InterlinearizerLoader
774-
projectId={testProjectId}
775-
useWebViewScrollGroupScrRef={makeScrollGroupHook()}
776-
useWebViewState={makeWebViewState()}
777-
/>,
778-
);
779-
780-
expect(screen.getByTestId('tab-toolbar')).toBeInTheDocument();
781-
});
782-
783627
it('renders without error when useData provides a topMenu with items', () => {
784628
const mockWebViewMenu = {
785629
topMenu: {

src/__tests__/components/ProjectMetadataModal.test.tsx

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,6 @@ describe('ProjectMetadataModal', () => {
5252
jest.mocked(papi.notifications.send).mockResolvedValue('mock-notification-id');
5353
});
5454

55-
it('renders nothing while strings are loading', () => {
56-
jest.mocked(useLocalizedStrings).mockReturnValue([{}, true]);
57-
const { container } = render(<ProjectMetadataModal {...testProps} />);
58-
expect(container.firstChild).toBeNull();
59-
});
60-
6155
it('renders the modal heading', () => {
6256
render(<ProjectMetadataModal {...testProps} />);
6357
expect(screen.getByRole('heading', { name: /project info/i })).toBeInTheDocument();
@@ -180,26 +174,6 @@ describe('ProjectMetadataModal', () => {
180174
);
181175
});
182176

183-
it('replaces an existing description when description is cleared and retyped', async () => {
184-
render(<ProjectMetadataModal {...testProps} description="Old Desc" />);
185-
186-
const descInput = screen.getByLabelText(/^description$/i);
187-
await userEvent.clear(descInput);
188-
await userEvent.type(descInput, 'Replaced');
189-
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
190-
191-
await waitFor(() =>
192-
expect(mockSendCommand).toHaveBeenCalledWith(
193-
'interlinearizer.updateProjectMetadata',
194-
'il-project-uuid',
195-
undefined,
196-
'Replaced',
197-
['en'],
198-
undefined,
199-
),
200-
);
201-
});
202-
203177
it('sends undefined description when description is cleared', async () => {
204178
render(<ProjectMetadataModal {...testProps} description="Old Desc" />);
205179

@@ -234,16 +208,6 @@ describe('ProjectMetadataModal', () => {
234208
expect(onClose).not.toHaveBeenCalled();
235209
});
236210

237-
it('does not send a notification when save sendCommand resolves with undefined (project not found)', async () => {
238-
mockSendCommand.mockResolvedValue(undefined);
239-
render(<ProjectMetadataModal {...testProps} />);
240-
241-
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
242-
243-
await waitFor(() => expect(mockSendCommand).toHaveBeenCalled());
244-
expect(papi.notifications.send).not.toHaveBeenCalled();
245-
});
246-
247211
it('does not call onProjectSaved, onClose, or send a notification when save sendCommand rejects', async () => {
248212
mockSendCommand.mockRejectedValue(new Error('save failed'));
249213
const onProjectSaved = jest.fn();

0 commit comments

Comments
 (0)