@linktr.ee/messaging-react 1.37.0 → 1.38.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 (29) hide show
  1. package/dist/Card-DwgUtqsA.js +127 -0
  2. package/dist/Card-DwgUtqsA.js.map +1 -0
  3. package/dist/Card-RgHsp9x1.js +138 -0
  4. package/dist/Card-RgHsp9x1.js.map +1 -0
  5. package/dist/{index-DOsC03ZN.js → index-B_4pciGp.js} +1411 -1358
  6. package/dist/index-B_4pciGp.js.map +1 -0
  7. package/dist/index.d.ts +21 -1
  8. package/dist/index.js +15 -13
  9. package/package.json +1 -1
  10. package/src/components/{LockedAttachment/components → AttachmentCard}/MediaPlayer.tsx +4 -3
  11. package/src/components/AttachmentCard/Thumbnail.tsx +150 -0
  12. package/src/components/AttachmentCard/index.tsx +114 -0
  13. package/src/components/LockedAttachment/components/Creator/Card.tsx +123 -113
  14. package/src/components/LockedAttachment/components/Visitor/Card.tsx +43 -42
  15. package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +12 -0
  16. package/src/components/MediaMessage/MediaMessage.stories.tsx +45 -4
  17. package/src/components/MediaMessage/MediaMessage.test.tsx +151 -153
  18. package/src/components/MediaMessage/index.tsx +239 -349
  19. package/src/index.ts +7 -3
  20. package/dist/Card-BHrnmHeu.js +0 -167
  21. package/dist/Card-BHrnmHeu.js.map +0 -1
  22. package/dist/Card-D4vEgqWt.js +0 -195
  23. package/dist/Card-D4vEgqWt.js.map +0 -1
  24. package/dist/index-DOsC03ZN.js.map +0 -1
  25. package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +0 -114
  26. package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +0 -81
  27. /package/src/components/{LockedAttachment → AttachmentCard}/utils/icons.ts +0 -0
  28. /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.test.ts +0 -0
  29. /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.ts +0 -0
@@ -1,19 +1,10 @@
1
1
  import React from 'react'
2
2
  import type { LocalMessage } from 'stream-chat'
3
- import { beforeAll, describe, expect, it, vi } from 'vitest'
3
+ import { describe, expect, it, vi } from 'vitest'
4
4
 
5
5
  import { renderWithProviders, screen, fireEvent } from '../../test/utils'
6
6
 
7
- import { MediaMessage } from '.'
8
-
9
- beforeAll(() => {
10
- HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) {
11
- this.setAttribute('open', '')
12
- })
13
- HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
14
- this.removeAttribute('open')
15
- })
16
- })
7
+ import { MediaMessage, resolveLinkAttachment } from '.'
17
8
 
18
9
  vi.mock('../Avatar', () => ({
19
10
  Avatar: ({ id }: { id: string }) => (
@@ -21,15 +12,13 @@ vi.mock('../Avatar', () => ({
21
12
  ),
22
13
  }))
23
14
 
24
- vi.mock('../LockedAttachment/components/MediaPlayer', () => ({
15
+ vi.mock('../AttachmentCard/MediaPlayer', () => ({
25
16
  default: ({
26
17
  source,
27
18
  mimeType,
28
- onContainerClick,
29
19
  }: {
30
20
  source: string
31
21
  mimeType: string
32
- onContainerClick?: (e: React.MouseEvent) => void
33
22
  }) => (
34
23
  <div
35
24
  role="button"
@@ -37,17 +26,15 @@ vi.mock('../LockedAttachment/components/MediaPlayer', () => ({
37
26
  data-testid="media-player"
38
27
  data-source={source}
39
28
  data-mime-type={mimeType}
40
- onClick={onContainerClick}
41
- onKeyDown={undefined}
42
29
  />
43
30
  ),
44
31
  }))
45
32
 
46
- vi.mock('../LockedAttachment/utils/icons', () => ({
33
+ vi.mock('../AttachmentCard/utils/icons', () => ({
47
34
  renderTypeIcon: () => <span data-testid="type-icon" />,
48
35
  }))
49
36
 
50
- vi.mock('../LockedAttachment/utils/mimeType', () => ({
37
+ vi.mock('../AttachmentCard/utils/mimeType', () => ({
51
38
  getSourceType: (mimeType: string) => {
52
39
  if (mimeType.startsWith('video/')) return 'video'
53
40
  if (mimeType.startsWith('image/')) return 'image'
@@ -98,203 +85,195 @@ describe('MediaMessage', () => {
98
85
  expect(player).toHaveAttribute('data-source', 'https://cdn.example.com/clip.mp4')
99
86
  })
100
87
 
101
- it('opens viewer when the video player container is clicked', () => {
88
+ it('renders MediaPlayer for an audio attachment', () => {
102
89
  renderWithProviders(
103
90
  <MediaMessage
104
91
  message={msg({
105
92
  attachments: [
106
93
  {
107
- type: 'video',
108
- asset_url: 'https://cdn.example.com/clip.mp4',
109
- mime_type: 'video/mp4',
110
- title: 'My Clip',
94
+ type: 'audio',
95
+ asset_url: 'https://cdn.example.com/track.mp3',
96
+ mime_type: 'audio/mpeg',
111
97
  },
112
98
  ],
113
99
  })}
114
100
  />
115
101
  )
116
102
 
117
- fireEvent.click(screen.getByTestId('media-player'))
118
- expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
119
- expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
103
+ expect(screen.getByTestId('media-player')).toHaveAttribute(
104
+ 'data-source',
105
+ 'https://cdn.example.com/track.mp3'
106
+ )
120
107
  })
121
108
 
122
- it('renders MediaPlayer for an audio attachment', () => {
109
+ it('renders an img for an image attachment', () => {
123
110
  renderWithProviders(
124
111
  <MediaMessage
125
112
  message={msg({
126
113
  attachments: [
127
114
  {
128
- type: 'audio',
129
- asset_url: 'https://cdn.example.com/track.mp3',
130
- mime_type: 'audio/mpeg',
115
+ type: 'image',
116
+ image_url: 'https://cdn.example.com/photo.jpg',
117
+ mime_type: 'image/jpeg',
118
+ title: 'My Photo',
131
119
  },
132
120
  ],
133
121
  })}
134
122
  />
135
123
  )
136
124
 
137
- expect(screen.getByTestId('media-player')).toHaveAttribute(
138
- 'data-source',
139
- 'https://cdn.example.com/track.mp3'
140
- )
125
+ const image = screen.getByRole('img', { name: 'My Photo' })
126
+ expect(image).toHaveAttribute('src', 'https://cdn.example.com/photo.jpg')
141
127
  })
142
128
 
143
- it('does not show viewer or expand button for audio', () => {
129
+ it('renders unknown file type as icon placeholder, not a link', () => {
144
130
  renderWithProviders(
145
131
  <MediaMessage
146
132
  message={msg({
147
133
  attachments: [
148
134
  {
149
- type: 'audio',
150
- asset_url: 'https://cdn.example.com/track.mp3',
151
- mime_type: 'audio/mpeg',
152
- title: 'My Track',
135
+ type: 'file',
136
+ asset_url: 'https://cdn.example.com/data.bin',
137
+ mime_type: 'application/octet-stream',
138
+ title: 'Unknown File',
153
139
  },
154
140
  ],
155
141
  })}
156
142
  />
157
143
  )
158
144
 
159
- expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
160
- expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
145
+ expect(screen.queryByRole('link')).not.toBeInTheDocument()
146
+ expect(screen.getByText('Unknown File')).toBeInTheDocument()
147
+ expect(screen.getAllByTestId('type-icon').length).toBeGreaterThanOrEqual(1)
161
148
  })
162
149
 
163
- it('renders an img for an image attachment and opens viewer on click', () => {
150
+ it('shows title and file size in meta row', () => {
164
151
  renderWithProviders(
165
152
  <MediaMessage
166
153
  message={msg({
167
154
  attachments: [
168
155
  {
169
- type: 'image',
170
- image_url: 'https://cdn.example.com/photo.jpg',
171
- mime_type: 'image/jpeg',
172
- title: 'My Photo',
156
+ type: 'video',
157
+ asset_url: 'https://cdn.example.com/clip.mp4',
158
+ mime_type: 'video/mp4',
159
+ title: 'Clip',
160
+ file_size: 2048,
173
161
  },
174
162
  ],
175
163
  })}
176
164
  />
177
165
  )
178
166
 
179
- const image = screen.getByRole('img', { name: 'My Photo' })
180
- expect(image).toHaveAttribute('src', 'https://cdn.example.com/photo.jpg')
181
-
182
- // Clicking the image button opens the viewer
183
- fireEvent.click(screen.getByRole('button', { name: 'My Photo' }))
184
- expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
185
- const viewerImages = screen.getAllByRole('img', { name: 'My Photo' })
186
- expect(viewerImages.some((img) => img.classList.contains('rounded-2xl'))).toBe(true)
167
+ expect(screen.getByText('Clip')).toBeInTheDocument()
168
+ expect(screen.getByText('2.0 KB')).toBeInTheDocument()
187
169
  })
188
170
 
189
- it('does not show expand button in chin for images, only download', () => {
171
+ it('renders Avatar for a message from another user', () => {
190
172
  renderWithProviders(
191
173
  <MediaMessage
174
+ isMyMessage={false}
192
175
  message={msg({
193
176
  attachments: [
194
177
  {
195
178
  type: 'image',
196
179
  image_url: 'https://cdn.example.com/photo.jpg',
197
180
  mime_type: 'image/jpeg',
198
- title: 'My Photo',
199
181
  },
200
182
  ],
201
183
  })}
202
184
  />
203
185
  )
204
186
 
205
- // Thumbnail button + download button — no separate expand/fullscreen button
206
- expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
207
- expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
187
+ expect(screen.getByTestId('avatar')).toBeInTheDocument()
188
+ expect(screen.getByTestId('avatar')).toHaveAttribute('data-user-id', 'user-1')
208
189
  })
209
190
 
210
- it('renders PDF card with a viewer button', () => {
191
+ it('does not render Avatar for own messages', () => {
211
192
  renderWithProviders(
212
193
  <MediaMessage
194
+ isMyMessage={true}
213
195
  message={msg({
214
196
  attachments: [
215
197
  {
216
- type: 'file',
217
- asset_url: 'https://cdn.example.com/report.pdf',
218
- mime_type: 'application/pdf',
219
- title: 'Annual Report',
198
+ type: 'image',
199
+ image_url: 'https://cdn.example.com/photo.jpg',
200
+ mime_type: 'image/jpeg',
220
201
  },
221
202
  ],
222
203
  })}
223
204
  />
224
205
  )
225
206
 
226
- expect(screen.getByRole('button', { name: 'Open PDF viewer' })).toBeInTheDocument()
227
- expect(screen.getByText('Annual Report')).toBeInTheDocument()
207
+ expect(screen.queryByTestId('avatar')).not.toBeInTheDocument()
228
208
  })
229
209
 
230
- it('opens PDF viewer when PDF thumbnail is clicked', () => {
231
- renderWithProviders(
210
+ it('applies the --me class for own messages', () => {
211
+ const { container } = renderWithProviders(
232
212
  <MediaMessage
213
+ isMyMessage={true}
233
214
  message={msg({
234
215
  attachments: [
235
216
  {
236
- type: 'file',
237
- asset_url: 'https://cdn.example.com/report.pdf',
238
- mime_type: 'application/pdf',
239
- title: 'Annual Report',
217
+ type: 'image',
218
+ image_url: 'https://cdn.example.com/photo.jpg',
219
+ mime_type: 'image/jpeg',
240
220
  },
241
221
  ],
242
222
  })}
243
223
  />
244
224
  )
245
225
 
246
- fireEvent.click(screen.getByRole('button', { name: 'Open PDF viewer' }))
247
- expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
248
- expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
226
+ expect(container.firstChild).toHaveClass('str-chat__message--me')
249
227
  })
250
228
 
251
- it('renders unknown file as a link with no viewer button', () => {
252
- renderWithProviders(
229
+ it('applies the --other class for messages from other users', () => {
230
+ const { container } = renderWithProviders(
253
231
  <MediaMessage
232
+ isMyMessage={false}
254
233
  message={msg({
255
234
  attachments: [
256
235
  {
257
- type: 'file',
258
- asset_url: 'https://cdn.example.com/data.bin',
259
- mime_type: 'application/octet-stream',
260
- title: 'Unknown File',
236
+ type: 'image',
237
+ image_url: 'https://cdn.example.com/photo.jpg',
238
+ mime_type: 'image/jpeg',
261
239
  },
262
240
  ],
263
241
  })}
264
242
  />
265
243
  )
266
244
 
267
- // No viewer button for unknown files
268
- expect(screen.queryByRole('button', { name: 'Open PDF viewer' })).not.toBeInTheDocument()
269
- // Thumbnail is a plain link (opens in new tab)
270
- const links = screen.getAllByRole('link')
271
- expect(links.some((l) => l.getAttribute('href') === 'https://cdn.example.com/data.bin')).toBe(true)
272
- expect(screen.getByText('Unknown File')).toBeInTheDocument()
245
+ expect(container.firstChild).toHaveClass('str-chat__message--other')
273
246
  })
274
247
 
275
- it('shows title and file size in chin', () => {
276
- renderWithProviders(
248
+ it('renders nothing when no attachments', () => {
249
+ const { container } = renderWithProviders(
250
+ <MediaMessage message={msg({ attachments: [] })} />
251
+ )
252
+
253
+ expect(container.firstChild).toBeNull()
254
+ })
255
+
256
+ it('uses dark card background for sent (Creator) messages', () => {
257
+ const { container } = renderWithProviders(
277
258
  <MediaMessage
259
+ isMyMessage={true}
278
260
  message={msg({
279
261
  attachments: [
280
262
  {
281
- type: 'video',
282
- asset_url: 'https://cdn.example.com/clip.mp4',
283
- mime_type: 'video/mp4',
284
- title: 'Clip',
285
- file_size: 2048,
263
+ type: 'image',
264
+ image_url: 'https://cdn.example.com/photo.jpg',
265
+ mime_type: 'image/jpeg',
286
266
  },
287
267
  ],
288
268
  })}
289
269
  />
290
270
  )
291
271
 
292
- expect(screen.getByText('Clip')).toBeInTheDocument()
293
- expect(screen.getByText('2.0 KB')).toBeInTheDocument()
272
+ expect(container.querySelector('.bg-\\[\\#121110\\]')).toBeInTheDocument()
294
273
  })
295
274
 
296
- it('renders Avatar for a message from another user', () => {
297
- renderWithProviders(
275
+ it('uses light card background for received (Visitor) messages', () => {
276
+ const { container } = renderWithProviders(
298
277
  <MediaMessage
299
278
  isMyMessage={false}
300
279
  message={msg({
@@ -309,31 +288,30 @@ describe('MediaMessage', () => {
309
288
  />
310
289
  )
311
290
 
312
- expect(screen.getByTestId('avatar')).toBeInTheDocument()
313
- expect(screen.getByTestId('avatar')).toHaveAttribute('data-user-id', 'user-1')
291
+ expect(container.querySelector('.bg-white')).toBeInTheDocument()
314
292
  })
315
293
 
316
- it('does not render Avatar for own messages', () => {
294
+ it('shows Download action for received (Visitor) image attachment', () => {
317
295
  renderWithProviders(
318
296
  <MediaMessage
319
- isMyMessage={true}
320
297
  message={msg({
321
298
  attachments: [
322
299
  {
323
300
  type: 'image',
324
301
  image_url: 'https://cdn.example.com/photo.jpg',
325
302
  mime_type: 'image/jpeg',
303
+ title: 'My Photo',
326
304
  },
327
305
  ],
328
306
  })}
329
307
  />
330
308
  )
331
309
 
332
- expect(screen.queryByTestId('avatar')).not.toBeInTheDocument()
310
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
333
311
  })
334
312
 
335
- it('applies the --me class for own messages', () => {
336
- const { container } = renderWithProviders(
313
+ it('does not show literal placeholder title for sent image without title', () => {
314
+ renderWithProviders(
337
315
  <MediaMessage
338
316
  isMyMessage={true}
339
317
  message={msg({
@@ -348,34 +326,46 @@ describe('MediaMessage', () => {
348
326
  />
349
327
  )
350
328
 
351
- expect(container.firstChild).toHaveClass('str-chat__message--me')
329
+ expect(screen.queryByText('Attachment title')).not.toBeInTheDocument()
352
330
  })
353
331
 
354
- it('applies the --other class for messages from other users', () => {
355
- const { container } = renderWithProviders(
332
+ it('does not show Download button for sent (Creator) image attachment', () => {
333
+ renderWithProviders(
356
334
  <MediaMessage
357
- isMyMessage={false}
335
+ isMyMessage={true}
358
336
  message={msg({
359
337
  attachments: [
360
338
  {
361
339
  type: 'image',
362
340
  image_url: 'https://cdn.example.com/photo.jpg',
363
341
  mime_type: 'image/jpeg',
342
+ title: 'My Photo',
364
343
  },
365
344
  ],
366
345
  })}
367
346
  />
368
347
  )
369
348
 
370
- expect(container.firstChild).toHaveClass('str-chat__message--other')
349
+ expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
371
350
  })
372
351
 
373
- it('renders nothing when no attachments', () => {
374
- const { container } = renderWithProviders(
375
- <MediaMessage message={msg({ attachments: [] })} />
352
+ it('shows Download action for received audio attachment', () => {
353
+ renderWithProviders(
354
+ <MediaMessage
355
+ message={msg({
356
+ attachments: [
357
+ {
358
+ type: 'audio',
359
+ asset_url: 'https://cdn.example.com/track.mp3',
360
+ mime_type: 'audio/mpeg',
361
+ title: 'My Track',
362
+ },
363
+ ],
364
+ })}
365
+ />
376
366
  )
377
367
 
378
- expect(container.firstChild).toBeNull()
368
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
379
369
  })
380
370
 
381
371
  it('renders a link card for a link attachment', () => {
@@ -423,98 +413,106 @@ describe('MediaMessage', () => {
423
413
  expect(screen.getByText('My Linktree')).toBeInTheDocument()
424
414
  })
425
415
 
426
- it('uses dark card background for sent messages', () => {
427
- const { container } = renderWithProviders(
416
+ it('does not show download or expand buttons for link cards', () => {
417
+ renderWithProviders(
428
418
  <MediaMessage
429
- isMyMessage={true}
430
419
  message={msg({
431
420
  attachments: [
432
421
  {
433
- type: 'image',
434
- image_url: 'https://cdn.example.com/photo.jpg',
435
- mime_type: 'image/jpeg',
422
+ type: 'link',
423
+ og_scrape_url: 'https://linktr.ee/someone',
424
+ title: 'My Linktree',
436
425
  },
437
426
  ],
438
427
  })}
439
428
  />
440
429
  )
441
430
 
442
- expect(container.querySelector('.bg-\\[\\#121110\\]')).toBeInTheDocument()
431
+ expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
432
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
443
433
  })
444
434
 
445
- it('uses light card background for received messages', () => {
435
+ it('MediaMessage.Visitor renders card without chat shell', () => {
446
436
  const { container } = renderWithProviders(
447
- <MediaMessage
448
- isMyMessage={false}
437
+ <MediaMessage.Visitor
449
438
  message={msg({
450
439
  attachments: [
451
440
  {
452
441
  type: 'image',
453
442
  image_url: 'https://cdn.example.com/photo.jpg',
454
443
  mime_type: 'image/jpeg',
444
+ title: 'Solo',
455
445
  },
456
446
  ],
457
447
  })}
458
448
  />
459
449
  )
460
450
 
461
- expect(container.querySelector('.bg-\\[\\#F3F3F1\\]')).toBeInTheDocument()
451
+ expect(container.querySelector('.str-chat__message')).not.toBeInTheDocument()
452
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
462
453
  })
463
454
 
464
- it('shows download button for image attachment', () => {
465
- renderWithProviders(
466
- <MediaMessage
455
+ it('MediaMessage.Creator renders card without chat shell', () => {
456
+ const { container } = renderWithProviders(
457
+ <MediaMessage.Creator
467
458
  message={msg({
468
459
  attachments: [
469
460
  {
470
461
  type: 'image',
471
462
  image_url: 'https://cdn.example.com/photo.jpg',
472
463
  mime_type: 'image/jpeg',
473
- title: 'My Photo',
474
464
  },
475
465
  ],
476
466
  })}
477
467
  />
478
468
  )
479
469
 
480
- expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
470
+ expect(container.querySelector('.str-chat__message')).not.toBeInTheDocument()
471
+ expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
481
472
  })
482
473
 
483
- it('shows download button for audio attachment', () => {
484
- renderWithProviders(
485
- <MediaMessage
486
- message={msg({
487
- attachments: [
488
- {
489
- type: 'audio',
490
- asset_url: 'https://cdn.example.com/track.mp3',
491
- mime_type: 'audio/mpeg',
492
- title: 'My Track',
493
- },
494
- ],
495
- })}
496
- />
497
- )
498
-
499
- expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
474
+ it('resolveLinkAttachment ignores empty-string og_scrape_url without asset_url', () => {
475
+ const message = msg({
476
+ attachments: [
477
+ {
478
+ type: 'image',
479
+ image_url: 'https://cdn.example.com/photo.jpg',
480
+ mime_type: 'image/jpeg',
481
+ og_scrape_url: '',
482
+ },
483
+ ],
484
+ })
485
+ expect(resolveLinkAttachment(message)).toBeUndefined()
500
486
  })
501
487
 
502
- it('does not show download or expand buttons for link cards', () => {
488
+ it('Download button triggers fetch and uses fallback on failure', async () => {
489
+ const fetchSpy = vi.spyOn(global, 'fetch').mockRejectedValue(new Error('fail'))
490
+ const openSpy = vi.spyOn(window, 'open').mockReturnValue({
491
+ location: { href: '' },
492
+ close: vi.fn(),
493
+ } as unknown as Window)
494
+
503
495
  renderWithProviders(
504
496
  <MediaMessage
505
497
  message={msg({
506
498
  attachments: [
507
499
  {
508
- type: 'link',
509
- og_scrape_url: 'https://linktr.ee/someone',
510
- title: 'My Linktree',
500
+ type: 'image',
501
+ image_url: 'https://cdn.example.com/photo.jpg',
502
+ mime_type: 'image/jpeg',
503
+ title: 'My Photo',
511
504
  },
512
505
  ],
513
506
  })}
514
507
  />
515
508
  )
516
509
 
517
- expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
518
- expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
510
+ fireEvent.click(screen.getByRole('button', { name: 'Download' }))
511
+ await vi.waitFor(() => {
512
+ expect(fetchSpy).toHaveBeenCalled()
513
+ })
514
+
515
+ fetchSpy.mockRestore()
516
+ openSpy.mockRestore()
519
517
  })
520
518
  })