@linktr.ee/messaging-react 4.1.7-rc-1787240421 → 4.1.7-rc-1787247439

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.
@@ -25,6 +25,14 @@ describe('formatFileSize', () => {
25
25
  expect(formatFileSize(Infinity)).toBe('')
26
26
  expect(formatFileSize(-1)).toBe('')
27
27
  })
28
+
29
+ it('handles unit boundaries without crossing into the next unit early', () => {
30
+ expect(formatFileSize(1023)).toBe('1023 B')
31
+ expect(formatFileSize(1024 * 1024 - 1)).toBe('1024.0 KB')
32
+ expect(formatFileSize(1024 * 1024)).toBe('1.00 MB')
33
+ expect(formatFileSize(1024 ** 3 - 1)).toBe('1024.00 MB')
34
+ expect(formatFileSize(1024 ** 3)).toBe('1.00 GB')
35
+ })
28
36
  })
29
37
 
30
38
  describe('getFileExtensionLabel', () => {
@@ -43,6 +51,18 @@ describe('getFileExtensionLabel', () => {
43
51
  expect(getFileExtensionLabel('application/octet-stream')).toBeUndefined()
44
52
  expect(getFileExtensionLabel()).toBeUndefined()
45
53
  })
54
+
55
+ it('handles dot boundaries, extension length, and MIME fallbacks', () => {
56
+ expect(getFileExtensionLabel('text/plain', '.gitignore')).toBe('TXT')
57
+ expect(getFileExtensionLabel('text/plain', 'notes.')).toBe('TXT')
58
+ expect(getFileExtensionLabel('image/webp', 'photo.webp')).toBe('WEBP')
59
+ expect(getFileExtensionLabel('image/webp', 'photo.abcdef')).toBe('WEBP')
60
+ expect(getFileExtensionLabel('application/json', 'data.12345')).toBe(
61
+ '12345'
62
+ )
63
+ expect(getFileExtensionLabel('image/*')).toBeUndefined()
64
+ expect(getFileExtensionLabel('application/octet-stream')).toBeUndefined()
65
+ })
46
66
  })
47
67
 
48
68
  describe('buildCompactMetaLabel', () => {
@@ -59,6 +79,23 @@ describe('buildCompactMetaLabel', () => {
59
79
  it('returns undefined when nothing is renderable', () => {
60
80
  expect(buildCompactMetaLabel()).toBeUndefined()
61
81
  })
82
+
83
+ it('drops non-positive and non-number sizes while retaining size-only labels', () => {
84
+ expect(buildCompactMetaLabel('application/pdf', 'report.pdf', 0)).toBe(
85
+ 'PDF'
86
+ )
87
+ expect(buildCompactMetaLabel('application/pdf', 'report.pdf', -1)).toBe(
88
+ 'PDF'
89
+ )
90
+ expect(buildCompactMetaLabel(undefined, undefined, 1024)).toBe('1.0 KB')
91
+ expect(
92
+ buildCompactMetaLabel(
93
+ 'application/octet-stream',
94
+ undefined,
95
+ '1024' as never
96
+ )
97
+ ).toBeUndefined()
98
+ })
62
99
  })
63
100
 
64
101
  describe('filenameFromUrl', () => {
@@ -77,4 +114,11 @@ describe('filenameFromUrl', () => {
77
114
  it('falls back to a default when the URL cannot be parsed', () => {
78
115
  expect(filenameFromUrl('not a url at all')).toBe('download')
79
116
  })
117
+
118
+ it('handles trailing slashes, deep paths, and query strings', () => {
119
+ expect(filenameFromUrl('https://cdn.example.com/folder/')).toBe('download')
120
+ expect(
121
+ filenameFromUrl('https://cdn.example.com/a/b/c/report.pdf?download=1')
122
+ ).toBe('report.pdf')
123
+ })
80
124
  })
@@ -0,0 +1,113 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { triggerDownload } from './triggerDownload'
4
+
5
+ describe('triggerDownload', () => {
6
+ const fetchMock = vi.fn()
7
+ const openMock = vi.fn()
8
+ const createObjectURLMock = vi.fn()
9
+ const revokeObjectURLMock = vi.fn()
10
+ const clickMock = vi
11
+ .spyOn(HTMLAnchorElement.prototype, 'click')
12
+ .mockImplementation(() => undefined)
13
+
14
+ beforeEach(() => {
15
+ vi.stubGlobal('fetch', fetchMock)
16
+ vi.spyOn(window, 'open').mockImplementation(openMock)
17
+ Object.defineProperty(URL, 'createObjectURL', {
18
+ configurable: true,
19
+ value: createObjectURLMock,
20
+ })
21
+ Object.defineProperty(URL, 'revokeObjectURL', {
22
+ configurable: true,
23
+ value: revokeObjectURLMock,
24
+ })
25
+ fetchMock.mockReset()
26
+ openMock.mockReset()
27
+ createObjectURLMock.mockReset()
28
+ revokeObjectURLMock.mockReset()
29
+ clickMock.mockClear()
30
+ })
31
+
32
+ it('downloads a successful response with the supplied filename', async () => {
33
+ const blob = new Blob(['file'])
34
+ fetchMock.mockResolvedValue({
35
+ ok: true,
36
+ blob: () => Promise.resolve(blob),
37
+ })
38
+ createObjectURLMock.mockReturnValue('blob:attachment')
39
+ const appendSpy = vi.spyOn(document.body, 'appendChild')
40
+ const removeSpy = vi.spyOn(document.body, 'removeChild')
41
+
42
+ await expect(
43
+ triggerDownload('https://cdn.example.com/file.pdf', 'report.pdf')
44
+ ).resolves.toBeUndefined()
45
+
46
+ expect(fetchMock).toHaveBeenCalledWith('https://cdn.example.com/file.pdf', {
47
+ mode: 'cors',
48
+ })
49
+ expect(createObjectURLMock).toHaveBeenCalledWith(blob)
50
+ expect(clickMock).toHaveBeenCalledOnce()
51
+ expect(revokeObjectURLMock).toHaveBeenCalledWith('blob:attachment')
52
+ const anchor = appendSpy.mock.calls.at(-1)?.[0] as HTMLAnchorElement
53
+ expect(anchor.href).toBe('blob:attachment')
54
+ expect(anchor.download).toBe('report.pdf')
55
+ expect(anchor.style.display).toBe('none')
56
+ expect(removeSpy).toHaveBeenCalledWith(anchor)
57
+ })
58
+
59
+ it('uses the URL filename when no filename is supplied', async () => {
60
+ fetchMock.mockResolvedValue({
61
+ ok: true,
62
+ blob: () => Promise.resolve(new Blob()),
63
+ })
64
+ createObjectURLMock.mockReturnValue('blob:attachment')
65
+ const appendSpy = vi.spyOn(document.body, 'appendChild')
66
+
67
+ await triggerDownload('https://cdn.example.com/a/My%20file.pdf')
68
+
69
+ expect(
70
+ (appendSpy.mock.calls.at(-1)?.[0] as HTMLAnchorElement).download
71
+ ).toBe('My file.pdf')
72
+ })
73
+
74
+ it.each(['a non-ok response', 'a rejected fetch'])(
75
+ 'falls back to a new window for %s',
76
+ async (label) => {
77
+ if (label === 'a non-ok response') {
78
+ fetchMock.mockResolvedValue({ ok: false, status: 404 })
79
+ } else {
80
+ fetchMock.mockRejectedValue(new Error('offline'))
81
+ }
82
+ openMock.mockReturnValue({})
83
+
84
+ await expect(
85
+ triggerDownload('https://cdn.example.com/file.pdf')
86
+ ).resolves.toBeUndefined()
87
+
88
+ expect(openMock).toHaveBeenCalledWith(
89
+ 'https://cdn.example.com/file.pdf',
90
+ '_blank',
91
+ 'noopener,noreferrer'
92
+ )
93
+ }
94
+ )
95
+
96
+ it('uses a raw-url anchor when the popup is blocked', async () => {
97
+ fetchMock.mockRejectedValue(new Error('offline'))
98
+ openMock.mockReturnValue(null)
99
+ const appendSpy = vi.spyOn(document.body, 'appendChild')
100
+ const removeSpy = vi.spyOn(document.body, 'removeChild')
101
+
102
+ await expect(
103
+ triggerDownload('https://cdn.example.com/file.pdf')
104
+ ).resolves.toBeUndefined()
105
+
106
+ const anchor = appendSpy.mock.calls.at(-1)?.[0] as HTMLAnchorElement
107
+ expect(anchor.href).toBe('https://cdn.example.com/file.pdf')
108
+ expect(anchor.download).toBe('file.pdf')
109
+ expect(anchor.target).toBe('_blank')
110
+ expect(anchor.rel).toBe('noopener noreferrer')
111
+ expect(removeSpy).toHaveBeenCalledWith(anchor)
112
+ })
113
+ })
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
4
+
5
+ import { useCarousel } from './useCarousel'
6
+
7
+ const Harness = ({
8
+ open,
9
+ length,
10
+ initialIndex = 0,
11
+ }: {
12
+ open: boolean
13
+ length: number
14
+ initialIndex?: number
15
+ }) => {
16
+ const { index, prev, next } = useCarousel({ open, length, initialIndex })
17
+ return (
18
+ <div>
19
+ <span data-testid="index">{index}</span>
20
+ <button type="button" onClick={prev}>
21
+ Prev
22
+ </button>
23
+ <button type="button" onClick={next}>
24
+ Next
25
+ </button>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ describe('useCarousel', () => {
31
+ it('clamps initial index, wraps, and resets on reopen', () => {
32
+ const { rerender } = renderWithProviders(
33
+ <Harness open length={3} initialIndex={99} />
34
+ )
35
+ expect(screen.getByTestId('index')).toHaveTextContent('2')
36
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }))
37
+ expect(screen.getByTestId('index')).toHaveTextContent('0')
38
+ fireEvent.click(screen.getByRole('button', { name: 'Prev' }))
39
+ expect(screen.getByTestId('index')).toHaveTextContent('2')
40
+ rerender(<Harness open={false} length={3} initialIndex={1} />)
41
+ rerender(<Harness open length={3} initialIndex={1} />)
42
+ expect(screen.getByTestId('index')).toHaveTextContent('1')
43
+ })
44
+
45
+ it('handles arrows only when open with multiple items', () => {
46
+ const { rerender } = renderWithProviders(
47
+ <Harness open length={2} initialIndex={0} />
48
+ )
49
+ const right = fireEvent.keyDown(window, { key: 'ArrowRight' })
50
+ expect(screen.getByTestId('index')).toHaveTextContent('1')
51
+ expect(right).toBe(false)
52
+ for (const tagName of ['video', 'audio'] as const) {
53
+ const media = document.createElement(tagName)
54
+ media.tabIndex = 0
55
+ document.body.appendChild(media)
56
+ media.focus()
57
+ fireEvent.keyDown(window, { key: 'ArrowLeft' })
58
+ expect(screen.getByTestId('index')).toHaveTextContent('1')
59
+ media.remove()
60
+ }
61
+ rerender(<Harness open length={1} />)
62
+ fireEvent.keyDown(window, { key: 'ArrowRight' })
63
+ expect(screen.getByTestId('index')).toHaveTextContent('0')
64
+ rerender(<Harness open={false} length={2} />)
65
+ fireEvent.keyDown(window, { key: 'ArrowRight' })
66
+ expect(screen.getByTestId('index')).toHaveTextContent('0')
67
+ })
68
+ })
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { VIEWER_ACTION_CLASS, VIEWER_NAV_CLASS } from './viewerChromeClasses'
4
+
5
+ describe('viewer chrome classes', () => {
6
+ it('keeps the load-bearing action button tokens', () => {
7
+ expect(VIEWER_ACTION_CLASS).toContain('size-10')
8
+ expect(VIEWER_ACTION_CLASS).toContain('rounded-full')
9
+ })
10
+
11
+ it('keeps the load-bearing navigation button tokens', () => {
12
+ expect(VIEWER_NAV_CLASS).toContain('absolute')
13
+ expect(VIEWER_NAV_CLASS).toContain('size-12')
14
+ expect(VIEWER_NAV_CLASS).toContain('-translate-y-1/2')
15
+ })
16
+ })