Skip to content

Commit de371c1

Browse files
Minor adjustments
1 parent 6c7d54a commit de371c1

7 files changed

Lines changed: 93 additions & 175 deletions

File tree

src/__tests__/components/MorphemeEditor.test.tsx

Lines changed: 20 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ describe('MorphemeBreakdownPopover', () => {
1313
it('renders with the initial value pre-filled', () => {
1414
render(
1515
<MorphemeBreakdownPopover
16-
hasExistingBreakdown={false}
1716
initialValue="un- believe -able"
1817
onSave={jest.fn()}
1918
onClose={jest.fn()}
@@ -24,27 +23,15 @@ describe('MorphemeBreakdownPopover', () => {
2423
});
2524

2625
it('auto-focuses and selects the input on mount', () => {
27-
render(
28-
<MorphemeBreakdownPopover
29-
hasExistingBreakdown={false}
30-
initialValue="word"
31-
onSave={jest.fn()}
32-
onClose={jest.fn()}
33-
/>,
34-
);
26+
render(<MorphemeBreakdownPopover initialValue="word" onSave={jest.fn()} onClose={jest.fn()} />);
3527
expect(screen.getByRole('textbox')).toHaveFocus();
3628
});
3729

3830
it('calls onSave and onClose when Done button is clicked', async () => {
3931
const onSave = jest.fn();
4032
const onClose = jest.fn();
4133
render(
42-
<MorphemeBreakdownPopover
43-
hasExistingBreakdown={false}
44-
initialValue="un- believe"
45-
onSave={onSave}
46-
onClose={onClose}
47-
/>,
34+
<MorphemeBreakdownPopover initialValue="un- believe" onSave={onSave} onClose={onClose} />,
4835
);
4936
await userEvent.click(screen.getByRole('button', { name: 'Done' }));
5037
expect(onSave).toHaveBeenCalledWith('un- believe');
@@ -53,14 +40,7 @@ describe('MorphemeBreakdownPopover', () => {
5340

5441
it('calls onSave with the edited value', async () => {
5542
const onSave = jest.fn();
56-
render(
57-
<MorphemeBreakdownPopover
58-
hasExistingBreakdown={false}
59-
initialValue="word"
60-
onSave={onSave}
61-
onClose={jest.fn()}
62-
/>,
63-
);
43+
render(<MorphemeBreakdownPopover initialValue="word" onSave={onSave} onClose={jest.fn()} />);
6444
await userEvent.clear(screen.getByRole('textbox'));
6545
await userEvent.type(screen.getByRole('textbox'), 'wor -d');
6646
await userEvent.click(screen.getByRole('button', { name: 'Done' }));
@@ -70,14 +50,7 @@ describe('MorphemeBreakdownPopover', () => {
7050
it('commits on Enter key', async () => {
7151
const onSave = jest.fn();
7252
const onClose = jest.fn();
73-
render(
74-
<MorphemeBreakdownPopover
75-
hasExistingBreakdown={false}
76-
initialValue="test"
77-
onSave={onSave}
78-
onClose={onClose}
79-
/>,
80-
);
53+
render(<MorphemeBreakdownPopover initialValue="test" onSave={onSave} onClose={onClose} />);
8154
await userEvent.keyboard('{Enter}');
8255
expect(onSave).toHaveBeenCalledWith('test');
8356
expect(onClose).toHaveBeenCalledTimes(1);
@@ -86,14 +59,7 @@ describe('MorphemeBreakdownPopover', () => {
8659
it('dismisses without saving on Escape key', async () => {
8760
const onSave = jest.fn();
8861
const onClose = jest.fn();
89-
render(
90-
<MorphemeBreakdownPopover
91-
hasExistingBreakdown={false}
92-
initialValue="test"
93-
onSave={onSave}
94-
onClose={onClose}
95-
/>,
96-
);
62+
render(<MorphemeBreakdownPopover initialValue="test" onSave={onSave} onClose={onClose} />);
9763
await userEvent.keyboard('{Escape}');
9864
expect(onSave).not.toHaveBeenCalled();
9965
expect(onClose).toHaveBeenCalledTimes(1);
@@ -102,80 +68,36 @@ describe('MorphemeBreakdownPopover', () => {
10268
it('dismisses without saving when Cancel button is clicked', async () => {
10369
const onSave = jest.fn();
10470
const onClose = jest.fn();
105-
render(
106-
<MorphemeBreakdownPopover
107-
hasExistingBreakdown={false}
108-
initialValue="test"
109-
onSave={onSave}
110-
onClose={onClose}
111-
/>,
112-
);
71+
render(<MorphemeBreakdownPopover initialValue="test" onSave={onSave} onClose={onClose} />);
11372
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
11473
expect(onSave).not.toHaveBeenCalled();
11574
expect(onClose).toHaveBeenCalledTimes(1);
11675
});
11776

118-
it('saves when the backdrop is clicked and a breakdown already exists', async () => {
119-
const onSave = jest.fn();
120-
render(
121-
<MorphemeBreakdownPopover
122-
hasExistingBreakdown
123-
initialValue="test"
124-
onSave={onSave}
125-
onClose={jest.fn()}
126-
/>,
127-
);
128-
// The backdrop is the fixed full-screen div; getByRole won't find it, so query the DOM.
129-
const backdrop = document.querySelector('.tw\\:fixed.tw\\:inset-0');
130-
if (!backdrop) throw new Error('Backdrop not found');
131-
await userEvent.click(backdrop);
132-
expect(onSave).toHaveBeenCalledWith('test');
133-
});
134-
13577
it('closes when the backdrop is clicked', async () => {
13678
const onClose = jest.fn();
137-
render(
138-
<MorphemeBreakdownPopover
139-
hasExistingBreakdown
140-
initialValue="test"
141-
onSave={jest.fn()}
142-
onClose={onClose}
143-
/>,
144-
);
79+
render(<MorphemeBreakdownPopover initialValue="test" onSave={jest.fn()} onClose={onClose} />);
80+
// The backdrop is the fixed full-screen div; getByRole won't find it, so query the DOM.
14581
const backdrop = document.querySelector('.tw\\:fixed.tw\\:inset-0');
14682
if (!backdrop) throw new Error('Backdrop not found');
14783
await userEvent.click(backdrop);
14884
expect(onClose).toHaveBeenCalledTimes(1);
14985
});
15086

151-
it('saves on backdrop click when no breakdown exists but the text was edited', async () => {
87+
it('saves on backdrop click when the text was edited', async () => {
15288
const onSave = jest.fn();
153-
render(
154-
<MorphemeBreakdownPopover
155-
hasExistingBreakdown={false}
156-
initialValue="test"
157-
onSave={onSave}
158-
onClose={jest.fn()}
159-
/>,
160-
);
89+
render(<MorphemeBreakdownPopover initialValue="test" onSave={onSave} onClose={jest.fn()} />);
16190
await userEvent.type(screen.getByRole('textbox'), ' -er');
16291
const backdrop = document.querySelector('.tw\\:fixed.tw\\:inset-0');
16392
if (!backdrop) throw new Error('Backdrop not found');
16493
await userEvent.click(backdrop);
16594
expect(onSave).toHaveBeenCalledWith('test -er');
16695
});
16796

168-
it('does not save on backdrop click when no breakdown exists and the text is unchanged', async () => {
97+
it('does not save on backdrop click when the text is unchanged', async () => {
16998
const onSave = jest.fn();
17099
const onClose = jest.fn();
171-
render(
172-
<MorphemeBreakdownPopover
173-
hasExistingBreakdown={false}
174-
initialValue="test"
175-
onSave={onSave}
176-
onClose={onClose}
177-
/>,
178-
);
100+
render(<MorphemeBreakdownPopover initialValue="test" onSave={onSave} onClose={onClose} />);
179101
const backdrop = document.querySelector('.tw\\:fixed.tw\\:inset-0');
180102
if (!backdrop) throw new Error('Backdrop not found');
181103
await userEvent.click(backdrop);
@@ -185,14 +107,7 @@ describe('MorphemeBreakdownPopover', () => {
185107

186108
it('does not save on backdrop click when the input is only whitespace', async () => {
187109
const onSave = jest.fn();
188-
render(
189-
<MorphemeBreakdownPopover
190-
hasExistingBreakdown
191-
initialValue=" "
192-
onSave={onSave}
193-
onClose={jest.fn()}
194-
/>,
195-
);
110+
render(<MorphemeBreakdownPopover initialValue=" " onSave={onSave} onClose={jest.fn()} />);
196111
const backdrop = document.querySelector('.tw\\:fixed.tw\\:inset-0');
197112
if (!backdrop) throw new Error('Backdrop not found');
198113
await userEvent.click(backdrop);
@@ -201,56 +116,28 @@ describe('MorphemeBreakdownPopover', () => {
201116

202117
it('does not dismiss when clicking inside the popover panel', async () => {
203118
const onClose = jest.fn();
204-
render(
205-
<MorphemeBreakdownPopover
206-
hasExistingBreakdown={false}
207-
initialValue="test"
208-
onSave={jest.fn()}
209-
onClose={onClose}
210-
/>,
211-
);
119+
render(<MorphemeBreakdownPopover initialValue="test" onSave={jest.fn()} onClose={onClose} />);
212120
const label = screen.getByText('Split into morphemes');
213121
await userEvent.click(label);
214122
expect(onClose).not.toHaveBeenCalled();
215123
});
216124

217125
it('does not call onSave when the input is empty', async () => {
218126
const onSave = jest.fn();
219-
render(
220-
<MorphemeBreakdownPopover
221-
hasExistingBreakdown={false}
222-
initialValue=""
223-
onSave={onSave}
224-
onClose={jest.fn()}
225-
/>,
226-
);
127+
render(<MorphemeBreakdownPopover initialValue="" onSave={onSave} onClose={jest.fn()} />);
227128
await userEvent.click(screen.getByRole('button', { name: 'Done' }));
228129
expect(onSave).not.toHaveBeenCalled();
229130
});
230131

231132
it('does not call onSave when the input is only whitespace', async () => {
232133
const onSave = jest.fn();
233-
render(
234-
<MorphemeBreakdownPopover
235-
hasExistingBreakdown={false}
236-
initialValue=" "
237-
onSave={onSave}
238-
onClose={jest.fn()}
239-
/>,
240-
);
134+
render(<MorphemeBreakdownPopover initialValue=" " onSave={onSave} onClose={jest.fn()} />);
241135
await userEvent.click(screen.getByRole('button', { name: 'Done' }));
242136
expect(onSave).not.toHaveBeenCalled();
243137
});
244138

245139
it('does not render a Delete button when onDelete is not provided', () => {
246-
render(
247-
<MorphemeBreakdownPopover
248-
hasExistingBreakdown
249-
initialValue="test"
250-
onSave={jest.fn()}
251-
onClose={jest.fn()}
252-
/>,
253-
);
140+
render(<MorphemeBreakdownPopover initialValue="test" onSave={jest.fn()} onClose={jest.fn()} />);
254141
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
255142
});
256143

@@ -260,7 +147,6 @@ describe('MorphemeBreakdownPopover', () => {
260147
const onClose = jest.fn();
261148
render(
262149
<MorphemeBreakdownPopover
263-
hasExistingBreakdown
264150
initialValue="un- believe"
265151
onSave={onSave}
266152
onClose={onClose}
@@ -274,14 +160,7 @@ describe('MorphemeBreakdownPopover', () => {
274160
});
275161

276162
it('portals the panel to document.body so segment rows cannot stack above it', () => {
277-
render(
278-
<MorphemeBreakdownPopover
279-
hasExistingBreakdown={false}
280-
initialValue="test"
281-
onSave={jest.fn()}
282-
onClose={jest.fn()}
283-
/>,
284-
);
163+
render(<MorphemeBreakdownPopover initialValue="test" onSave={jest.fn()} onClose={jest.fn()} />);
285164
const panel = screen.getByText('Split into morphemes').closest('div');
286165
expect(panel?.parentElement).toBe(document.body);
287166
});
@@ -292,14 +171,7 @@ describe('MorphemeBreakdownPopover', () => {
292171
.spyOn(Element.prototype, 'getBoundingClientRect')
293172
.mockReturnValueOnce(new DOMRect(50, 100, 40, 20))
294173
.mockReturnValueOnce(new DOMRect(0, 0, 200, 100));
295-
render(
296-
<MorphemeBreakdownPopover
297-
hasExistingBreakdown={false}
298-
initialValue="test"
299-
onSave={jest.fn()}
300-
onClose={jest.fn()}
301-
/>,
302-
);
174+
render(<MorphemeBreakdownPopover initialValue="test" onSave={jest.fn()} onClose={jest.fn()} />);
303175
const panel = screen.getByText('Split into morphemes').closest('div');
304176
// Anchor bottom (120) plus the 4px margin.
305177
expect(panel).toHaveStyle({ top: '124px', left: '50px' });
@@ -312,14 +184,7 @@ describe('MorphemeBreakdownPopover', () => {
312184
.spyOn(Element.prototype, 'getBoundingClientRect')
313185
.mockReturnValueOnce(new DOMRect(50, 700, 40, 20))
314186
.mockReturnValueOnce(new DOMRect(0, 0, 200, 100));
315-
render(
316-
<MorphemeBreakdownPopover
317-
hasExistingBreakdown={false}
318-
initialValue="test"
319-
onSave={jest.fn()}
320-
onClose={jest.fn()}
321-
/>,
322-
);
187+
render(<MorphemeBreakdownPopover initialValue="test" onSave={jest.fn()} onClose={jest.fn()} />);
323188
const panel = screen.getByText('Split into morphemes').closest('div');
324189
// Anchor top (700) minus panel height (100) minus the 4px margin.
325190
expect(panel).toHaveStyle({ top: '596px', left: '50px' });

src/__tests__/components/PhraseBox.test.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,31 @@ jest.mock('../../components/TokenChip', () => {
5858
* @param props.token - The word token to render.
5959
* @param props.isSplitFree - When true, marks the chip as a would-be-free token.
6060
* @param props.onRemove - Called when the remove button is clicked; omitted for edge tokens.
61+
* @param props.showMorphology - Exposed as a data attribute so tests can verify every PhraseBox
62+
* render path forwards the strip-wide morphology toggle.
6163
* @returns A span containing the surface text, a gloss input, and an optional remove button.
6264
*/
6365
function MockTokenChip({
6466
onFocus,
6567
token,
6668
isSplitFree,
6769
onRemove,
70+
showMorphology,
6871
}: Readonly<{
6972
onFocus?: () => void;
7073
token: Token;
7174
isSplitFree?: boolean;
7275
onRemove?: () => void;
76+
showMorphology?: boolean;
7377
}>) {
7478
const gloss = mockUseGloss(token.ref);
7579
const dispatch = mockUseGlossDispatch();
7680
return (
77-
<span data-testid={`token-${token.ref}`} data-split-free={isSplitFree ? 'true' : 'false'}>
81+
<span
82+
data-testid={`token-${token.ref}`}
83+
data-split-free={isSplitFree ? 'true' : 'false'}
84+
data-show-morphology={showMorphology ? 'true' : 'false'}
85+
>
7886
{token.surfaceText}
7987
<input
8088
aria-label={`Gloss for ${token.surfaceText}`}
@@ -511,6 +519,21 @@ describe('PhraseBox', () => {
511519
expect(screen.getByTestId('inert-punct-1')).toBeInTheDocument();
512520
});
513521

522+
it('forwards showMorphology to chips in a non-edit-target box during edit mode', () => {
523+
renderBox(
524+
<PhraseBox {...requiredProps()} tokens={[TEST_TOKEN, TEST_TOKEN_2]} />,
525+
// Edit mode is active for a different phrase, so this free box renders via the fallback
526+
// path; its chips must keep their morpheme rows rather than collapsing while editing.
527+
{
528+
phraseMode: { kind: 'edit', phraseId: 'other-phrase', originalTokens: [] },
529+
showMorphology: true,
530+
},
531+
);
532+
533+
expect(screen.getByTestId('token-token-1')).toHaveAttribute('data-show-morphology', 'true');
534+
expect(screen.getByTestId('token-token-2')).toHaveAttribute('data-show-morphology', 'true');
535+
});
536+
514537
it('renders punctuation between tokens in confirm-unlink mode', () => {
515538
mockUsePhraseLinkForToken.mockReturnValue(TEST_PHRASE_LINK);
516539
renderBox(

src/__tests__/store/analysisSlice.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,26 @@ describe('writeMorphemes', () => {
600600
expect(updated?.morphemes?.[2].form).toBe('-able');
601601
});
602602

603+
it('preserves distinct glosses on duplicate morpheme forms in order (reduplication)', () => {
604+
const ta: TokenAnalysis = {
605+
id: 'ta-1',
606+
surfaceText: 'baba',
607+
morphemes: [
608+
{ id: 'm-1', form: 'ba', writingSystem: 'und', gloss: { und: 'first' } },
609+
{ id: 'm-2', form: 'ba', writingSystem: 'und', gloss: { und: 'second' } },
610+
],
611+
};
612+
const store = createAnalysisStore({
613+
analysis: { analysis: makeAnalysis(ta), analysisLanguage: 'und' },
614+
});
615+
616+
store.dispatch(writeMorphemes('tok-1', 'baba', ['ba', 'ba']));
617+
618+
const updated = store.getState().analysis.analysis.tokenAnalyses.find((a) => a.id === 'ta-1');
619+
expect(updated?.morphemes?.[0].gloss).toStrictEqual({ und: 'first' });
620+
expect(updated?.morphemes?.[1].gloss).toStrictEqual({ und: 'second' });
621+
});
622+
603623
it('removes an orphaned approved link and creates a fresh analysis', () => {
604624
const orphanLink: TokenAnalysisLink = {
605625
analysisId: 'old-uuid',

0 commit comments

Comments
 (0)