Skip to content

Commit 36bb8be

Browse files
committed
frontend: add tests for RecoveryDataComponent
1 parent cea5cfb commit 36bb8be

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { render, screen, fireEvent, waitFor, within } from '@testing-library/preact';
2+
import { describe, it, expect, vi, beforeEach } from 'vitest';
3+
import { signal } from '@preact/signals';
4+
5+
// Use dynamic import so we can spy on named exports from the module
6+
const importComponent = () => import('../RecoveryDataComponent');
7+
8+
describe('RecoveryDataComponent', () => {
9+
beforeEach(() => {
10+
vi.clearAllMocks();
11+
});
12+
13+
it('renders modal content when shown and triggers save', async () => {
14+
const mod = await importComponent();
15+
const { RecoveryDataComponent } = mod;
16+
17+
const show = signal(true);
18+
const email = '[email protected]';
19+
const secret = new Uint8Array([1, 2, 3]);
20+
21+
render(<RecoveryDataComponent email={email} secret={secret} show={show} />);
22+
23+
expect(screen.getByTestId('modal-title').textContent).toBe('save_recovery_data');
24+
expect(screen.getByTestId('modal-body').textContent).toContain('save_recovery_data_text');
25+
const footer = screen.getByTestId('modal-footer');
26+
const closeButton = within(footer).getByRole('button', { name: 'close' });
27+
28+
expect(closeButton.className).toContain('btn-danger');
29+
30+
const saveButton = screen.getByRole('button', { name: 'save' });
31+
fireEvent.click(saveButton);
32+
33+
await waitFor(() => {
34+
expect(closeButton.className).toContain('btn-primary');
35+
});
36+
37+
fireEvent.click(closeButton);
38+
expect(show.value).toBe(false);
39+
});
40+
41+
it('calls onHide and navigates on modal close', async () => {
42+
const { RecoveryDataComponent } = await importComponent();
43+
44+
const show = signal(true);
45+
46+
render(<RecoveryDataComponent email={'[email protected]'} secret={new Uint8Array()} show={show} />);
47+
48+
const closeButtons = screen.getAllByTestId('modal-close');
49+
const bottomClose = closeButtons[closeButtons.length - 1];
50+
fireEvent.click(bottomClose);
51+
52+
await waitFor(() => {
53+
expect(show.value).toBe(false);
54+
expect(window.location.replace).toHaveBeenCalledWith('/');
55+
});
56+
});
57+
});
58+
59+
describe('saveRecoveryData', () => {
60+
beforeEach(() => {
61+
vi.clearAllMocks();
62+
});
63+
64+
it('creates a downloadable backup file and revokes URL', async () => {
65+
const { saveRecoveryData } = await importComponent();
66+
67+
const email = '[email protected]';
68+
const secret = new Uint8Array([9, 8, 7, 6]);
69+
70+
const createUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:test-url');
71+
const revokeUrl = vi.spyOn(URL, 'revokeObjectURL');
72+
73+
const clickSpy = vi
74+
.spyOn(HTMLAnchorElement.prototype as unknown as { click: () => void }, 'click')
75+
.mockImplementation(function (this: HTMLAnchorElement) {
76+
expect(this.download).toBe('john_doe_at_example_com_my_warp_charger_com_recovery_data');
77+
});
78+
79+
const hashBytes = new Uint8Array([1, 2, 3, 4]).buffer;
80+
(window.crypto.subtle.digest as unknown as (algo: string, data: ArrayBufferView) => Promise<ArrayBuffer>) =
81+
vi.fn().mockResolvedValue(hashBytes);
82+
83+
await saveRecoveryData(secret, email);
84+
85+
expect(createUrl).toHaveBeenCalledTimes(1);
86+
expect(clickSpy).toHaveBeenCalledTimes(1);
87+
expect(revokeUrl).toHaveBeenCalledWith('blob:test-url');
88+
89+
expect(window.crypto.subtle.digest).toHaveBeenCalledWith('SHA-256', expect.anything());
90+
});
91+
});

frontend/src/test-setup.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,12 +498,47 @@ beforeAll(() => {
498498
Object.defineProperty(window, 'location', {
499499
value: {
500500
reload: vi.fn(),
501+
replace: vi.fn(),
501502
href: 'http://localhost:3000',
502503
},
503504
writable: true,
504505
});
505506

506507
window.scrollTo = vi.fn();
508+
509+
// Provide minimal Web Crypto mock used by components
510+
if (!('crypto' in window)) {
511+
// @ts-expect-error - define minimal crypto
512+
window.crypto = {};
513+
}
514+
if (!('subtle' in window.crypto)) {
515+
// @ts-expect-error - define minimal subtle
516+
window.crypto.subtle = { digest: vi.fn().mockResolvedValue(new ArrayBuffer(0)) };
517+
}
518+
519+
// URL.createObjectURL / revokeObjectURL in jsdom
520+
if (!('createObjectURL' in URL)) {
521+
// @ts-expect-error - add createObjectURL
522+
URL.createObjectURL = vi.fn(() => 'blob:mock');
523+
}
524+
if (!('revokeObjectURL' in URL)) {
525+
// @ts-expect-error - add revokeObjectURL
526+
URL.revokeObjectURL = vi.fn();
527+
}
528+
529+
// File polyfill for environments missing it
530+
if (typeof (globalThis as unknown as { File?: unknown }).File === 'undefined') {
531+
class PolyfillFile extends Blob {
532+
name: string;
533+
lastModified: number;
534+
constructor(bits: BlobPart[], name: string, options?: FilePropertyBag) {
535+
super(bits, options);
536+
this.name = name;
537+
this.lastModified = Date.now();
538+
}
539+
}
540+
(globalThis as unknown as { File: unknown }).File = PolyfillFile;
541+
}
507542
});
508543

509544
vi.mock('./components/Alert', () => ({

0 commit comments

Comments
 (0)