@servicetitan/notifications 28.5.0 → 29.0.0

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 (182) hide show
  1. package/dist/__tests__/intercept.test.d.ts +2 -0
  2. package/dist/__tests__/intercept.test.d.ts.map +1 -0
  3. package/dist/__tests__/intercept.test.js +13 -0
  4. package/dist/__tests__/intercept.test.js.map +1 -0
  5. package/dist/__tests__/notifications-service.test.d.ts +2 -0
  6. package/dist/__tests__/notifications-service.test.d.ts.map +1 -0
  7. package/dist/__tests__/notifications-service.test.js +42 -0
  8. package/dist/__tests__/notifications-service.test.js.map +1 -0
  9. package/dist/api/notifications.api.d.ts +2 -2
  10. package/dist/api/notifications.api.d.ts.map +1 -1
  11. package/dist/common.d.ts +4 -22
  12. package/dist/common.d.ts.map +1 -1
  13. package/dist/common.js +4 -11
  14. package/dist/common.js.map +1 -1
  15. package/dist/components/__tests__/notifications.test.d.ts +2 -0
  16. package/dist/components/__tests__/notifications.test.d.ts.map +1 -0
  17. package/dist/components/__tests__/notifications.test.js +93 -0
  18. package/dist/components/__tests__/notifications.test.js.map +1 -0
  19. package/dist/components/notifications.d.ts +8 -1
  20. package/dist/components/notifications.d.ts.map +1 -1
  21. package/dist/components/notifications.js +43 -23
  22. package/dist/components/notifications.js.map +1 -1
  23. package/dist/demo/action-button-preview.d.ts.map +1 -1
  24. package/dist/demo/action-button-preview.js +2 -2
  25. package/dist/demo/action-button-preview.js.map +1 -1
  26. package/dist/demo/basic-preview.d.ts.map +1 -1
  27. package/dist/demo/basic-preview.js +1 -1
  28. package/dist/demo/basic-preview.js.map +1 -1
  29. package/dist/demo/container.d.ts +1 -3
  30. package/dist/demo/container.d.ts.map +1 -1
  31. package/dist/demo/container.js.map +1 -1
  32. package/dist/demo/duration-preview.d.ts.map +1 -1
  33. package/dist/demo/duration-preview.js +2 -2
  34. package/dist/demo/duration-preview.js.map +1 -1
  35. package/dist/demo/multiline-message-preview.d.ts.map +1 -1
  36. package/dist/demo/multiline-message-preview.js +2 -2
  37. package/dist/demo/multiline-message-preview.js.map +1 -1
  38. package/dist/demo/prevent-duplicates-preview.d.ts.map +1 -1
  39. package/dist/demo/prevent-duplicates-preview.js +2 -2
  40. package/dist/demo/prevent-duplicates-preview.js.map +1 -1
  41. package/dist/demo/progress-preview.d.ts.map +1 -1
  42. package/dist/demo/progress-preview.js +2 -2
  43. package/dist/demo/progress-preview.js.map +1 -1
  44. package/dist/demo/server-custom-preview.d.ts +1 -1
  45. package/dist/demo/server-custom-preview.d.ts.map +1 -1
  46. package/dist/demo/server-custom-preview.js +21 -7
  47. package/dist/demo/server-custom-preview.js.map +1 -1
  48. package/dist/demo/server-custom.d.ts +0 -1
  49. package/dist/demo/server-custom.d.ts.map +1 -1
  50. package/dist/demo/server-custom.js +25 -21
  51. package/dist/demo/server-custom.js.map +1 -1
  52. package/dist/demo/server-default.d.ts.map +1 -1
  53. package/dist/demo/server-default.js +1 -1
  54. package/dist/demo/server-default.js.map +1 -1
  55. package/dist/demo/status-variations-preview.d.ts.map +1 -1
  56. package/dist/demo/status-variations-preview.js +2 -2
  57. package/dist/demo/status-variations-preview.js.map +1 -1
  58. package/dist/demo/status-variations.d.ts.map +1 -1
  59. package/dist/demo/status-variations.js.map +1 -1
  60. package/dist/index.d.ts +3 -4
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +3 -4
  63. package/dist/index.js.map +1 -1
  64. package/dist/intercept.d.ts +4 -0
  65. package/dist/intercept.d.ts.map +1 -0
  66. package/dist/intercept.js +5 -0
  67. package/dist/intercept.js.map +1 -0
  68. package/dist/notifications-channel.d.ts +2 -6
  69. package/dist/notifications-channel.d.ts.map +1 -1
  70. package/dist/notifications-channel.js.map +1 -1
  71. package/dist/notifications-service.d.ts +10 -8
  72. package/dist/notifications-service.d.ts.map +1 -1
  73. package/dist/notifications-service.js.map +1 -1
  74. package/dist/notifications.stories.d.ts +14 -0
  75. package/dist/notifications.stories.d.ts.map +1 -0
  76. package/dist/notifications.stories.js +20 -0
  77. package/dist/notifications.stories.js.map +1 -0
  78. package/dist/stores/__mocks__/mock-notifications-channel.d.ts +11 -0
  79. package/dist/stores/__mocks__/mock-notifications-channel.d.ts.map +1 -0
  80. package/dist/stores/__mocks__/mock-notifications-channel.js +36 -0
  81. package/dist/stores/__mocks__/mock-notifications-channel.js.map +1 -0
  82. package/dist/stores/__tests__/notifications.store.test.d.ts +2 -0
  83. package/dist/stores/__tests__/notifications.store.test.d.ts.map +1 -0
  84. package/dist/stores/__tests__/notifications.store.test.js +360 -0
  85. package/dist/stores/__tests__/notifications.store.test.js.map +1 -0
  86. package/dist/stores/notifications.store.d.ts +20 -8
  87. package/dist/stores/notifications.store.d.ts.map +1 -1
  88. package/dist/stores/notifications.store.js +156 -45
  89. package/dist/stores/notifications.store.js.map +1 -1
  90. package/dist/utils/__tests__/date-from-string.test.d.ts +2 -0
  91. package/dist/utils/__tests__/date-from-string.test.d.ts.map +1 -0
  92. package/dist/utils/__tests__/date-from-string.test.js +39 -0
  93. package/dist/utils/__tests__/date-from-string.test.js.map +1 -0
  94. package/dist/utils/__tests__/use-compatible-navigate.test.d.ts +2 -0
  95. package/dist/utils/__tests__/use-compatible-navigate.test.d.ts.map +1 -0
  96. package/dist/utils/__tests__/use-compatible-navigate.test.js +27 -0
  97. package/dist/utils/__tests__/use-compatible-navigate.test.js.map +1 -0
  98. package/dist/utils/date-from-string.d.ts.map +1 -0
  99. package/dist/utils/date-from-string.js +19 -0
  100. package/dist/utils/date-from-string.js.map +1 -0
  101. package/package.json +6 -6
  102. package/src/__tests__/intercept.test.ts +18 -0
  103. package/src/__tests__/notifications-service.test.ts +62 -0
  104. package/src/api/notifications.api.ts +2 -2
  105. package/src/common.ts +12 -27
  106. package/src/components/__tests__/notifications.test.tsx +107 -0
  107. package/src/components/notifications.tsx +59 -36
  108. package/src/demo/action-button-preview.tsx +4 -6
  109. package/src/demo/basic-preview.tsx +2 -4
  110. package/src/demo/container.tsx +1 -4
  111. package/src/demo/duration-preview.tsx +4 -6
  112. package/src/demo/multiline-message-preview.tsx +3 -9
  113. package/src/demo/prevent-duplicates-preview.tsx +3 -9
  114. package/src/demo/progress-preview.tsx +3 -9
  115. package/src/demo/server-custom-preview.tsx +17 -14
  116. package/src/demo/server-custom.tsx +30 -29
  117. package/src/demo/server-default.tsx +1 -3
  118. package/src/demo/status-variations-preview.tsx +4 -6
  119. package/src/demo/status-variations.tsx +0 -1
  120. package/src/index.ts +13 -4
  121. package/src/intercept.ts +14 -0
  122. package/src/notifications-channel.ts +2 -6
  123. package/src/notifications-service.ts +14 -42
  124. package/src/stores/__mocks__/mock-notifications-channel.ts +31 -0
  125. package/src/stores/__tests__/notifications.store.test.ts +458 -0
  126. package/src/stores/notifications.store.ts +178 -53
  127. package/src/utils/__tests__/date-from-string.test.ts +53 -0
  128. package/src/utils/__tests__/use-compatible-navigate.test.ts +43 -0
  129. package/src/utils/date-from-string.ts +22 -0
  130. package/dist/components/__tests__/container.test.d.ts +0 -2
  131. package/dist/components/__tests__/container.test.d.ts.map +0 -1
  132. package/dist/components/__tests__/container.test.js +0 -59
  133. package/dist/components/__tests__/container.test.js.map +0 -1
  134. package/dist/components/container.d.ts +0 -3
  135. package/dist/components/container.d.ts.map +0 -1
  136. package/dist/components/container.js +0 -26
  137. package/dist/components/container.js.map +0 -1
  138. package/dist/components/default-notification.d.ts +0 -7
  139. package/dist/components/default-notification.d.ts.map +0 -1
  140. package/dist/components/default-notification.js +0 -26
  141. package/dist/components/default-notification.js.map +0 -1
  142. package/dist/components/default-notification.module.css +0 -3
  143. package/dist/components/no-ssr.d.ts +0 -5
  144. package/dist/components/no-ssr.d.ts.map +0 -1
  145. package/dist/components/no-ssr.js +0 -9
  146. package/dist/components/no-ssr.js.map +0 -1
  147. package/dist/components/notifications-unwrapped.d.ts +0 -3
  148. package/dist/components/notifications-unwrapped.d.ts.map +0 -1
  149. package/dist/components/notifications-unwrapped.js +0 -27
  150. package/dist/components/notifications-unwrapped.js.map +0 -1
  151. package/dist/components/shadow-dom.d.ts +0 -3
  152. package/dist/components/shadow-dom.d.ts.map +0 -1
  153. package/dist/components/shadow-dom.js +0 -41
  154. package/dist/components/shadow-dom.js.map +0 -1
  155. package/dist/components/use-style-sheets.d.ts +0 -2
  156. package/dist/components/use-style-sheets.d.ts.map +0 -1
  157. package/dist/components/use-style-sheets.js +0 -15
  158. package/dist/components/use-style-sheets.js.map +0 -1
  159. package/dist/create-element.d.ts +0 -2
  160. package/dist/create-element.d.ts.map +0 -1
  161. package/dist/create-element.js +0 -8
  162. package/dist/create-element.js.map +0 -1
  163. package/dist/date-from-string.d.ts.map +0 -1
  164. package/dist/date-from-string.js +0 -27
  165. package/dist/date-from-string.js.map +0 -1
  166. package/dist/register.d.ts +0 -3
  167. package/dist/register.d.ts.map +0 -1
  168. package/dist/register.js +0 -5
  169. package/dist/register.js.map +0 -1
  170. package/src/components/__tests__/container.test.tsx +0 -71
  171. package/src/components/container.tsx +0 -35
  172. package/src/components/default-notification.module.css +0 -3
  173. package/src/components/default-notification.module.css.d.ts +0 -4
  174. package/src/components/default-notification.tsx +0 -59
  175. package/src/components/no-ssr.tsx +0 -11
  176. package/src/components/notifications-unwrapped.tsx +0 -51
  177. package/src/components/shadow-dom.tsx +0 -53
  178. package/src/components/use-style-sheets.ts +0 -19
  179. package/src/create-element.ts +0 -12
  180. package/src/date-from-string.ts +0 -30
  181. package/src/register.ts +0 -6
  182. /package/dist/{date-from-string.d.ts → utils/date-from-string.d.ts} +0 -0
@@ -0,0 +1,31 @@
1
+ type EventCallback = (data: any, context: any) => void;
2
+
3
+ export class MockNotificationsChannel {
4
+ private readonly bindings: Map<string, EventCallback>;
5
+
6
+ constructor() {
7
+ this.bindings = new Map();
8
+ Object.assign(this, { bind: this.bindEvent });
9
+ }
10
+
11
+ // eslint-disable-next-line @typescript-eslint/naming-convention
12
+ get global_emitter() {
13
+ return this;
14
+ }
15
+
16
+ unbind(event: string, fn: EventCallback, _context?: any) {
17
+ if (this.bindings.get(event) === fn) {
18
+ this.bindings.delete(event);
19
+ }
20
+ return this;
21
+ }
22
+
23
+ emit(event: string, data: any) {
24
+ this.bindings.get(event)?.(data, undefined);
25
+ }
26
+
27
+ private readonly bindEvent = (event: string, fn: EventCallback, _context?: any) => {
28
+ this.bindings.set(event, fn);
29
+ return this;
30
+ };
31
+ }
@@ -0,0 +1,458 @@
1
+ import { toast } from '@servicetitan/anvil2';
2
+ import { MouseEventHandler } from 'react';
3
+
4
+ import {
5
+ Notification,
6
+ NotificationProcessStatus,
7
+ NotificationsApi,
8
+ } from '../../api/notifications.api';
9
+ import { DefaultNotificationOptions, FunctionAction, LinkAction, Status } from '../../common';
10
+ import { MockNotificationsChannel } from '../__mocks__/mock-notifications-channel';
11
+
12
+ import { NotificationsStore } from '../notifications.store';
13
+
14
+ jest.mock('@servicetitan/anvil2', () => ({
15
+ toast: {
16
+ info: jest.fn(makeId),
17
+ success: jest.fn(),
18
+ warning: jest.fn(),
19
+ danger: jest.fn(),
20
+ update: jest.fn(),
21
+ dismiss: jest.fn(),
22
+ },
23
+ }));
24
+
25
+ class MockNotificationsApi implements NotificationsApi {
26
+ getNotifications = jest.fn().mockResolvedValue({ data: [] });
27
+ getNotification = jest.fn();
28
+ changeIsReadProperty = jest.fn();
29
+ }
30
+
31
+ function makeId() {
32
+ return Math.round(1e6 * Math.random()).toString(36);
33
+ }
34
+
35
+ function getServerStatusName(status: NotificationProcessStatus) {
36
+ return {
37
+ [NotificationProcessStatus.Info]: 'Info',
38
+ [NotificationProcessStatus.InProgress]: 'InProgress',
39
+ [NotificationProcessStatus.Failure]: 'Failure',
40
+ [NotificationProcessStatus.Success]: 'Success',
41
+ }[status];
42
+ }
43
+
44
+ describe(NotificationsStore.name, () => {
45
+ let store: NotificationsStore | undefined;
46
+
47
+ beforeEach(() => {
48
+ store = undefined;
49
+ jest.clearAllMocks();
50
+ jest.useFakeTimers();
51
+ });
52
+
53
+ afterEach(() => {
54
+ store?.dispose();
55
+ jest.useRealTimers();
56
+ });
57
+
58
+ describe('when notification is added', () => {
59
+ let options: DefaultNotificationOptions;
60
+ let preventDuplicates: boolean | undefined;
61
+
62
+ beforeEach(() => {
63
+ options = { title: 'Foo', message: 'bar', duration: 0, progress: 0 };
64
+ preventDuplicates = undefined;
65
+ });
66
+
67
+ const subject = () => {
68
+ if (!store) {
69
+ store = new NotificationsStore();
70
+ store.initialize(0);
71
+ }
72
+ store.add(options, preventDuplicates);
73
+ };
74
+
75
+ function itAddsToast({ method = 'info' }: { method?: keyof typeof toast } = {}) {
76
+ test(`adds ${method} toast`, () => {
77
+ subject();
78
+
79
+ const { status, ...toastOptions } = options;
80
+ expect(toast[method]).toHaveBeenCalledWith(expect.objectContaining(toastOptions));
81
+ });
82
+ }
83
+
84
+ itAddsToast();
85
+
86
+ describe('when preventDuplicates is true', () => {
87
+ beforeEach(() => (preventDuplicates = true));
88
+
89
+ describe('with existing notification', () => {
90
+ beforeEach(() => {
91
+ subject();
92
+ jest.clearAllMocks();
93
+ });
94
+
95
+ test('ignores duplicate notification', () => {
96
+ subject();
97
+
98
+ expect(toast.info).not.toHaveBeenCalled();
99
+ });
100
+
101
+ describe('with different notification', () => {
102
+ beforeEach(() => (options.title = `!${options.title}`));
103
+
104
+ itAddsToast();
105
+ });
106
+ });
107
+ });
108
+
109
+ interface TestCase {
110
+ status: Status;
111
+ method: keyof typeof toast;
112
+ }
113
+
114
+ describe.each([
115
+ { status: Status.Success, method: 'success' },
116
+ { status: Status.Warning, method: 'warning' },
117
+ { status: Status.Error, method: 'danger' },
118
+ ] as TestCase[])('when status is "$status"', ({ method, status }) => {
119
+ beforeEach(() => (options.status = status));
120
+
121
+ itAddsToast({ method });
122
+ });
123
+
124
+ describe('when notification has action', () => {
125
+ let navigate: jest.Func | undefined;
126
+
127
+ beforeEach(() => {
128
+ options.action = { label: 'foo', link: 'bar' };
129
+ navigate = jest.fn();
130
+ });
131
+
132
+ test('sets duration to false', () => {
133
+ subject();
134
+
135
+ expect(toast.info).toHaveBeenCalledWith(
136
+ expect.objectContaining({ duration: false })
137
+ );
138
+ });
139
+
140
+ describe('when action is clicked', () => {
141
+ const setup = () => {
142
+ subject();
143
+ store!.navigate = navigate as any;
144
+ jest.mocked(toast.info).mock.calls[0][0].actions?.primary.onClick?.({} as any);
145
+ };
146
+
147
+ test('dismisses toast', () => {
148
+ setup();
149
+
150
+ expect(toast.dismiss).toHaveBeenCalled();
151
+ });
152
+
153
+ test('navigates to link', () => {
154
+ setup();
155
+
156
+ expect(navigate).toHaveBeenCalledWith((options.action as LinkAction).link);
157
+ });
158
+
159
+ describe('when "navigate" is not defined', () => {
160
+ beforeEach(() => (navigate = undefined));
161
+
162
+ test('does nothing', () => {
163
+ expect(() => setup()).not.toThrow();
164
+ });
165
+ });
166
+
167
+ describe('when link is external', () => {
168
+ beforeEach(() => ((options.action as LinkAction).external = true));
169
+
170
+ test('opens link in new window', () => {
171
+ const openSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
172
+
173
+ setup();
174
+
175
+ expect(openSpy).toHaveBeenCalledWith((options.action as LinkAction).link);
176
+ });
177
+ });
178
+
179
+ describe('when action is callback', () => {
180
+ beforeEach(() => {
181
+ delete (options.action as any).link;
182
+ (options.action as FunctionAction).onClick = jest.fn();
183
+ });
184
+
185
+ test('invokes callback', () => {
186
+ setup();
187
+
188
+ expect((options.action as FunctionAction).onClick).toHaveBeenCalled();
189
+ });
190
+ });
191
+ });
192
+ });
193
+ });
194
+
195
+ describe('when notification is published', () => {
196
+ const userId = 42;
197
+ const api = new MockNotificationsApi();
198
+ const publisher = new MockNotificationsChannel();
199
+ const options: DefaultNotificationOptions = { title: 'Foo' };
200
+ let notification: Notification;
201
+
202
+ beforeEach(() => {
203
+ notification = {
204
+ id: 1,
205
+ userId,
206
+ type: 'ServerNotification',
207
+ status: NotificationProcessStatus.Info,
208
+ isRead: false,
209
+ createdOn: new Date(),
210
+ modifiedOn: new Date(),
211
+ version: 1,
212
+ payload: JSON.stringify(options),
213
+ };
214
+ });
215
+
216
+ afterEach(() => store?.dispose());
217
+
218
+ const subject = () => {
219
+ if (!store) {
220
+ store = new NotificationsStore(api, () => publisher as any);
221
+ store.initialize(userId);
222
+ }
223
+
224
+ publisher.emit('NotificationEvent', notification);
225
+ };
226
+
227
+ function itAddsToast(method: keyof typeof toast = 'info') {
228
+ test(`adds ${method} toast`, () => {
229
+ subject();
230
+
231
+ expect(toast[method]).toHaveBeenCalledWith(expect.objectContaining(options));
232
+ });
233
+ }
234
+
235
+ itAddsToast();
236
+
237
+ interface TestCase {
238
+ status: NotificationProcessStatus;
239
+ method: keyof typeof toast;
240
+ }
241
+
242
+ describe.each([
243
+ { status: NotificationProcessStatus.Success, method: 'success' },
244
+ { status: NotificationProcessStatus.Failure, method: 'danger' },
245
+ { status: NotificationProcessStatus.InProgress, method: 'info' },
246
+ ] as TestCase[])('when status is "$status"', ({ method, status }) => {
247
+ beforeEach(() => (notification.status = status));
248
+
249
+ itAddsToast(method);
250
+ });
251
+
252
+ describe('when notification is for different user', () => {
253
+ beforeEach(() => ++notification.userId);
254
+
255
+ test('ignores notification', () => {
256
+ subject();
257
+
258
+ expect(toast.info).not.toHaveBeenCalled();
259
+ });
260
+ });
261
+
262
+ describe('when notification is closed', () => {
263
+ const setup = () => {
264
+ subject();
265
+ jest.mocked(toast.info).mock.calls[0][0].onClose?.({} as any);
266
+ };
267
+
268
+ test('notifies api', () => {
269
+ setup();
270
+
271
+ expect(api.changeIsReadProperty).toHaveBeenCalledWith(
272
+ notification.id,
273
+ true,
274
+ notification.version
275
+ );
276
+ });
277
+ });
278
+
279
+ describe('when notification has interceptor', () => {
280
+ const newOptions = { title: `!${options.title}` };
281
+ const interceptor = jest.fn();
282
+
283
+ beforeEach(() => {
284
+ notification.type = makeId();
285
+ interceptor.mockImplementation(original => ({ ...original, payload: newOptions }));
286
+ NotificationsStore.intercept(notification.type, interceptor);
287
+ });
288
+
289
+ test('adds toast with interceptor return value', () => {
290
+ subject();
291
+
292
+ expect(toast.info).toHaveBeenCalledWith(expect.objectContaining(newOptions));
293
+ });
294
+
295
+ describe('dismiss callback', () => {
296
+ let dismissCallback: Function;
297
+ let onClose: MouseEventHandler<any>;
298
+ let toastId: string;
299
+
300
+ beforeEach(() => {
301
+ subject();
302
+
303
+ dismissCallback = interceptor.mock.calls[0][1];
304
+ onClose = jest.mocked(toast.info).mock.calls[0][0].onClose!;
305
+ toastId = jest.mocked(toast.info).mock.results[0].value;
306
+
307
+ jest.clearAllMocks();
308
+ });
309
+
310
+ test('dismisses toast', () => {
311
+ dismissCallback();
312
+
313
+ expect(toast.dismiss).toHaveBeenCalledWith(toastId);
314
+ });
315
+
316
+ describe('when toast is already closed', () => {
317
+ beforeEach(() => onClose({} as any));
318
+
319
+ test('does nothing', () => {
320
+ dismissCallback();
321
+
322
+ expect(toast.dismiss).not.toHaveBeenCalled();
323
+ });
324
+ });
325
+ });
326
+
327
+ describe('when interceptor returns undefined', () => {
328
+ beforeEach(() => interceptor.mockReturnValue(undefined));
329
+
330
+ test('ignores notification', () => {
331
+ subject();
332
+
333
+ expect(toast.info).not.toHaveBeenCalled();
334
+ });
335
+ });
336
+
337
+ describe('when interceptor is removed', () => {
338
+ beforeEach(() => NotificationsStore.intercept(notification.type));
339
+
340
+ itAddsToast();
341
+ });
342
+ });
343
+
344
+ [NotificationProcessStatus.Info, NotificationProcessStatus.InProgress].forEach(status => {
345
+ const name = getServerStatusName(status);
346
+
347
+ describe(`when "${name}" notification already exists`, () => {
348
+ beforeEach(() => {
349
+ notification.status = status;
350
+ subject();
351
+ });
352
+
353
+ function itUpdatesToast() {
354
+ test('updates toast', () => {
355
+ subject();
356
+
357
+ expect(toast.update).toHaveBeenCalledWith(
358
+ expect.any(String),
359
+ expect.objectContaining(options)
360
+ );
361
+ });
362
+ }
363
+
364
+ function itIgnoresNotification() {
365
+ test('ignores notification', () => {
366
+ subject();
367
+
368
+ expect(toast.update).not.toHaveBeenCalled();
369
+ });
370
+ }
371
+
372
+ itIgnoresNotification();
373
+
374
+ describe('when duplicate is newer', () => {
375
+ beforeEach(() => (notification.modifiedOn = new Date(Date.now() + 1)));
376
+
377
+ itUpdatesToast();
378
+
379
+ describe('when duplicate has different status', () => {
380
+ beforeEach(() => (notification.status = NotificationProcessStatus.Success));
381
+
382
+ test('replaces toast', () => {
383
+ subject();
384
+
385
+ expect(toast.dismiss).toHaveBeenCalled();
386
+ expect(toast.success).toHaveBeenCalledWith(
387
+ expect.objectContaining(options)
388
+ );
389
+ });
390
+
391
+ describe('when notification has no payload', () => {
392
+ beforeEach(() => delete notification.payload);
393
+
394
+ test('replaces with empty toast', () => {
395
+ subject();
396
+
397
+ expect(toast.dismiss).toHaveBeenCalled();
398
+ expect(toast.success).toHaveBeenCalledWith(
399
+ expect.objectContaining({ title: undefined })
400
+ );
401
+ });
402
+ });
403
+
404
+ if (status !== NotificationProcessStatus.InProgress) {
405
+ describe('when new status is "InProgress"', () => {
406
+ beforeEach(() => {
407
+ notification.status = NotificationProcessStatus.InProgress;
408
+ });
409
+
410
+ itUpdatesToast();
411
+ });
412
+ }
413
+ });
414
+
415
+ if (status === NotificationProcessStatus.InProgress) {
416
+ describe('when existing notification was closed', () => {
417
+ beforeEach(() => {
418
+ jest.mocked(toast.info).mock.calls[0][0].onClose?.({} as any);
419
+ });
420
+
421
+ itIgnoresNotification();
422
+
423
+ describe('when duplicate has different status', () => {
424
+ beforeEach(() => {
425
+ notification.status = NotificationProcessStatus.Success;
426
+ });
427
+
428
+ itAddsToast();
429
+ });
430
+
431
+ describe('when notification was close over a day ago', () => {
432
+ const DAY_INTERVAL = 24 * 60 * 60 * 1000;
433
+
434
+ beforeEach(() => {
435
+ jest.clearAllMocks();
436
+ jest.setSystemTime(Date.now() + DAY_INTERVAL + 1);
437
+ jest.advanceTimersByTime(DAY_INTERVAL + 1);
438
+ });
439
+
440
+ itAddsToast();
441
+ });
442
+ });
443
+ }
444
+ });
445
+ });
446
+ });
447
+
448
+ describe('when api has pending notifications', () => {
449
+ beforeEach(() => api.getNotifications.mockResolvedValue({ data: [notification] }));
450
+
451
+ test('adds toast', () => {
452
+ subject();
453
+
454
+ expect(toast.info).toHaveBeenCalledWith(expect.objectContaining(options));
455
+ });
456
+ });
457
+ });
458
+ });