box-ui-elements 23.4.0-beta.36 → 23.4.0-beta.37

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.
Files changed (69) hide show
  1. package/dist/explorer.js +1 -1
  2. package/dist/preview.js +1 -1
  3. package/dist/sidebar.js +1 -1
  4. package/es/elements/content-preview/PreviewNavigation.js +0 -2
  5. package/es/elements/content-preview/PreviewNavigation.js.flow +0 -2
  6. package/es/elements/content-preview/PreviewNavigation.js.map +1 -1
  7. package/es/elements/content-sidebar/versions/VersionsSidebarContainer.js +29 -7
  8. package/es/elements/content-sidebar/versions/VersionsSidebarContainer.js.flow +44 -5
  9. package/es/elements/content-sidebar/versions/VersionsSidebarContainer.js.map +1 -1
  10. package/es/elements/content-sidebar/withSidebarAnnotations.js +141 -35
  11. package/es/elements/content-sidebar/withSidebarAnnotations.js.flow +199 -37
  12. package/es/elements/content-sidebar/withSidebarAnnotations.js.map +1 -1
  13. package/i18n/bn-IN.js +1 -1
  14. package/i18n/bn-IN.properties +4 -0
  15. package/i18n/da-DK.js +1 -1
  16. package/i18n/da-DK.properties +4 -0
  17. package/i18n/de-DE.js +1 -1
  18. package/i18n/de-DE.properties +4 -0
  19. package/i18n/en-AU.js +1 -1
  20. package/i18n/en-AU.properties +4 -0
  21. package/i18n/en-CA.js +1 -1
  22. package/i18n/en-CA.properties +4 -0
  23. package/i18n/en-GB.js +1 -1
  24. package/i18n/en-GB.properties +4 -0
  25. package/i18n/es-419.js +1 -1
  26. package/i18n/es-419.properties +4 -0
  27. package/i18n/es-ES.js +1 -1
  28. package/i18n/es-ES.properties +4 -0
  29. package/i18n/fi-FI.js +1 -1
  30. package/i18n/fi-FI.properties +4 -0
  31. package/i18n/fr-CA.js +1 -1
  32. package/i18n/fr-CA.properties +4 -0
  33. package/i18n/fr-FR.js +1 -1
  34. package/i18n/fr-FR.properties +4 -0
  35. package/i18n/hi-IN.js +1 -1
  36. package/i18n/hi-IN.properties +4 -0
  37. package/i18n/it-IT.js +1 -1
  38. package/i18n/it-IT.properties +4 -0
  39. package/i18n/ja-JP.js +1 -1
  40. package/i18n/ja-JP.properties +4 -0
  41. package/i18n/ko-KR.js +1 -1
  42. package/i18n/ko-KR.properties +4 -0
  43. package/i18n/nb-NO.js +1 -1
  44. package/i18n/nb-NO.properties +4 -0
  45. package/i18n/nl-NL.js +1 -1
  46. package/i18n/nl-NL.properties +4 -0
  47. package/i18n/pl-PL.js +1 -1
  48. package/i18n/pl-PL.properties +4 -0
  49. package/i18n/pt-BR.js +1 -1
  50. package/i18n/pt-BR.properties +4 -0
  51. package/i18n/ru-RU.js +1 -1
  52. package/i18n/ru-RU.properties +4 -0
  53. package/i18n/sv-SE.js +1 -1
  54. package/i18n/sv-SE.properties +4 -0
  55. package/i18n/tr-TR.js +1 -1
  56. package/i18n/tr-TR.properties +4 -0
  57. package/i18n/zh-CN.js +1 -1
  58. package/i18n/zh-CN.properties +4 -0
  59. package/i18n/zh-TW.js +1 -1
  60. package/i18n/zh-TW.properties +4 -0
  61. package/package.json +1 -1
  62. package/src/elements/content-preview/PreviewNavigation.js +0 -2
  63. package/src/elements/content-preview/__tests__/PreviewNavigation.test.js +12 -12
  64. package/src/elements/content-sidebar/__tests__/withSidebarAnnotations.rtl.test.js +1152 -0
  65. package/src/elements/content-sidebar/versions/VersionsSidebarContainer.js +44 -5
  66. package/src/elements/content-sidebar/versions/__tests__/VersionsSidebarContainer.test.js +200 -43
  67. package/src/elements/content-sidebar/versions/__tests__/__snapshots__/VersionsSidebarContainer.test.js.snap +2 -2
  68. package/src/elements/content-sidebar/withSidebarAnnotations.js +199 -37
  69. package/src/elements/content-sidebar/__tests__/withSidebarAnnotations.test.js +0 -626
@@ -0,0 +1,1152 @@
1
+ import * as React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import { render } from '../../../test-utils/testing-library';
4
+ import withSidebarAnnotations from '../withSidebarAnnotations';
5
+ import { Action } from '../../common/annotator-context/types';
6
+ import { FEED_ITEM_TYPE_VERSION } from '../../../constants';
7
+
8
+ describe('elements/content-sidebar/withSidebarAnnotations', () => {
9
+ const TestComponent = React.forwardRef((props, ref) => <div data-testid="test-component" ref={ref} />);
10
+ const WrappedComponent = withSidebarAnnotations(TestComponent);
11
+
12
+ const currentUser = {
13
+ id: 'foo',
14
+ };
15
+
16
+ const file = {
17
+ id: 'id',
18
+ file_version: {
19
+ id: '123',
20
+ },
21
+ };
22
+
23
+ const feedAPI = {
24
+ addAnnotation: jest.fn(),
25
+ addPendingReply: jest.fn(),
26
+ feedItems: jest.fn(),
27
+ getCachedItems: jest.fn(),
28
+ deleteAnnotation: jest.fn(),
29
+ deleteFeedItem: jest.fn(),
30
+ deleteReplyItem: jest.fn(),
31
+ modifyFeedItemRepliesCountBy: jest.fn(),
32
+ updateFeedItem: jest.fn(),
33
+ updateReplyItem: jest.fn(),
34
+ };
35
+
36
+ const api = {
37
+ getFeedAPI: () => feedAPI,
38
+ };
39
+
40
+ const getAnnotationsPath = jest.fn();
41
+ const getAnnotationsMatchPath = jest.fn();
42
+
43
+ const annotatorContextProps = {
44
+ getAnnotationsMatchPath,
45
+ getAnnotationsPath,
46
+ };
47
+
48
+ const history = {
49
+ push: jest.fn(),
50
+ replace: jest.fn(),
51
+ };
52
+
53
+ const onVersionChange = jest.fn();
54
+
55
+ const internalSidebarNavigationHandler = jest.fn();
56
+
57
+ const defaultProps = {
58
+ api,
59
+ ...annotatorContextProps,
60
+ file,
61
+ history,
62
+ onVersionChange,
63
+ location: { pathname: '/activity' },
64
+ };
65
+
66
+ const createComponentElement = (props = {}) => {
67
+ const componentProps = {
68
+ ...defaultProps,
69
+ ...props,
70
+ };
71
+
72
+ return (
73
+ <MemoryRouter initialEntries={['/activity']}>
74
+ <WrappedComponent {...componentProps} />
75
+ </MemoryRouter>
76
+ );
77
+ };
78
+
79
+ const createRouterDisabledComponentElement = (props = {}) => {
80
+ const routerDisabledProps = {
81
+ routerDisabled: true,
82
+ internalSidebarNavigationHandler,
83
+ internalSidebarNavigation: { sidebar: 'activity' },
84
+ ...defaultProps,
85
+ ...props,
86
+ };
87
+
88
+ return <WrappedComponent {...routerDisabledProps} />;
89
+ };
90
+
91
+ const renderWithSidebarAnnotations = (props = {}) => {
92
+ return render(createComponentElement(props));
93
+ };
94
+
95
+ const renderWithSidebarAnnotationsRouterDisabled = (props = {}) => {
96
+ return render(createRouterDisabledComponentElement(props));
97
+ };
98
+
99
+ beforeEach(() => {
100
+ jest.resetAllMocks();
101
+ });
102
+
103
+ describe('constructor', () => {
104
+ test('should call redirectDeeplinkedAnnotation (use side effect to test it)', () => {
105
+ renderWithSidebarAnnotations();
106
+
107
+ expect(getAnnotationsMatchPath).toHaveBeenCalledTimes(1);
108
+ });
109
+
110
+ test.each`
111
+ fileVersionId | annotationId | expectedCallCount
112
+ ${undefined} | ${'987'} | ${0}
113
+ ${'123'} | ${'987'} | ${0}
114
+ ${'124'} | ${'987'} | ${1}
115
+ ${'124'} | ${undefined} | ${1}
116
+ `(
117
+ 'should call history.replace appropriately if router location annotationId=$annotationId and fileVersionId=$fileVersionId',
118
+ ({ annotationId, fileVersionId, expectedCallCount }) => {
119
+ // Setup mocks before rendering (constructor will run during render)
120
+ getAnnotationsMatchPath.mockReturnValue({ params: { annotationId, fileVersionId } });
121
+
122
+ renderWithSidebarAnnotations();
123
+
124
+ expect(history.replace).toHaveBeenCalledTimes(expectedCallCount);
125
+ },
126
+ );
127
+
128
+ test.each`
129
+ fileVersionId | annotationId | expectedPath
130
+ ${'124'} | ${'987'} | ${'/activity/annotations/123/987'}
131
+ ${'124'} | ${undefined} | ${'/activity/annotations/123'}
132
+ `('should call history.replace with $expectedPath', ({ fileVersionId, annotationId, expectedPath }) => {
133
+ getAnnotationsMatchPath.mockReturnValue({ params: { annotationId, fileVersionId } });
134
+ getAnnotationsPath.mockReturnValue(expectedPath);
135
+
136
+ renderWithSidebarAnnotations();
137
+
138
+ expect(history.replace).toHaveBeenCalledWith(expectedPath);
139
+ });
140
+
141
+ describe('constructor - Router Disabled', () => {
142
+ test('should call redirectDeeplinkedAnnotation when router disabled', () => {
143
+ // In router-disabled mode, getAnnotationsMatchPath is NOT called
144
+ // Only getInternalNavigationMatch is used internally
145
+ renderWithSidebarAnnotationsRouterDisabled();
146
+
147
+ expect(getAnnotationsMatchPath).toHaveBeenCalledTimes(0);
148
+ });
149
+
150
+ test.each`
151
+ fileVersionId | annotationId | expectedCallCount
152
+ ${undefined} | ${'987'} | ${0}
153
+ ${'123'} | ${'987'} | ${0}
154
+ ${'124'} | ${'987'} | ${1}
155
+ ${'124'} | ${undefined} | ${1}
156
+ `(
157
+ 'should call internalSidebarNavigationHandler appropriately if internal navigation annotationId=$annotationId and fileVersionId=$fileVersionId',
158
+ ({ annotationId, fileVersionId, expectedCallCount }) => {
159
+ // Only provide fileVersionId and annotationId if they should trigger navigation
160
+ const internalNavigation = fileVersionId
161
+ ? {
162
+ sidebar: 'activity',
163
+ activeFeedEntryType: 'annotations',
164
+ activeFeedEntryId: annotationId,
165
+ fileVersionId,
166
+ }
167
+ : { sidebar: 'activity' };
168
+
169
+ renderWithSidebarAnnotationsRouterDisabled({
170
+ internalSidebarNavigation: internalNavigation,
171
+ });
172
+
173
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledTimes(expectedCallCount);
174
+ },
175
+ );
176
+
177
+ test.each`
178
+ fileVersionId | annotationId | expectedNavigation
179
+ ${'124'} | ${'987'} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: '987', fileVersionId: '123' }}
180
+ ${'124'} | ${undefined} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: undefined, fileVersionId: '123' }}
181
+ `(
182
+ 'should call internalSidebarNavigationHandler with correct navigation for fileVersionId=$fileVersionId and annotationId=$annotationId',
183
+ ({ fileVersionId, annotationId, expectedNavigation }) => {
184
+ const internalNavigation = {
185
+ sidebar: 'activity',
186
+ activeFeedEntryType: 'annotations',
187
+ activeFeedEntryId: annotationId,
188
+ fileVersionId,
189
+ };
190
+
191
+ renderWithSidebarAnnotationsRouterDisabled({
192
+ internalSidebarNavigation: internalNavigation,
193
+ });
194
+
195
+ // The second parameter is `true` indicating this is a redirect/correction
196
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledWith(expectedNavigation, true);
197
+ },
198
+ );
199
+ });
200
+ });
201
+
202
+ describe('componentDidUpdate', () => {
203
+ test.each`
204
+ fileId | expectedCount
205
+ ${'123'} | ${0}
206
+ ${'456'} | ${1}
207
+ `('should call onVersionChange appropriately if file id changes to $fileId', ({ fileId, expectedCount }) => {
208
+ const { rerender } = renderWithSidebarAnnotations({
209
+ fileId: '123',
210
+ });
211
+
212
+ jest.clearAllMocks();
213
+
214
+ // Trigger componentDidUpdate by changing the fileId
215
+ rerender(
216
+ createComponentElement({
217
+ fileId,
218
+ }),
219
+ );
220
+
221
+ expect(onVersionChange).toHaveBeenCalledTimes(expectedCount);
222
+
223
+ if (expectedCount > 0) {
224
+ expect(onVersionChange).toHaveBeenCalledWith(null);
225
+ }
226
+ });
227
+ });
228
+
229
+ describe('refreshActivitySidebar', () => {
230
+ test.each`
231
+ pathname | isOpen | hasRefCurrent | expectedCount
232
+ ${'/'} | ${false} | ${false} | ${0}
233
+ ${'/details'} | ${true} | ${false} | ${0}
234
+ ${'/activity'} | ${false} | ${false} | ${0}
235
+ ${'/activity'} | ${true} | ${false} | ${0}
236
+ ${'/activity'} | ${false} | ${true} | ${0}
237
+ ${'/activity'} | ${true} | ${true} | ${1}
238
+ ${'/activity/versions/12345'} | ${true} | ${true} | ${1}
239
+ ${'/activity/versions/12345/67890'} | ${true} | ${true} | ${1}
240
+ ${'/details'} | ${true} | ${true} | ${0}
241
+ ${'/'} | ${true} | ${true} | ${0}
242
+ `(
243
+ 'should refresh the sidebarPanels ref accordingly if pathname=$pathname, isOpen=$isOpen, hasRefCurrent=$hasRefCurrent',
244
+ ({ pathname, isOpen, hasRefCurrent, expectedCount }) => {
245
+ const mockRefresh = jest.fn();
246
+ const annotationUpdate = {
247
+ id: '123',
248
+ description: {
249
+ message: 'text',
250
+ },
251
+ };
252
+ const annotatorStateMock = {
253
+ action: Action.UPDATE_END,
254
+ annotation: annotationUpdate,
255
+ };
256
+
257
+ // Create a custom test component that exposes the ref
258
+ const TestComponentWithRef = React.forwardRef((props, ref) => {
259
+ React.useImperativeHandle(ref, () => (hasRefCurrent ? { refresh: mockRefresh } : null));
260
+ return <div data-testid="test-component" />;
261
+ });
262
+
263
+ const WrappedComponentWithRef = withSidebarAnnotations(TestComponentWithRef);
264
+
265
+ const { rerender } = render(
266
+ <MemoryRouter initialEntries={[pathname]}>
267
+ <WrappedComponentWithRef
268
+ {...defaultProps}
269
+ annotatorState={{ annotation: {}, action: Action.CREATE_START }}
270
+ currentUser={currentUser}
271
+ isOpen={isOpen}
272
+ location={{ pathname }}
273
+ />
274
+ </MemoryRouter>,
275
+ );
276
+
277
+ jest.clearAllMocks();
278
+
279
+ // Trigger updateAnnotation by changing the annotatorState
280
+ rerender(
281
+ <MemoryRouter initialEntries={[pathname]}>
282
+ <WrappedComponentWithRef
283
+ {...defaultProps}
284
+ annotatorState={annotatorStateMock}
285
+ currentUser={currentUser}
286
+ isOpen={isOpen}
287
+ location={{ pathname }}
288
+ />
289
+ </MemoryRouter>,
290
+ );
291
+
292
+ expect(mockRefresh).toHaveBeenCalledTimes(expectedCount);
293
+ if (expectedCount > 0) {
294
+ expect(mockRefresh).toHaveBeenCalledWith(false);
295
+ }
296
+ },
297
+ );
298
+
299
+ describe('refreshActivitySidebar - Router Disabled', () => {
300
+ test.each`
301
+ navigation | isOpen | hasRefCurrent | expectedCount
302
+ ${{ sidebar: 'details' }} | ${false} | ${false} | ${0}
303
+ ${{ sidebar: 'details' }} | ${true} | ${false} | ${0}
304
+ ${{ sidebar: 'activity' }} | ${false} | ${false} | ${0}
305
+ ${{ sidebar: 'activity' }} | ${true} | ${false} | ${0}
306
+ ${{ sidebar: 'activity' }} | ${false} | ${true} | ${0}
307
+ ${{ sidebar: 'activity' }} | ${true} | ${true} | ${1}
308
+ ${{ sidebar: 'activity', versionId: '12345' }} | ${true} | ${true} | ${1}
309
+ ${{ sidebar: 'activity', versionId: '12345', annotationId: '67890' }} | ${true} | ${true} | ${1}
310
+ ${{ sidebar: 'details' }} | ${true} | ${true} | ${0}
311
+ ${{ sidebar: 'metadata' }} | ${true} | ${true} | ${0}
312
+ `(
313
+ 'should refresh the sidebarPanels ref accordingly if navigation=$navigation, isOpen=$isOpen, hasRefCurrent=$hasRefCurrent',
314
+ ({ navigation, isOpen, hasRefCurrent, expectedCount }) => {
315
+ const mockRefresh = jest.fn();
316
+ const annotationUpdate = {
317
+ id: '123',
318
+ description: {
319
+ message: 'text',
320
+ },
321
+ };
322
+ const annotatorStateMock = {
323
+ action: Action.UPDATE_END,
324
+ annotation: annotationUpdate,
325
+ };
326
+
327
+ // Create a custom test component that exposes the ref
328
+ const TestComponentWithRef = React.forwardRef((props, ref) => {
329
+ React.useImperativeHandle(ref, () => (hasRefCurrent ? { refresh: mockRefresh } : null));
330
+ return <div data-testid="test-component" />;
331
+ });
332
+
333
+ const WrappedComponentWithRef = withSidebarAnnotations(TestComponentWithRef);
334
+
335
+ const { rerender } = render(
336
+ <WrappedComponentWithRef
337
+ {...defaultProps}
338
+ routerDisabled
339
+ internalSidebarNavigationHandler={internalSidebarNavigationHandler}
340
+ internalSidebarNavigation={navigation}
341
+ annotatorState={{ annotation: {}, action: Action.CREATE_START }}
342
+ currentUser={currentUser}
343
+ isOpen={isOpen}
344
+ />,
345
+ );
346
+
347
+ jest.clearAllMocks();
348
+
349
+ // Trigger updateAnnotation by changing the annotatorState
350
+ rerender(
351
+ <WrappedComponentWithRef
352
+ {...defaultProps}
353
+ routerDisabled
354
+ internalSidebarNavigationHandler={internalSidebarNavigationHandler}
355
+ internalSidebarNavigation={navigation}
356
+ annotatorState={annotatorStateMock}
357
+ currentUser={currentUser}
358
+ isOpen={isOpen}
359
+ />,
360
+ );
361
+
362
+ expect(mockRefresh).toHaveBeenCalledTimes(expectedCount);
363
+ if (expectedCount > 0) {
364
+ expect(mockRefresh).toHaveBeenCalledWith(false);
365
+ }
366
+ },
367
+ );
368
+ });
369
+ });
370
+
371
+ describe('addAnnotation', () => {
372
+ test('should throw if no user', () => {
373
+ const annotatorStateMock = {
374
+ annotation: {},
375
+ action: Action.CREATE_END,
376
+ meta: { requestId: '123' },
377
+ };
378
+
379
+ expect(() => {
380
+ const { rerender } = renderWithSidebarAnnotations({
381
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
382
+ });
383
+
384
+ rerender(
385
+ createComponentElement({
386
+ annotatorState: annotatorStateMock,
387
+ }),
388
+ );
389
+ }).toThrow('Bad box user!');
390
+ });
391
+
392
+ test('should do nothing if meta or requestId is not present', () => {
393
+ const annotatorStateMock = {
394
+ annotation: {},
395
+ action: Action.CREATE_END,
396
+ };
397
+
398
+ const { rerender } = renderWithSidebarAnnotations({
399
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
400
+ currentUser,
401
+ });
402
+
403
+ jest.clearAllMocks();
404
+
405
+ rerender(
406
+ createComponentElement({
407
+ annotatorState: annotatorStateMock,
408
+ currentUser,
409
+ }),
410
+ );
411
+
412
+ expect(feedAPI.addAnnotation).not.toHaveBeenCalled();
413
+ });
414
+
415
+ test.each`
416
+ hasItems | expectedAddCount
417
+ ${undefined} | ${0}
418
+ ${[]} | ${1}
419
+ `(
420
+ 'should add the annotation to the feed cache accordingly if the cache items is $hasItems',
421
+ ({ hasItems, expectedAddCount }) => {
422
+ const annotatorStateMock = {
423
+ annotation: {},
424
+ action: Action.CREATE_END,
425
+ meta: { requestId: '123' },
426
+ };
427
+
428
+ feedAPI.getCachedItems.mockReturnValue({ items: hasItems });
429
+
430
+ const { rerender } = renderWithSidebarAnnotations({
431
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
432
+ currentUser,
433
+ });
434
+
435
+ jest.clearAllMocks();
436
+ feedAPI.getCachedItems.mockReturnValue({ items: hasItems });
437
+
438
+ rerender(
439
+ createComponentElement({
440
+ annotatorState: annotatorStateMock,
441
+ currentUser,
442
+ }),
443
+ );
444
+
445
+ expect(feedAPI.addAnnotation).toHaveBeenCalledTimes(expectedAddCount);
446
+
447
+ if (expectedAddCount > 0) {
448
+ expect(feedAPI.addAnnotation).toHaveBeenCalledWith(
449
+ file,
450
+ currentUser,
451
+ {},
452
+ '123',
453
+ false, // isPending = false for 'create_end' action
454
+ );
455
+ }
456
+ },
457
+ );
458
+
459
+ test.each`
460
+ annotation | expectedCount
461
+ ${{}} | ${1}
462
+ ${undefined} | ${0}
463
+ ${null} | ${0}
464
+ `(
465
+ 'should call addAnnotation $expectedCount times if annotation changed to $annotation',
466
+ ({ annotation, expectedCount }) => {
467
+ const annotatorStateMock = {
468
+ annotation,
469
+ action: Action.CREATE_END,
470
+ meta: { requestId: '123' },
471
+ };
472
+
473
+ feedAPI.getCachedItems.mockReturnValue({ items: [] });
474
+
475
+ const { rerender } = renderWithSidebarAnnotations({
476
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
477
+ currentUser,
478
+ });
479
+
480
+ jest.clearAllMocks();
481
+ feedAPI.getCachedItems.mockReturnValue({ items: [] });
482
+
483
+ // Trigger componentDidUpdate by changing the annotatorState
484
+ rerender(
485
+ createComponentElement({
486
+ annotatorState: annotatorStateMock,
487
+ currentUser,
488
+ }),
489
+ );
490
+
491
+ // Verify the side effect of addAnnotation being called
492
+ expect(feedAPI.addAnnotation).toHaveBeenCalledTimes(expectedCount);
493
+
494
+ if (expectedCount > 0) {
495
+ expect(feedAPI.addAnnotation).toHaveBeenCalledWith(
496
+ file,
497
+ currentUser,
498
+ annotation,
499
+ '123',
500
+ false, // isPending = false for 'create_end' action
501
+ );
502
+ }
503
+ },
504
+ );
505
+ });
506
+
507
+ describe('addAnnotationReply', () => {
508
+ test.each`
509
+ action
510
+ ${Action.REPLY_CREATE_START}
511
+ ${Action.REPLY_CREATE_END}
512
+ `('should call addAnnotationReply if given action = $action', ({ action }) => {
513
+ const annotation = { id: '123' };
514
+ const annotationReply = { id: '456', tagged_message: 'abc' };
515
+ const requestId = 'comment_456';
516
+ const annotatorStateMock = {
517
+ action,
518
+ annotation,
519
+ annotationReply,
520
+ meta: { requestId },
521
+ };
522
+
523
+ feedAPI.getCachedItems.mockReturnValue({
524
+ items: [
525
+ {
526
+ id: '123',
527
+ replies: [annotationReply],
528
+ total_reply_count: 2,
529
+ },
530
+ ],
531
+ });
532
+
533
+ const { rerender } = renderWithSidebarAnnotations({
534
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
535
+ currentUser,
536
+ });
537
+
538
+ jest.clearAllMocks();
539
+
540
+ rerender(
541
+ createComponentElement({
542
+ annotatorState: annotatorStateMock,
543
+ currentUser,
544
+ }),
545
+ );
546
+
547
+ if (action === Action.REPLY_CREATE_START) {
548
+ expect(feedAPI.addPendingReply).toHaveBeenCalledTimes(1);
549
+ expect(feedAPI.addPendingReply).toHaveBeenCalledWith(annotation.id, currentUser, {
550
+ ...annotationReply,
551
+ id: requestId,
552
+ });
553
+ expect(feedAPI.modifyFeedItemRepliesCountBy).not.toHaveBeenCalled();
554
+ expect(feedAPI.updateReplyItem).not.toHaveBeenCalled();
555
+ } else {
556
+ expect(feedAPI.modifyFeedItemRepliesCountBy).toHaveBeenCalledTimes(1);
557
+ expect(feedAPI.modifyFeedItemRepliesCountBy).toHaveBeenCalledWith(annotation.id, 1);
558
+ expect(feedAPI.updateReplyItem).toHaveBeenCalledTimes(1);
559
+ expect(feedAPI.updateReplyItem).toHaveBeenCalledWith(
560
+ { ...annotationReply, isPending: false },
561
+ annotation.id,
562
+ requestId,
563
+ );
564
+ expect(feedAPI.addPendingReply).not.toHaveBeenCalled();
565
+ }
566
+ });
567
+ });
568
+
569
+ describe('deleteAnnotation', () => {
570
+ test.each`
571
+ action
572
+ ${Action.DELETE_START}
573
+ ${Action.DELETE_END}
574
+ `('should call deleteAnnotation if given action = $action', ({ action }) => {
575
+ const annotation = { id: '123' };
576
+ const annotatorStateMock = {
577
+ annotation,
578
+ action,
579
+ };
580
+
581
+ const { rerender } = renderWithSidebarAnnotations({
582
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
583
+ });
584
+
585
+ jest.clearAllMocks();
586
+
587
+ rerender(
588
+ createComponentElement({
589
+ annotatorState: annotatorStateMock,
590
+ }),
591
+ );
592
+
593
+ if (action === Action.DELETE_START) {
594
+ expect(feedAPI.updateFeedItem).toHaveBeenCalledTimes(1);
595
+ expect(feedAPI.updateFeedItem).toHaveBeenCalledWith({ isPending: true }, annotation.id);
596
+ expect(feedAPI.deleteFeedItem).not.toHaveBeenCalled();
597
+ } else {
598
+ expect(feedAPI.deleteFeedItem).toHaveBeenCalledTimes(1);
599
+ expect(feedAPI.deleteFeedItem).toHaveBeenCalledWith(annotation.id);
600
+ expect(feedAPI.updateFeedItem).not.toHaveBeenCalled();
601
+ }
602
+ });
603
+ });
604
+
605
+ describe('deleteAnnotationReply', () => {
606
+ test.each`
607
+ action
608
+ ${Action.REPLY_DELETE_START}
609
+ ${Action.REPLY_DELETE_END}
610
+ `('should call deleteAnnotationReply if given action = $action', ({ action }) => {
611
+ const annotation = { id: '123' };
612
+ const annotationReply = { id: '456' };
613
+ const annotatorStateMock = {
614
+ action,
615
+ annotation,
616
+ annotationReply,
617
+ };
618
+
619
+ feedAPI.getCachedItems.mockReturnValue({
620
+ items: [
621
+ {
622
+ id: '123',
623
+ replies: [{ id: '456', tagged_message: 'abc' }],
624
+ total_reply_count: 2,
625
+ },
626
+ ],
627
+ });
628
+
629
+ const { rerender } = renderWithSidebarAnnotations({
630
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
631
+ });
632
+
633
+ jest.clearAllMocks();
634
+
635
+ rerender(
636
+ createComponentElement({
637
+ annotatorState: annotatorStateMock,
638
+ }),
639
+ );
640
+
641
+ if (action === Action.REPLY_DELETE_START) {
642
+ expect(feedAPI.updateReplyItem).toHaveBeenCalledTimes(1);
643
+ expect(feedAPI.updateReplyItem).toHaveBeenCalledWith(
644
+ { isPending: true },
645
+ annotation.id,
646
+ annotationReply.id,
647
+ );
648
+ expect(feedAPI.deleteReplyItem).not.toHaveBeenCalled();
649
+ expect(feedAPI.modifyFeedItemRepliesCountBy).not.toHaveBeenCalled();
650
+ } else {
651
+ expect(feedAPI.deleteReplyItem).toHaveBeenCalledTimes(1);
652
+ expect(feedAPI.deleteReplyItem).toHaveBeenCalledWith(annotationReply.id, annotation.id);
653
+ expect(feedAPI.modifyFeedItemRepliesCountBy).toHaveBeenCalledTimes(1);
654
+ expect(feedAPI.modifyFeedItemRepliesCountBy).toHaveBeenCalledWith(annotation.id, -1);
655
+ expect(feedAPI.updateReplyItem).not.toHaveBeenCalled();
656
+ }
657
+ });
658
+
659
+ test('should update appropriate annotation if reply is currently not in the feed given action = reply_delete_end', () => {
660
+ const annotation = { id: '123' };
661
+ const annotationReply = { id: '456' };
662
+ const annotatorStateMock = {
663
+ action: Action.REPLY_DELETE_END,
664
+ annotation,
665
+ annotationReply,
666
+ };
667
+
668
+ // Mock setup: reply with id '456' is NOT in the cached replies (only '999' is present)
669
+ feedAPI.getCachedItems.mockReturnValue({
670
+ items: [
671
+ {
672
+ id: '123',
673
+ replies: [{ id: '999', tagged_message: 'abc' }],
674
+ total_reply_count: 2,
675
+ },
676
+ ],
677
+ });
678
+
679
+ const { rerender } = renderWithSidebarAnnotations({
680
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
681
+ });
682
+
683
+ jest.clearAllMocks();
684
+
685
+ rerender(
686
+ createComponentElement({
687
+ annotatorState: annotatorStateMock,
688
+ }),
689
+ );
690
+
691
+ // Since reply is not in cached items, only modifyFeedItemRepliesCountBy should be called
692
+ expect(feedAPI.modifyFeedItemRepliesCountBy).toHaveBeenCalledTimes(1);
693
+ expect(feedAPI.modifyFeedItemRepliesCountBy).toHaveBeenCalledWith(annotation.id, -1);
694
+ expect(feedAPI.deleteReplyItem).not.toHaveBeenCalled();
695
+ expect(feedAPI.updateReplyItem).not.toHaveBeenCalled();
696
+ });
697
+ });
698
+
699
+ describe('updateAnnotation', () => {
700
+ test.each`
701
+ action | expectedIsPending
702
+ ${Action.UPDATE_START} | ${true}
703
+ ${Action.UPDATE_END} | ${false}
704
+ `('should call updateAnnotation if given action = $action', ({ action, expectedIsPending }) => {
705
+ const annotation = { id: '123', status: 'resolved' };
706
+ const annotatorStateMock = {
707
+ annotation,
708
+ action,
709
+ };
710
+
711
+ const { rerender } = renderWithSidebarAnnotations({
712
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
713
+ });
714
+
715
+ jest.clearAllMocks();
716
+
717
+ rerender(
718
+ createComponentElement({
719
+ annotatorState: annotatorStateMock,
720
+ }),
721
+ );
722
+
723
+ const expectedAnnotationData = { ...annotation, isPending: expectedIsPending };
724
+
725
+ expect(feedAPI.updateFeedItem).toHaveBeenCalledTimes(1);
726
+ expect(feedAPI.updateFeedItem).toHaveBeenCalledWith(expectedAnnotationData, annotation.id);
727
+ });
728
+ });
729
+
730
+ describe('updateAnnotationReply', () => {
731
+ test.each`
732
+ action | expectedIsPending
733
+ ${Action.REPLY_UPDATE_START} | ${true}
734
+ ${Action.REPLY_UPDATE_END} | ${false}
735
+ `('should call updateAnnotationReply if given action = $action', ({ action, expectedIsPending }) => {
736
+ const annotation = { id: '123' };
737
+ const annotationReply = { id: '456', tagged_message: 'abc' };
738
+ const annotatorStateMock = {
739
+ action,
740
+ annotation,
741
+ annotationReply,
742
+ };
743
+
744
+ const { rerender } = renderWithSidebarAnnotations({
745
+ annotatorState: { annotation: {}, action: Action.CREATE_START },
746
+ });
747
+
748
+ jest.clearAllMocks();
749
+
750
+ rerender(
751
+ createComponentElement({
752
+ annotatorState: annotatorStateMock,
753
+ }),
754
+ );
755
+
756
+ const expectedReplyData = { ...annotationReply, isPending: expectedIsPending };
757
+
758
+ expect(feedAPI.updateReplyItem).toHaveBeenCalledTimes(1);
759
+ expect(feedAPI.updateReplyItem).toHaveBeenCalledWith(expectedReplyData, annotation.id, annotationReply.id);
760
+ });
761
+ });
762
+
763
+ describe('updateActiveAnnotation', () => {
764
+ test.each`
765
+ condition | prevActiveAnnotationId | activeAnnotationId | isAnnotationsPath | expectedHistoryPushCalls
766
+ ${'annotation ids are the same'} | ${'123'} | ${'123'} | ${true} | ${0}
767
+ ${'annotation ids are different'} | ${'123'} | ${'234'} | ${true} | ${1}
768
+ ${'annotation deselected on annotations path'} | ${'123'} | ${null} | ${true} | ${1}
769
+ ${'annotation deselected not on annotations path'} | ${'123'} | ${null} | ${false} | ${0}
770
+ ${'annotation selected not on annotations path'} | ${null} | ${'123'} | ${false} | ${1}
771
+ ${'annotation selected on annotations path'} | ${null} | ${'123'} | ${true} | ${1}
772
+ `(
773
+ 'should call updateActiveAnnotation appropriately if $condition',
774
+ ({ prevActiveAnnotationId, activeAnnotationId, isAnnotationsPath, expectedHistoryPushCalls }) => {
775
+ const location = { pathname: '/activity', state: { foo: 'bar' } };
776
+
777
+ getAnnotationsPath.mockReturnValue('/activity/annotations/456/123');
778
+ getAnnotationsMatchPath.mockReturnValue(
779
+ isAnnotationsPath ? { params: { fileVersionId: '456' } } : null,
780
+ );
781
+
782
+ const { rerender } = renderWithSidebarAnnotations({
783
+ annotatorState: { activeAnnotationId: prevActiveAnnotationId },
784
+ location,
785
+ });
786
+
787
+ jest.clearAllMocks();
788
+
789
+ rerender(
790
+ createComponentElement({
791
+ annotatorState: { activeAnnotationId, action: 'set_active' },
792
+ location,
793
+ }),
794
+ );
795
+
796
+ expect(history.push).toHaveBeenCalledTimes(expectedHistoryPushCalls);
797
+ },
798
+ );
799
+
800
+ test.each`
801
+ activeAnnotationId | fileVersionId | location | expectedPath | expectedState
802
+ ${'234'} | ${'456'} | ${{ pathname: '/' }} | ${'/activity/annotations/456/234'} | ${{ open: true }}
803
+ ${'234'} | ${undefined} | ${{ pathname: '/' }} | ${'/activity/annotations/123/234'} | ${{ open: true }}
804
+ ${null} | ${'456'} | ${{ pathname: '/' }} | ${'/activity/annotations/456'} | ${undefined}
805
+ ${null} | ${'456'} | ${{ pathname: '/', state: { foo: 'bar' } }} | ${'/activity/annotations/456'} | ${{ foo: 'bar' }}
806
+ ${'234'} | ${'456'} | ${{ pathname: '/', state: { foo: 'bar' } }} | ${'/activity/annotations/456/234'} | ${{ open: true }}
807
+ `(
808
+ 'should set location path based on match param fileVersionId=$fileVersionId and activeAnnotationId=$activeAnnotationId',
809
+ ({ activeAnnotationId, fileVersionId, location, expectedPath, expectedState }) => {
810
+ getAnnotationsMatchPath.mockReturnValue({ params: { fileVersionId } });
811
+ getAnnotationsPath.mockReturnValue(expectedPath);
812
+
813
+ const { rerender } = renderWithSidebarAnnotations({
814
+ annotatorState: { activeAnnotationId: 'initial' },
815
+ location,
816
+ });
817
+
818
+ jest.clearAllMocks();
819
+
820
+ rerender(
821
+ createComponentElement({
822
+ annotatorState: { activeAnnotationId, action: 'set_active' },
823
+ location,
824
+ }),
825
+ );
826
+
827
+ expect(history.push).toHaveBeenCalledTimes(1);
828
+ expect(history.push).toHaveBeenCalledWith({ pathname: expectedPath, state: expectedState });
829
+ },
830
+ );
831
+
832
+ test('should use the provided fileVersionId in the annotatorState if provided', () => {
833
+ const { rerender } = renderWithSidebarAnnotations({
834
+ annotatorState: { activeAnnotationId: 'initial' },
835
+ location: { pathname: '/' },
836
+ });
837
+
838
+ jest.clearAllMocks();
839
+
840
+ rerender(
841
+ createComponentElement({
842
+ annotatorState: {
843
+ activeAnnotationFileVersionId: '456',
844
+ activeAnnotationId: '123',
845
+ action: 'set_active',
846
+ },
847
+ location: { pathname: '/' },
848
+ }),
849
+ );
850
+
851
+ expect(getAnnotationsPath).toHaveBeenCalledWith('456', '123');
852
+ });
853
+
854
+ test('should fall back to the fileVersionId in the file if none other is provided', () => {
855
+ const { rerender } = renderWithSidebarAnnotations({
856
+ annotatorState: { activeAnnotationId: null },
857
+ location: { pathname: '/' },
858
+ });
859
+
860
+ jest.clearAllMocks();
861
+
862
+ rerender(
863
+ createComponentElement({
864
+ annotatorState: {
865
+ // No activeAnnotationFileVersionId provided, so it should fall back to file version
866
+ activeAnnotationId: 'some-annotation-id',
867
+ action: 'set_active',
868
+ },
869
+ location: { pathname: '/' },
870
+ }),
871
+ );
872
+
873
+ expect(getAnnotationsPath).toHaveBeenCalledWith('123', 'some-annotation-id');
874
+ });
875
+ });
876
+
877
+ describe('updateActiveAnnotation - Router Disabled', () => {
878
+ test.each`
879
+ condition | prevActiveAnnotationId | activeAnnotationId | isAnnotationsPath | expectedNavigationHandlerCalls
880
+ ${'annotation ids are the same'} | ${'123'} | ${'123'} | ${true} | ${0}
881
+ ${'annotation ids are different'} | ${'123'} | ${'234'} | ${true} | ${1}
882
+ ${'annotation deselected on annotations path'} | ${'123'} | ${null} | ${true} | ${1}
883
+ ${'annotation deselected not on annotations path'} | ${'123'} | ${null} | ${false} | ${0}
884
+ ${'annotation selected not on annotations path'} | ${null} | ${'123'} | ${false} | ${1}
885
+ ${'annotation selected on annotations path'} | ${null} | ${'123'} | ${true} | ${1}
886
+ `(
887
+ 'should call internalSidebarNavigationHandler appropriately if $condition',
888
+ ({ prevActiveAnnotationId, activeAnnotationId, isAnnotationsPath, expectedNavigationHandlerCalls }) => {
889
+ // Build prevNavigation dynamically based on isAnnotationsPath
890
+ let prevNavigation;
891
+ if (isAnnotationsPath) {
892
+ prevNavigation = {
893
+ sidebar: 'activity',
894
+ activeFeedEntryType: 'annotations',
895
+ fileVersionId: '456',
896
+ // For getInternalNavigationMatch to work, we need activeFeedEntryId to be truthy
897
+ // Use the prevActiveAnnotationId if available, otherwise use a placeholder for "on annotations path"
898
+ activeFeedEntryId: prevActiveAnnotationId || 'placeholder',
899
+ };
900
+ } else {
901
+ prevNavigation = { sidebar: 'activity' };
902
+ }
903
+
904
+ const { rerender } = renderWithSidebarAnnotationsRouterDisabled({
905
+ internalSidebarNavigation: prevNavigation,
906
+ annotatorState: { activeAnnotationId: prevActiveAnnotationId },
907
+ });
908
+
909
+ jest.clearAllMocks();
910
+
911
+ rerender(
912
+ createRouterDisabledComponentElement({
913
+ internalSidebarNavigation: prevNavigation,
914
+ annotatorState: { activeAnnotationId, action: 'set_active' },
915
+ }),
916
+ );
917
+
918
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledTimes(expectedNavigationHandlerCalls);
919
+ },
920
+ );
921
+
922
+ test('should use the provided fileVersionId from internal navigation if provided', () => {
923
+ const internalNavigation = {};
924
+
925
+ const { rerender } = renderWithSidebarAnnotationsRouterDisabled({
926
+ internalSidebarNavigation: internalNavigation,
927
+ annotatorState: { activeAnnotationId: 'initial' },
928
+ });
929
+
930
+ jest.clearAllMocks();
931
+
932
+ rerender(
933
+ createRouterDisabledComponentElement({
934
+ internalSidebarNavigation: internalNavigation,
935
+ annotatorState: {
936
+ activeAnnotationFileVersionId: '456',
937
+ activeAnnotationId: '123',
938
+ action: 'set_active',
939
+ },
940
+ }),
941
+ );
942
+
943
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledTimes(1);
944
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledWith({
945
+ sidebar: 'activity',
946
+ activeFeedEntryType: 'annotations',
947
+ activeFeedEntryId: '123',
948
+ fileVersionId: '456',
949
+ open: true,
950
+ });
951
+ });
952
+
953
+ test('should fall back to file version if no fileVersionId in internal navigation', () => {
954
+ const internalNavigation = {
955
+ sidebar: 'activity',
956
+ };
957
+
958
+ const { rerender } = renderWithSidebarAnnotationsRouterDisabled({
959
+ internalSidebarNavigation: internalNavigation,
960
+ annotatorState: { activeAnnotationId: null },
961
+ });
962
+
963
+ jest.clearAllMocks();
964
+
965
+ rerender(
966
+ createRouterDisabledComponentElement({
967
+ internalSidebarNavigation: internalNavigation,
968
+ annotatorState: {
969
+ activeAnnotationId: 'some-annotation-id',
970
+ action: 'set_active',
971
+ },
972
+ }),
973
+ );
974
+
975
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledTimes(1);
976
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledWith({
977
+ sidebar: 'activity',
978
+ activeFeedEntryType: 'annotations',
979
+ activeFeedEntryId: 'some-annotation-id',
980
+ fileVersionId: '123',
981
+ open: true,
982
+ });
983
+ });
984
+
985
+ test.each`
986
+ activeAnnotationId | fileVersionId | internalNavigation | expectedNavigation
987
+ ${'234'} | ${'456'} | ${{ sidebar: 'activity' }} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: '234', fileVersionId: '456', open: true }}
988
+ ${'234'} | ${undefined} | ${{ sidebar: 'activity' }} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: '234', fileVersionId: '123', open: true }}
989
+ ${null} | ${'456'} | ${{ sidebar: 'activity' }} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: undefined, fileVersionId: '456' }}
990
+ ${null} | ${'456'} | ${{ sidebar: 'activity', someOtherProp: 'value' }} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: undefined, fileVersionId: '456' }}
991
+ ${'234'} | ${'456'} | ${{ sidebar: 'activity', someOtherProp: 'value' }} | ${{ sidebar: 'activity', activeFeedEntryType: 'annotations', activeFeedEntryId: '234', fileVersionId: '456', open: true }}
992
+ `(
993
+ 'should set internal navigation based on fileVersionId=$fileVersionId and activeAnnotationId=$activeAnnotationId',
994
+ ({ activeAnnotationId, fileVersionId, internalNavigation, expectedNavigation }) => {
995
+ const { rerender } = renderWithSidebarAnnotationsRouterDisabled({
996
+ internalSidebarNavigation: internalNavigation,
997
+ annotatorState: { activeAnnotationId: 'initial' },
998
+ });
999
+
1000
+ jest.clearAllMocks();
1001
+
1002
+ // Set up the navigation to simulate having the fileVersionId in navigation
1003
+ const navigationWithFileVersion = fileVersionId
1004
+ ? {
1005
+ ...internalNavigation,
1006
+ activeFeedEntryType: 'annotations',
1007
+ activeFeedEntryId: 'placeholder', // Need this for getInternalNavigationMatch to work
1008
+ fileVersionId,
1009
+ }
1010
+ : internalNavigation;
1011
+
1012
+ rerender(
1013
+ createRouterDisabledComponentElement({
1014
+ internalSidebarNavigation: navigationWithFileVersion,
1015
+ annotatorState: { activeAnnotationId, action: 'set_active' },
1016
+ }),
1017
+ );
1018
+
1019
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledTimes(1);
1020
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledWith(expectedNavigation);
1021
+ },
1022
+ );
1023
+ });
1024
+
1025
+ describe('updateActiveVersion', () => {
1026
+ test.each`
1027
+ prevFileVersionId | fileVersionId | expectedOnVersionChangeCalls
1028
+ ${'122'} | ${'122'} | ${0}
1029
+ ${'122'} | ${undefined} | ${0}
1030
+ ${'122'} | ${'123'} | ${1}
1031
+ `(
1032
+ 'should call updateActiveVersion if fileVersionId changes from $prevFileVersionId to $fileVersionId',
1033
+ ({ prevFileVersionId, fileVersionId, expectedOnVersionChangeCalls }) => {
1034
+ const version = { type: FEED_ITEM_TYPE_VERSION, id: fileVersionId };
1035
+ const versions = [{ type: FEED_ITEM_TYPE_VERSION, id: prevFileVersionId }];
1036
+ if (fileVersionId) {
1037
+ versions.push(version);
1038
+ }
1039
+ feedAPI.getCachedItems.mockReturnValue({ items: versions });
1040
+
1041
+ getAnnotationsPath.mockReturnValue('/activity/annotations/123');
1042
+ getAnnotationsMatchPath
1043
+ .mockReturnValueOnce({ params: { fileVersionId: prevFileVersionId } }) // constructor call
1044
+ .mockReturnValueOnce({ params: { fileVersionId: prevFileVersionId } }) // first componentDidUpdate call
1045
+ .mockReturnValueOnce({ params: { fileVersionId } }); // second componentDidUpdate call
1046
+
1047
+ const { rerender } = renderWithSidebarAnnotations({
1048
+ location: { pathname: '/foo' },
1049
+ });
1050
+
1051
+ // When updateActiveVersion is called, it should get the NEW fileVersionId from the match
1052
+ getAnnotationsMatchPath.mockReturnValue({ params: { fileVersionId } });
1053
+
1054
+ jest.clearAllMocks();
1055
+
1056
+ rerender(
1057
+ createComponentElement({
1058
+ location: { pathname: '/bar' },
1059
+ }),
1060
+ );
1061
+
1062
+ expect(onVersionChange).toHaveBeenCalledTimes(expectedOnVersionChangeCalls);
1063
+
1064
+ if (expectedOnVersionChangeCalls > 0) {
1065
+ expect(onVersionChange).toHaveBeenCalledWith(
1066
+ version,
1067
+ expect.objectContaining({
1068
+ currentVersionId: '123',
1069
+ updateVersionToCurrent: expect.any(Function),
1070
+ }),
1071
+ );
1072
+
1073
+ const callback = onVersionChange.mock.calls[0][1].updateVersionToCurrent;
1074
+ callback();
1075
+ expect(getAnnotationsPath).toHaveBeenCalledWith('123');
1076
+ expect(history.push).toHaveBeenCalledWith('/activity/annotations/123');
1077
+ }
1078
+ },
1079
+ );
1080
+ });
1081
+
1082
+ describe('updateActiveVersion - Router Disabled', () => {
1083
+ test.each`
1084
+ prevFileVersionId | fileVersionId | expectedOnVersionChangeCalls
1085
+ ${'122'} | ${'122'} | ${0}
1086
+ ${'122'} | ${undefined} | ${0}
1087
+ ${'122'} | ${'123'} | ${1}
1088
+ `(
1089
+ 'should call updateActiveVersion if fileVersionId changes from $prevFileVersionId to $fileVersionId',
1090
+ ({ prevFileVersionId, fileVersionId, expectedOnVersionChangeCalls }) => {
1091
+ const version = { type: FEED_ITEM_TYPE_VERSION, id: fileVersionId };
1092
+
1093
+ const versions = [{ type: FEED_ITEM_TYPE_VERSION, id: prevFileVersionId }];
1094
+ if (fileVersionId) {
1095
+ versions.push({ type: FEED_ITEM_TYPE_VERSION, id: fileVersionId });
1096
+ }
1097
+ feedAPI.getCachedItems.mockReturnValue({ items: versions });
1098
+
1099
+ // Build internal navigation for router-disabled mode
1100
+ const prevInternalNavigation = {
1101
+ sidebar: 'activity',
1102
+ activeFeedEntryType: 'annotations',
1103
+ activeFeedEntryId: 'annotation123',
1104
+ fileVersionId: prevFileVersionId,
1105
+ };
1106
+
1107
+ const newInternalNavigation = fileVersionId
1108
+ ? {
1109
+ sidebar: 'activity',
1110
+ activeFeedEntryType: 'annotations',
1111
+ activeFeedEntryId: 'annotation123',
1112
+ fileVersionId,
1113
+ }
1114
+ : { sidebar: 'activity' };
1115
+
1116
+ const { rerender } = renderWithSidebarAnnotationsRouterDisabled({
1117
+ internalSidebarNavigation: prevInternalNavigation,
1118
+ });
1119
+
1120
+ // Clear mocks after initial render to only measure componentDidUpdate effects
1121
+ jest.clearAllMocks();
1122
+
1123
+ rerender(
1124
+ createRouterDisabledComponentElement({
1125
+ internalSidebarNavigation: newInternalNavigation,
1126
+ }),
1127
+ );
1128
+
1129
+ expect(onVersionChange).toHaveBeenCalledTimes(expectedOnVersionChangeCalls);
1130
+
1131
+ if (expectedOnVersionChangeCalls > 0) {
1132
+ expect(onVersionChange).toHaveBeenCalledWith(
1133
+ version,
1134
+ expect.objectContaining({
1135
+ currentVersionId: '123',
1136
+ updateVersionToCurrent: expect.any(Function),
1137
+ }),
1138
+ );
1139
+
1140
+ const callback = onVersionChange.mock.calls[0][1].updateVersionToCurrent;
1141
+ callback();
1142
+ expect(internalSidebarNavigationHandler).toHaveBeenCalledWith({
1143
+ sidebar: 'activity',
1144
+ activeFeedEntryType: 'annotations',
1145
+ activeFeedEntryId: undefined,
1146
+ fileVersionId: '123',
1147
+ });
1148
+ }
1149
+ },
1150
+ );
1151
+ });
1152
+ });