Skip to content

Commit be24d09

Browse files
committed
frontend: add tests for indext.tsx
1 parent 66f07f9 commit be24d09

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

frontend/src/index.test.tsx

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { render, screen } from '@testing-library/preact';
3+
import * as utils from './utils.js';
4+
import Median from 'median-js-bridge';
5+
6+
// Default: stub preact.render to avoid module-level mount; tests re-do this before importing index as needed
7+
vi.mock('preact', async (importOriginal) => {
8+
const actual = await importOriginal<typeof import('preact')>();
9+
return { ...actual, render: vi.fn() };
10+
});
11+
12+
// Silence console noise from iframe warning in App
13+
vi.spyOn(console, 'warn').mockImplementation(() => {});
14+
15+
describe('index.tsx', () => {
16+
beforeEach(() => {
17+
// Clean up spies between tests
18+
vi.restoreAllMocks();
19+
// Re-silence console.warn after restore
20+
vi.spyOn(console, 'warn').mockImplementation(() => {});
21+
22+
// Default state and environment
23+
(utils.loggedIn as { value: number }).value = utils.AppState.LoggedOut;
24+
// @ts-expect-error test flag for early-return guard in App
25+
window.ServiceWorker = {};
26+
27+
// Ensure container exists
28+
let app = document.getElementById('app');
29+
if (!app) {
30+
app = document.createElement('div');
31+
app.id = 'app';
32+
document.body.appendChild(app);
33+
}
34+
});
35+
36+
it('shows a message when ServiceWorker is missing', async () => {
37+
// Remove ServiceWorker to hit the early return branch
38+
// @ts-expect-error test override
39+
delete window.ServiceWorker;
40+
41+
vi.resetModules();
42+
vi.doMock('preact', async (importOriginal) => {
43+
const actual = await importOriginal<typeof import('preact')>();
44+
return { ...actual, render: vi.fn() };
45+
});
46+
const { App } = await import('./index');
47+
render(<App />);
48+
expect(screen.getByText('no_service_worker')).toBeInTheDocument();
49+
});
50+
51+
it('renders logged in view with navbar and router', async () => {
52+
(utils.loggedIn as { value: number }).value = utils.AppState.LoggedIn;
53+
54+
vi.resetModules();
55+
vi.doMock('preact', async (importOriginal) => {
56+
const actual = await importOriginal<typeof import('preact')>();
57+
return { ...actual, render: vi.fn() };
58+
});
59+
const { App } = await import('./index');
60+
render(<App />);
61+
expect(screen.getByRole('navigation')).toBeInTheDocument();
62+
});
63+
64+
it('sets favicon href on load', async () => {
65+
const link = document.createElement('link');
66+
link.setAttribute('rel', 'icon');
67+
document.head.appendChild(link);
68+
69+
vi.resetModules();
70+
vi.doMock('preact', async (importOriginal) => {
71+
const actual = await importOriginal<typeof import('preact')>();
72+
return { ...actual, render: vi.fn() };
73+
});
74+
await import('./index');
75+
76+
expect((link as HTMLLinkElement).href).toContain('favicon.png');
77+
});
78+
79+
it('registers unhandledrejection handler in debug mode and uses Median.share.downloadFile', async () => {
80+
(utils.isDebugMode as { value: boolean }).value = true;
81+
82+
vi.resetModules();
83+
vi.doMock('preact', async (importOriginal) => {
84+
const actual = await importOriginal<typeof import('preact')>();
85+
return { ...actual, render: vi.fn() };
86+
});
87+
await import('./index');
88+
89+
const nativeSpy = vi.spyOn(Median, 'isNativeApp').mockReturnValue(true);
90+
const evt = new Event('unhandledrejection');
91+
(evt as unknown as { reason?: unknown }).reason = { message: 'boom', stack: 'boom\nstack' };
92+
window.dispatchEvent(evt);
93+
94+
expect(Median.share.downloadFile).toHaveBeenCalled();
95+
nativeSpy.mockRestore();
96+
});
97+
98+
it('migrates secret key to service worker and shows loading state', async () => {
99+
(utils.loggedIn as { value: number }).value = utils.AppState.Loading;
100+
localStorage.setItem('secretKey', 'abc');
101+
// Ensure a controller exists for postMessage path
102+
// @ts-expect-error extend navigator
103+
navigator.serviceWorker = { controller: { postMessage: vi.fn() } };
104+
// Ensure ServiceWorker is present to avoid early return branch
105+
// @ts-expect-error define presence flag
106+
window.ServiceWorker = {};
107+
// Avoid network calls from version checker
108+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('{}', { status: 200 }));
109+
110+
vi.resetModules();
111+
vi.doMock('preact', async (importOriginal) => {
112+
const actual = await importOriginal<typeof import('preact')>();
113+
return { ...actual, render: vi.fn() };
114+
});
115+
const { App } = await import('./index');
116+
render(<App />);
117+
118+
expect(window.localStorage.getItem('secretKey')).toBeNull();
119+
expect(screen.getByText('Loading...')).toBeInTheDocument();
120+
});
121+
122+
it('renders Recovery state without Footer when running in native app', async () => {
123+
(utils.loggedIn as { value: number }).value = utils.AppState.Recovery;
124+
// Ensure ServiceWorker is present to avoid early return
125+
// @ts-expect-error define presence flag
126+
window.ServiceWorker = {};
127+
// Force native app so Footer is hidden in Recovery view
128+
const medianModule = await import('median-js-bridge');
129+
const nativeSpy = vi.spyOn(medianModule.default, 'isNativeApp').mockReturnValue(true);
130+
131+
vi.resetModules();
132+
vi.doMock('preact', async (importOriginal) => {
133+
const actual = await importOriginal<typeof import('preact')>();
134+
return { ...actual, render: vi.fn() };
135+
});
136+
const { App } = await import('./index');
137+
render(<App />);
138+
139+
expect(screen.getByTestId('error-alert')).toBeInTheDocument();
140+
expect(document.getElementById('footer')).toBeFalsy();
141+
142+
nativeSpy.mockRestore();
143+
});
144+
145+
it('resets document title on Router onRouteChange in LoggedIn state', async () => {
146+
(utils.loggedIn as { value: number }).value = utils.AppState.LoggedIn;
147+
// Ensure ServiceWorker is present to avoid early return
148+
// @ts-expect-error define presence flag
149+
window.ServiceWorker = {};
150+
document.title = 'not_app_name';
151+
152+
vi.resetModules();
153+
// Mock Navbar to set connected=true so the first title-setting effect doesn't run
154+
vi.doMock('./components/Navbar.js', () => ({
155+
connected: { value: true },
156+
CustomNavbar: () => <div data-testid="navbar" />,
157+
}));
158+
// Mock preact render
159+
vi.doMock('preact', async (importOriginal) => {
160+
const actual = await importOriginal<typeof import('preact')>();
161+
return { ...actual, render: vi.fn() };
162+
});
163+
// Mock Router to immediately invoke onRouteChange once
164+
vi.doMock('preact-iso', () => ({
165+
LocationProvider: ({ children }: { children?: any }) => <div>{children}</div>,
166+
Router: ({ children, onRouteChange }: { children?: any; onRouteChange?: () => void }) => {
167+
onRouteChange && onRouteChange();
168+
return <div>{children}</div>;
169+
},
170+
Route: ({ children }: { children?: any }) => <div>{children}</div>,
171+
useLocation: () => ({ route: vi.fn(), url: '/' }),
172+
useRoute: () => ({ params: {} }),
173+
lazy: (_loader: () => Promise<unknown>) => (_props: Record<string, unknown>) => <div data-testid="lazy-component" />,
174+
}));
175+
176+
const { App } = await import('./index');
177+
render(<App />);
178+
179+
expect(document.title).toBe('app_name');
180+
});
181+
});

frontend/src/test-setup.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ vi.mock('react-i18next', () => ({
337337
return key;
338338
},
339339
}),
340+
Trans: ({ children }: { children?: ComponentChildren }) => h('span', {}, children),
340341
}));
341342

342343
// Mock libsodium-wrappers
@@ -394,21 +395,62 @@ vi.mock('./utils', () => ({
394395
resetSecret: vi.fn(),
395396
clearSecretKeyFromServiceWorker: vi.fn().mockResolvedValue(undefined),
396397
FRONTEND_URL: '',
398+
refresh_access_token: vi.fn(),
399+
startVersionChecking: vi.fn(),
397400
}));
398401

402+
// Some files import from './utils.js' (with extension) — provide identical mock for that path
403+
vi.mock('./utils.js', () => ({
404+
fetchClient: {
405+
GET: vi.fn(),
406+
POST: vi.fn(),
407+
PUT: vi.fn(),
408+
DELETE: vi.fn(),
409+
},
410+
get_decrypted_secret: vi.fn(),
411+
pub_key: new Uint8Array(),
412+
secret: new Uint8Array(),
413+
PASSWORD_PATTERN: /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/,
414+
generate_hash: vi.fn(),
415+
generate_random_bytes: vi.fn(),
416+
get_salt: vi.fn(),
417+
get_salt_for_user: vi.fn(),
418+
concat_salts: vi.fn(),
419+
AppState: { Loading: 0, LoggedIn: 1, LoggedOut: 2, Recovery: 3 },
420+
loggedIn: { value: 0 },
421+
storeSecretKeyInServiceWorker: vi.fn(),
422+
bc: { postMessage: vi.fn() },
423+
isDebugMode: { value: false },
424+
resetSecret: vi.fn(),
425+
clearSecretKeyFromServiceWorker: vi.fn().mockResolvedValue(undefined),
426+
FRONTEND_URL: '',
427+
refresh_access_token: vi.fn(),
428+
startVersionChecking: vi.fn(),
429+
}));
430+
431+
// Assets aliases used in index.tsx
432+
vi.mock('logo', () => ({ default: 'logo.png' }));
433+
vi.mock('favicon', () => ({ default: '/favicon.png' }));
434+
399435
// Mock preact-iso
400436
vi.mock('preact-iso', () => ({
401437
useLocation: () => ({
402438
route: vi.fn(),
403439
url: '/',
404440
}),
441+
useRoute: () => ({ params: {} }),
442+
LocationProvider: ({ children }: { children?: ComponentChildren }) => h('div', {}, children),
443+
Router: ({ children }: { children?: ComponentChildren }) => h('div', {}, children),
444+
Route: ({ children }: { children?: ComponentChildren }) => h('div', {}, children),
445+
lazy: (_loader: () => Promise<unknown>) => (props: Record<string, unknown>) => h('div', { 'data-testid': 'lazy-component', ...props }),
405446
}));
406447

407448
// Mock median-js-bridge
408449
vi.mock('median-js-bridge', () => ({
409450
default: {
410451
isNativeApp: () => false,
411452
sidebar: { setItems: vi.fn() },
453+
share: { downloadFile: vi.fn() },
412454
},
413455
}));
414456

@@ -549,6 +591,22 @@ beforeAll(() => {
549591
}
550592
});
551593

594+
// Provide a consistent mock for modules importing './components/Alert'
552595
vi.mock('./components/Alert', () => ({
553596
showAlert: vi.fn(),
597+
ErrorAlert: () => h('div', { 'data-testid': 'error-alert' }),
598+
}));
599+
600+
// Direct import path mocks for react-bootstrap components used by index.tsx
601+
vi.mock('react-bootstrap/Row', () => ({
602+
default: ({ children, ...props }: MockComponentProps) => h('div', { ...props, className: 'row' }, children),
603+
}));
604+
vi.mock('react-bootstrap/Card', () => ({
605+
default: ({ children, ...props }: MockComponentProps) => h('div', { ...props, className: 'card' }, children),
606+
}));
607+
vi.mock('react-bootstrap/Tabs', () => ({
608+
default: ({ children, ...props }: MockComponentProps) => h('div', { ...props, className: 'tabs' }, children),
609+
}));
610+
vi.mock('react-bootstrap/Tab', () => ({
611+
default: ({ children, ...props }: MockComponentProps) => h('div', { ...props, className: 'tab-pane' }, children),
554612
}));

0 commit comments

Comments
 (0)