@guardian/stand 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,450 +0,0 @@
1
- import { act, render } from '@testing-library/react';
2
- import { screen } from '@testing-library/react';
3
- import userEvent from '@testing-library/user-event';
4
- import type { EditorView } from 'prosemirror-view';
5
- import { mockEditorViewMethods } from '../mocks/prosemirror-view';
6
- import { Byline } from './Byline';
7
- import type { BylineModel, TaggedContributor } from './lib';
8
-
9
- jest.mock('prosemirror-view', () => {
10
- const actualProsemirrorView = jest.requireActual('prosemirror-view');
11
- const Class = actualProsemirrorView.EditorView as typeof EditorView;
12
- class MockEditorView extends Class {
13
- posAtCoords = mockEditorViewMethods.posAtCoords;
14
- coordsAtPos = mockEditorViewMethods.coordsAtPos;
15
- }
16
- return {
17
- ...actualProsemirrorView,
18
- EditorView: MockEditorView,
19
- };
20
- });
21
-
22
- // Mock the offsetParent used for visibility in the test dom
23
- // See https://github.com/jsdom/jsdom/issues/1261
24
- Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
25
- get() {
26
- return this.parentNode;
27
- },
28
- });
29
-
30
- // Mock scrollIntoView for testing, else we get `TypeError: selectedOption.scrollIntoView is not a function` errors
31
- Element.prototype.scrollIntoView = jest.fn();
32
-
33
- const mockSearchContributors: (
34
- selectedText: string,
35
- ) => Promise<TaggedContributor[]> = (selectedText: string) => {
36
- const allContributors: TaggedContributor[] = [
37
- {
38
- path: 'profile/mahesh-makani',
39
- label: 'Mahesh Makani',
40
- internalLabel: 'Mahesh Makani (Software Engineer)',
41
- tagId: '1',
42
- },
43
- {
44
- path: 'profile/andrew-howe-ely',
45
- label: 'Andrew Howe-Ely',
46
- tagId: '2',
47
- },
48
- {
49
- path: 'profile/jane-smith',
50
- label: 'Jane Smith',
51
- internalLabel: 'Jane Smith (Journalist)',
52
- tagId: '3',
53
- },
54
- {
55
- path: 'profile/john-doe',
56
- label: 'John Doe',
57
- tagId: '4',
58
- },
59
- ];
60
-
61
- const filteredContributors = allContributors.filter((contributor) =>
62
- contributor.label
63
- .toLowerCase()
64
- .startsWith(selectedText.toLowerCase().trim()),
65
- );
66
-
67
- return Promise.resolve(filteredContributors);
68
- };
69
-
70
- describe('Byline editor', () => {
71
- it('shows dropdown with untagged contributor suggestion, hides when click away', async () => {
72
- const user = userEvent.setup();
73
- render(
74
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
75
- );
76
-
77
- await act(async () => {
78
- await user.type(screen.getByRole('combobox'), 'Test');
79
- });
80
-
81
- const dropdown = screen.getByRole('listbox');
82
- expect(dropdown).toHaveTextContent(
83
- 'Add "Test" as untagged contributor',
84
- );
85
-
86
- await act(async () => {
87
- await user.click(document.body);
88
- });
89
- expect(dropdown).not.toBeVisible();
90
- });
91
- it('adds a chip from the dropdown', async () => {
92
- const user = userEvent.setup();
93
- render(
94
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
95
- );
96
-
97
- const editor = screen.getByRole('combobox');
98
- await act(async () => {
99
- await user.type(screen.getByRole('combobox'), 'Test');
100
- await user.click(screen.getByRole('option'));
101
- });
102
-
103
- const chip = editor.querySelector('chip[data-label="Test"]');
104
- expect(chip).toBeInTheDocument();
105
- expect(chip).toHaveTextContent('Test');
106
- });
107
- it('deletes a chip when clicking x', async () => {
108
- const user = userEvent.setup();
109
- render(
110
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
111
- );
112
-
113
- const editor = screen.getByRole('combobox');
114
- await act(async () => {
115
- await user.type(screen.getByRole('combobox'), 'Test');
116
- await user.click(screen.getByRole('option'));
117
- });
118
-
119
- const chip = editor.querySelector('chip[data-label="Test"]');
120
- expect(chip).toBeInTheDocument();
121
-
122
- const deleteHander = screen.getByTitle('Delete Test');
123
-
124
- await act(async () => {
125
- await user.click(deleteHander);
126
- });
127
- expect(chip).not.toBeInTheDocument();
128
- });
129
- it('hides dropdown with escape key', async () => {
130
- const user = userEvent.setup();
131
- render(
132
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
133
- );
134
-
135
- await act(async () => {
136
- await user.type(screen.getByRole('combobox'), 'Test');
137
- });
138
-
139
- const dropdown = screen.getByRole('listbox');
140
- expect(dropdown).toHaveTextContent(
141
- 'Add "Test" as untagged contributor',
142
- );
143
-
144
- await act(async () => {
145
- await user.type(screen.getByRole('combobox'), '{Escape}');
146
- });
147
- expect(dropdown).not.toBeVisible();
148
- });
149
- it('displays placeholder text when no content is present', async () => {
150
- const user = userEvent.setup();
151
- render(
152
- <Byline
153
- allowUntaggedContributors={true}
154
- placeholder="Placeholder"
155
- handleSave={() => {}}
156
- />,
157
- );
158
-
159
- const placeholder = await screen.findByText('Placeholder');
160
- expect(placeholder).toBeInTheDocument();
161
-
162
- await act(async () => {
163
- await user.type(screen.getByRole('combobox'), 'Test');
164
- });
165
-
166
- expect(placeholder).not.toBeVisible();
167
- });
168
- it('renders search options in dropdown', async () => {
169
- const user = userEvent.setup();
170
- render(
171
- <Byline
172
- allowUntaggedContributors={true}
173
- placeholder="Placeholder"
174
- handleSave={() => {}}
175
- searchContributors={mockSearchContributors}
176
- />,
177
- );
178
-
179
- await act(async () => {
180
- await user.click(screen.getByRole('combobox'));
181
- await user.type(screen.getByRole('combobox'), 'Ma'); // Type "Ma" to match "Mahesh Makani"
182
- });
183
-
184
- // testing internalLabel
185
- const mahesh = screen.getByText('Mahesh Makani (Software Engineer)');
186
- expect(mahesh).toBeInTheDocument();
187
-
188
- await act(async () => {
189
- await user.clear(screen.getByRole('combobox'));
190
- await user.type(screen.getByRole('combobox'), 'A');
191
- });
192
-
193
- // testing label
194
- const andrew = screen.getByText('Andrew Howe-Ely');
195
- expect(andrew).toBeInTheDocument();
196
- });
197
- it('moves the selected option in dropdown with arrow keys', async () => {
198
- const user = userEvent.setup();
199
- render(
200
- <Byline
201
- allowUntaggedContributors={true}
202
- placeholder="Placeholder"
203
- handleSave={() => {}}
204
- searchContributors={mockSearchContributors}
205
- />,
206
- );
207
-
208
- await act(async () => {
209
- await user.click(screen.getByRole('combobox'));
210
- await user.type(screen.getByRole('combobox'), 'J'); // Type "J" to match both "Jane Smith" and "John Doe"
211
- await user.type(screen.getByRole('combobox'), '{ArrowDown}');
212
- await user.type(screen.getByRole('combobox'), '{ArrowDown}');
213
- });
214
-
215
- const lastOption = screen.getAllByRole('option').pop();
216
- expect(lastOption).toHaveAttribute('aria-selected', 'true');
217
- });
218
- it('adds a chip with the Enter key', async () => {
219
- const user = userEvent.setup();
220
- render(
221
- <Byline
222
- allowUntaggedContributors={true}
223
- placeholder="Placeholder"
224
- handleSave={() => {}}
225
- searchContributors={mockSearchContributors}
226
- />,
227
- );
228
- const editor = screen.getByRole('combobox');
229
-
230
- await act(async () => {
231
- await user.click(editor);
232
- await user.type(screen.getByRole('combobox'), 'Andrew'); // Type full name to ensure it's found
233
- });
234
-
235
- // Wait for the dropdown to appear and verify the option exists
236
- const andrewOption = await screen.findByText('Andrew Howe-Ely');
237
- expect(andrewOption).toBeInTheDocument();
238
-
239
- await act(async () => {
240
- await user.type(screen.getByRole('combobox'), '{Enter}'); // Select the first option
241
- });
242
-
243
- const chip = editor.querySelector('chip[data-label="Andrew Howe-Ely"]');
244
- expect(chip).toBeInTheDocument();
245
- expect(chip).toHaveTextContent('Andrew Howe-Ely');
246
- });
247
- it('executes save function on every keypress', async () => {
248
- const user = userEvent.setup();
249
- const mockHandleSave = jest.fn();
250
- render(
251
- <Byline
252
- allowUntaggedContributors={true}
253
- placeholder="Placeholder"
254
- handleSave={mockHandleSave}
255
- searchContributors={mockSearchContributors}
256
- />,
257
- );
258
- const editor = screen.getByRole('combobox');
259
-
260
- await act(async () => {
261
- await user.type(editor, 'T');
262
- await user.type(editor, 'e');
263
- await user.type(editor, 's');
264
- await user.type(editor, 't');
265
- });
266
-
267
- expect(mockHandleSave).toHaveBeenCalledTimes(4);
268
- });
269
- it('executes save function with correct input at each keypress', async () => {
270
- const user = userEvent.setup();
271
- const saveLog: BylineModel[] = [];
272
- const mockHandleSave = (value: BylineModel) => {
273
- saveLog.push(value);
274
- };
275
- render(
276
- <Byline
277
- allowUntaggedContributors={true}
278
- placeholder="Placeholder"
279
- handleSave={mockHandleSave}
280
- searchContributors={mockSearchContributors}
281
- />,
282
- );
283
- const editor = screen.getByRole('combobox');
284
-
285
- await act(async () => {
286
- await user.type(editor, 'T');
287
- await user.type(editor, 'e');
288
- await user.type(editor, 's');
289
- await user.type(editor, 't');
290
- });
291
-
292
- expect(saveLog.at(0)?.pop()?.value).toBe('T');
293
- expect(saveLog.at(1)?.pop()?.value).toBe('Te');
294
- expect(saveLog.at(2)?.pop()?.value).toBe('Tes');
295
- expect(saveLog.at(3)?.pop()?.value).toBe('Test');
296
- });
297
- it('passes * character to search function', async () => {
298
- const user = userEvent.setup();
299
- const mockSearch = jest.fn().mockImplementation(mockSearchContributors);
300
- render(
301
- <Byline
302
- allowUntaggedContributors={true}
303
- placeholder="Placeholder"
304
- handleSave={() => {}}
305
- searchContributors={mockSearch}
306
- />,
307
- );
308
- const editor = screen.getByRole('combobox');
309
-
310
- await act(async () => {
311
- await user.type(editor, '*Test');
312
- });
313
-
314
- expect(mockSearch).toHaveBeenLastCalledWith('*Test');
315
-
316
- await act(async () => {
317
- await user.clear(editor);
318
- await user.type(editor, 'T*est');
319
- });
320
-
321
- expect(mockSearch).toHaveBeenLastCalledWith('T*est');
322
-
323
- await act(async () => {
324
- await user.clear(editor);
325
- await user.type(editor, 'Te-St*');
326
- });
327
-
328
- expect(mockSearch).toHaveBeenLastCalledWith('Te-St*');
329
- });
330
- });
331
-
332
- describe('trackTypingFromStart behavior', () => {
333
- it('starts tracking when typing from start', async () => {
334
- const user = userEvent.setup();
335
- const mockHandleSave = jest.fn();
336
-
337
- render(
338
- <Byline
339
- allowUntaggedContributors={true}
340
- handleSave={mockHandleSave}
341
- searchContributors={mockSearchContributors}
342
- initialValue={[
343
- { type: 'text', value: 'Existing content and more text' },
344
- ]}
345
- />,
346
- );
347
-
348
- const editor = screen.getByRole('combobox');
349
-
350
- // Start typing from the beginning
351
- await act(async () => {
352
- await user.click(editor);
353
- await user.keyboard('{Home}J');
354
- });
355
-
356
- expect(
357
- screen.queryByText('JExisting content and more text'),
358
- ).toBeInTheDocument();
359
-
360
- expect(screen.getByRole('listbox')).toBeVisible();
361
- expect(screen.getByText('Jane Smith (Journalist)')).toBeInTheDocument();
362
- expect(screen.getByText('John Doe')).toBeInTheDocument();
363
-
364
- await act(async () => {
365
- await user.keyboard('ohn');
366
- });
367
-
368
- expect(
369
- screen.queryByText('JohnExisting content and more text'),
370
- ).toBeInTheDocument();
371
- expect(screen.getByRole('listbox')).toBeVisible();
372
- expect(screen.getByText('John Doe')).toBeInTheDocument();
373
- expect(
374
- screen.queryByText('Jane Smith (Journalist)'),
375
- ).not.toBeInTheDocument();
376
-
377
- await act(async () => {
378
- await user.click(screen.getByText('John Doe'));
379
- });
380
-
381
- const chip = editor.querySelector('chip[data-label="John Doe"]');
382
- expect(chip).toBeInTheDocument();
383
-
384
- expect(
385
- screen.queryByText('Existing content and more text'),
386
- ).toBeInTheDocument();
387
- });
388
-
389
- it('handles backspace correctly within tracked range', async () => {
390
- const user = userEvent.setup();
391
- const mockHandleSave = jest.fn();
392
-
393
- render(
394
- <Byline
395
- allowUntaggedContributors={true}
396
- handleSave={mockHandleSave}
397
- searchContributors={mockSearchContributors}
398
- initialValue={[
399
- { type: 'text', value: 'Existing content and more text' },
400
- ]}
401
- />,
402
- );
403
-
404
- const editor = screen.getByRole('combobox');
405
-
406
- // Type some text
407
- await act(async () => {
408
- await user.click(editor);
409
- await user.keyboard('{Home}John');
410
- });
411
-
412
- expect(screen.getByRole('listbox')).toBeVisible();
413
- expect(screen.getByText('John Doe')).toBeInTheDocument();
414
-
415
- // backspace to fix a typo
416
- await act(async () => {
417
- await user.keyboard('{Backspace}'.repeat(2));
418
- });
419
-
420
- // should still be tracking (within range) and show contributors starting with "Jo"
421
- expect(screen.getByRole('listbox')).toBeVisible();
422
- expect(screen.getByText('John Doe')).toBeInTheDocument();
423
-
424
- // backspace and edit name
425
- await act(async () => {
426
- await user.keyboard('{Backspace}ane');
427
- });
428
-
429
- expect(
430
- screen.queryByText('JaneExisting content and more text'),
431
- ).toBeInTheDocument();
432
-
433
- expect(screen.getByRole('listbox')).toBeVisible();
434
- expect(
435
- screen.queryByText('Jane Smith (Journalist)'),
436
- ).toBeInTheDocument();
437
- expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
438
-
439
- await act(async () => {
440
- await user.click(screen.getByText('Jane Smith (Journalist)'));
441
- });
442
-
443
- const chip = editor.querySelector('chip[data-label="Jane Smith"]');
444
- expect(chip).toBeInTheDocument();
445
-
446
- expect(
447
- screen.queryByText('Existing content and more text'),
448
- ).toBeInTheDocument();
449
- });
450
- });