Skip to content

Commit 5f6f5d1

Browse files
Merge pull request #351 from JaneliaSciComp/mkitti-binary-file-display-tests
test: add unit tests for HexDump and useFileBinaryPreviewQuery
2 parents f155038 + aa728c5 commit 5f6f5d1

2 files changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { renderHook, waitFor } from '@testing-library/react';
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4+
import { useFileBinaryPreviewQuery } from '@/queries/fileContentQueries';
5+
6+
// Simplify buildUrl so we can intercept fetch without URL matching complexity
7+
vi.mock('@/utils', async () => {
8+
const actual = await vi.importActual('@/utils');
9+
return {
10+
...actual,
11+
buildUrl: () => '/api/content/test_fsp?subpath=file.bin'
12+
};
13+
});
14+
15+
describe('useFileBinaryPreviewQuery', () => {
16+
let queryClient: QueryClient;
17+
18+
beforeEach(() => {
19+
queryClient = new QueryClient({
20+
defaultOptions: { queries: { retry: false } }
21+
});
22+
vi.clearAllMocks();
23+
});
24+
25+
const wrapper = ({ children }: { children: React.ReactNode }) => (
26+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
27+
);
28+
29+
it('returns a Uint8Array from a 206 Partial Content response', async () => {
30+
const data = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);
31+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
32+
ok: true,
33+
status: 206,
34+
arrayBuffer: async () => data.buffer
35+
} as Response);
36+
37+
const { result } = renderHook(
38+
() => useFileBinaryPreviewQuery('test_fsp', 'file.bin'),
39+
{ wrapper }
40+
);
41+
42+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
43+
expect(result.current.data).toBeInstanceOf(Uint8Array);
44+
expect(Array.from(result.current.data!)).toEqual([0x50, 0x4b, 0x03, 0x04]);
45+
});
46+
47+
it('returns a Uint8Array from a 200 OK response (server ignores Range)', async () => {
48+
const data = new Uint8Array([0x01, 0x02, 0x03]);
49+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
50+
ok: true,
51+
status: 200,
52+
arrayBuffer: async () => data.buffer
53+
} as Response);
54+
55+
const { result } = renderHook(
56+
() => useFileBinaryPreviewQuery('test_fsp', 'file.bin'),
57+
{ wrapper }
58+
);
59+
60+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
61+
expect(result.current.data).toBeInstanceOf(Uint8Array);
62+
});
63+
64+
it('sets error state when the response is not ok', async () => {
65+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
66+
ok: false,
67+
status: 403,
68+
statusText: 'Forbidden'
69+
} as Response);
70+
71+
const { result } = renderHook(
72+
() => useFileBinaryPreviewQuery('test_fsp', 'file.bin'),
73+
{ wrapper }
74+
);
75+
76+
await waitFor(() => expect(result.current.isError).toBe(true));
77+
expect(result.current.error?.message).toContain('Forbidden');
78+
});
79+
80+
it('does not fetch when enabled is false', async () => {
81+
const fetchSpy = vi.spyOn(global, 'fetch');
82+
83+
const { result } = renderHook(
84+
() => useFileBinaryPreviewQuery('test_fsp', 'file.bin', false),
85+
{ wrapper }
86+
);
87+
88+
await new Promise(r => setTimeout(r, 50));
89+
expect(fetchSpy).not.toHaveBeenCalled();
90+
expect(result.current.isPending).toBe(true);
91+
});
92+
93+
it('does not fetch when fspName is undefined', async () => {
94+
const fetchSpy = vi.spyOn(global, 'fetch');
95+
96+
const { result } = renderHook(
97+
() => useFileBinaryPreviewQuery(undefined, 'file.bin'),
98+
{ wrapper }
99+
);
100+
101+
await new Promise(r => setTimeout(r, 50));
102+
expect(fetchSpy).not.toHaveBeenCalled();
103+
expect(result.current.isPending).toBe(true);
104+
});
105+
106+
it('sends a Range header requesting the first 512 bytes', async () => {
107+
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValueOnce({
108+
ok: true,
109+
status: 206,
110+
arrayBuffer: async () => new Uint8Array(512).buffer
111+
} as Response);
112+
113+
const { result } = renderHook(
114+
() => useFileBinaryPreviewQuery('test_fsp', 'file.bin'),
115+
{ wrapper }
116+
);
117+
118+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
119+
expect(fetchSpy).toHaveBeenCalledWith(
120+
expect.any(String),
121+
expect.objectContaining({
122+
headers: expect.objectContaining({ Range: 'bytes=0-511' })
123+
})
124+
);
125+
});
126+
127+
it('includes credentials in the fetch request', async () => {
128+
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValueOnce({
129+
ok: true,
130+
status: 206,
131+
arrayBuffer: async () => new Uint8Array(4).buffer
132+
} as Response);
133+
134+
const { result } = renderHook(
135+
() => useFileBinaryPreviewQuery('test_fsp', 'file.bin'),
136+
{ wrapper }
137+
);
138+
139+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
140+
expect(fetchSpy).toHaveBeenCalledWith(
141+
expect.any(String),
142+
expect.objectContaining({ credentials: 'include' })
143+
);
144+
});
145+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import HexDump from '@/components/ui/BrowsePage/HexDump';
4+
5+
describe('HexDump', () => {
6+
it('renders offset, hex values, and ASCII representation', () => {
7+
// 0x50 0x4B = "PK" — ZIP magic bytes
8+
const bytes = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);
9+
const { container } = render(<HexDump bytes={bytes} />);
10+
const pre = container.querySelector('pre')!;
11+
12+
expect(pre.textContent).toContain('0000:');
13+
expect(pre.textContent).toContain('50');
14+
expect(pre.textContent).toContain('4B');
15+
expect(pre.textContent).toContain('PK');
16+
});
17+
18+
it('uses uppercase hex for offset and byte values', () => {
19+
const bytes = new Uint8Array([0xab, 0xcd]);
20+
const { container } = render(<HexDump bytes={bytes} />);
21+
const pre = container.querySelector('pre')!;
22+
23+
expect(pre.textContent).toContain('0000:');
24+
expect(pre.textContent).toContain('AB');
25+
expect(pre.textContent).toContain('CD');
26+
expect(pre.textContent).not.toContain('ab');
27+
});
28+
29+
it('shows byte count when totalFileSize is not provided', () => {
30+
const bytes = new Uint8Array(4).fill(0);
31+
render(<HexDump bytes={bytes} />);
32+
expect(screen.getByText('4 bytes')).toBeInTheDocument();
33+
});
34+
35+
it('shows byte count when totalFileSize equals bytes.length', () => {
36+
const bytes = new Uint8Array(4).fill(0);
37+
render(<HexDump bytes={bytes} totalFileSize={4} />);
38+
expect(screen.getByText('4 bytes')).toBeInTheDocument();
39+
expect(screen.queryByText(/Showing first/)).not.toBeInTheDocument();
40+
});
41+
42+
it('shows truncation message when file is larger than the preview', () => {
43+
const bytes = new Uint8Array(512).fill(0);
44+
render(<HexDump bytes={bytes} totalFileSize={1024} />);
45+
expect(screen.getByText(/Showing first 512 of/)).toBeInTheDocument();
46+
expect(screen.getByText(/bytes/)).toBeInTheDocument();
47+
});
48+
49+
it('replaces non-printable bytes with dots in the ASCII column', () => {
50+
// 0x01 = non-printable control char; 0x41 = 'A'; 0x42 = 'B'
51+
const bytes = new Uint8Array([0x01, 0x41, 0x42]);
52+
const { container } = render(<HexDump bytes={bytes} />);
53+
const pre = container.querySelector('pre')!;
54+
55+
expect(pre.textContent).toContain('.AB');
56+
});
57+
58+
it('replaces null bytes with dots in the ASCII column', () => {
59+
const bytes = new Uint8Array([0x00, 0x00]);
60+
const { container } = render(<HexDump bytes={bytes} />);
61+
const pre = container.querySelector('pre')!;
62+
63+
expect(pre.textContent).toContain('..');
64+
});
65+
66+
it('handles an empty byte array', () => {
67+
const bytes = new Uint8Array(0);
68+
render(<HexDump bytes={bytes} />);
69+
expect(screen.getByText('0 bytes')).toBeInTheDocument();
70+
});
71+
72+
it('inserts a mid-row gap after the 8th byte', () => {
73+
const bytes = new Uint8Array(16).fill(0xab);
74+
const { container } = render(<HexDump bytes={bytes} />);
75+
const pre = container.querySelector('pre')!;
76+
const line = pre.textContent?.split('\n')[0] ?? '';
77+
78+
// Eight hex bytes, then triple space (join + gap element + join), then the 9th byte
79+
expect(line).toContain('AB AB AB AB AB AB AB AB AB');
80+
});
81+
82+
it('produces the correct number of rows for multi-row input', () => {
83+
// 17 bytes → two rows (16 + 1)
84+
const bytes = new Uint8Array(17).fill(0);
85+
const { container } = render(<HexDump bytes={bytes} />);
86+
const pre = container.querySelector('pre')!;
87+
const lines = (pre.textContent ?? '').split('\n').filter(Boolean);
88+
89+
expect(lines).toHaveLength(2);
90+
expect(lines[0]).toContain('0000:');
91+
expect(lines[1]).toContain('0010:');
92+
});
93+
94+
it('pads the offset with leading zeros', () => {
95+
// 32 bytes → second row offset is 0x10 = 16, displayed as "0010"
96+
const bytes = new Uint8Array(32).fill(0);
97+
const { container } = render(<HexDump bytes={bytes} />);
98+
const pre = container.querySelector('pre')!;
99+
100+
expect(pre.textContent).toContain('0010:');
101+
});
102+
});

0 commit comments

Comments
 (0)