Skip to content

Commit 40e6526

Browse files
test: add Go-back guard and LanguageSwitcher safeTranslate fallback tests
1 parent c131670 commit 40e6526

2 files changed

Lines changed: 132 additions & 16 deletions

File tree

src/components/navigation/LanguageSwitcher/__tests__/LanguageSwitcher.test.js

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { render, screen, waitFor } from '@testing-library/svelte';
22
import userEvent from '@testing-library/user-event';
33
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
44
import LanguageSwitcher from '../LanguageSwitcher.svelte';
5+
import { __setTranslatorMode } from 'svelte-i18n';
56

67
// Mock languages array - must be defined inline in mock factory
78
vi.mock('$lib/i18n', () => ({
@@ -20,7 +21,30 @@ let currentLocale = 'en';
2021
const localeSubscribers = [];
2122

2223
vi.mock('svelte-i18n', () => {
23-
// Create the mock locale object inside the factory
24+
let translatorMode = 'normal';
25+
const tSubscribers = [];
26+
27+
const normalT = (key, options) => {
28+
if (key === 'language_switcher.select_language') {
29+
return `Select language: ${options?.values?.language || ''}`;
30+
}
31+
if (key === 'language_switcher.available_languages') {
32+
return 'Available languages';
33+
}
34+
return key;
35+
};
36+
37+
const getTranslator = () => {
38+
if (translatorMode === 'non-function') return null;
39+
if (translatorMode === 'throws') {
40+
return () => {
41+
throw new Error('Translator error');
42+
};
43+
}
44+
if (translatorMode === 'returns-key') return (key) => key;
45+
return normalT;
46+
};
47+
2448
const mockLocale = {
2549
subscribe: vi.fn((fn) => {
2650
fn(currentLocale);
@@ -32,26 +56,26 @@ vi.mock('svelte-i18n', () => {
3256
localeSubscribers.forEach((fn) => fn(newLocale));
3357
})
3458
};
35-
// Mock t function that returns the key with interpolated values
36-
const mockT = vi.fn((key, options) => {
37-
if (key === 'language_switcher.select_language') {
38-
return `Select language: ${options?.values?.language || ''}`;
39-
}
40-
if (key === 'language_switcher.available_languages') {
41-
return 'Available languages';
42-
}
43-
return key;
44-
});
45-
// Create a mock svelte store for t
59+
4660
const mockTStore = {
4761
subscribe: vi.fn((fn) => {
48-
fn(mockT);
49-
return { unsubscribe: () => {} };
62+
tSubscribers.push(fn);
63+
fn(getTranslator());
64+
return () => {
65+
const index = tSubscribers.indexOf(fn);
66+
if (index !== -1) tSubscribers.splice(index, 1);
67+
};
5068
})
5169
};
70+
5271
return {
5372
locale: mockLocale,
54-
t: mockTStore
73+
t: mockTStore,
74+
__setTranslatorMode: (mode) => {
75+
translatorMode = mode;
76+
const translator = getTranslator();
77+
tSubscribers.forEach((fn) => fn(translator));
78+
}
5579
};
5680
});
5781

@@ -80,6 +104,7 @@ describe('LanguageSwitcher', () => {
80104

81105
currentLocale = 'en';
82106
localeSubscribers.length = 0;
107+
__setTranslatorMode('normal');
83108
mockEnvValue = {
84109
PUBLIC_LANGUAGE_SWITCHER_ENABLED: 'true',
85110
PUBLIC_LANGUAGE_SWITCHER_BUTTON_FORMAT: undefined,
@@ -572,6 +597,49 @@ describe('LanguageSwitcher', () => {
572597
});
573598
});
574599

600+
describe('safeTranslate fallbacks', () => {
601+
test('returns the raw key when $t resolves to a non-function', () => {
602+
__setTranslatorMode('non-function');
603+
render(LanguageSwitcher);
604+
605+
const button = screen.getByRole('button');
606+
// safeTranslate returns the key itself when the translator is not callable
607+
expect(button).toHaveAttribute('aria-label', 'language_switcher.select_language');
608+
});
609+
610+
test('returns the raw key and logs a warning when $t throws', async () => {
611+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
612+
__setTranslatorMode('throws');
613+
614+
const user = userEvent.setup();
615+
render(LanguageSwitcher);
616+
617+
const button = screen.getByRole('button');
618+
expect(button).toHaveAttribute('aria-label', 'language_switcher.select_language');
619+
620+
await user.click(button);
621+
expect(screen.getByRole('listbox')).toHaveAttribute(
622+
'aria-label',
623+
'language_switcher.available_languages'
624+
);
625+
626+
expect(consoleWarnSpy).toHaveBeenCalledWith(
627+
expect.stringContaining('[i18n fallback]'),
628+
'Translator error'
629+
);
630+
consoleWarnSpy.mockRestore();
631+
});
632+
633+
test('returns the raw key when the translator returns the key unchanged', () => {
634+
__setTranslatorMode('returns-key');
635+
render(LanguageSwitcher);
636+
637+
const button = screen.getByRole('button');
638+
// Translator passes the key through, so the aria-label is the raw key
639+
expect(button).toHaveAttribute('aria-label', 'language_switcher.select_language');
640+
});
641+
});
642+
575643
describe('Locale Variant Handling', () => {
576644
test('handles locale variants like en-US by extracting base code', () => {
577645
currentLocale = 'en-US';

src/routes/__tests__/error.test.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, waitFor } from '@testing-library/svelte';
1+
import { render, screen, waitFor, fireEvent } from '@testing-library/svelte';
22
import { beforeEach, describe, expect, test, vi } from 'vitest';
33
import ErrorPage from '$src/routes/+error.svelte';
44
import { locale, __setTranslatorMode } from 'svelte-i18n';
@@ -228,6 +228,54 @@ describe('ErrorPage', () => {
228228
});
229229
});
230230

231+
describe('Go back button behavior', () => {
232+
test('calls history.back() when there is browser history (length > 1)', async () => {
233+
const backSpy = vi.spyOn(window.history, 'back').mockImplementation(() => {});
234+
const lengthSpy = vi.spyOn(window.history, 'length', 'get').mockReturnValue(5);
235+
236+
render(ErrorPage);
237+
await fireEvent.click(screen.getByRole('button', { name: /Go back/i }));
238+
239+
expect(backSpy).toHaveBeenCalledOnce();
240+
241+
backSpy.mockRestore();
242+
lengthSpy.mockRestore();
243+
});
244+
245+
test('navigates to / when there is no browser history (length <= 1)', async () => {
246+
const backSpy = vi.spyOn(window.history, 'back').mockImplementation(() => {});
247+
const lengthSpy = vi.spyOn(window.history, 'length', 'get').mockReturnValue(1);
248+
249+
// Stub window.location to capture the href assignment without triggering
250+
const originalLocation = window.location;
251+
let hrefValue = originalLocation.href;
252+
Object.defineProperty(window, 'location', {
253+
configurable: true,
254+
value: {
255+
get href() {
256+
return hrefValue;
257+
},
258+
set href(v) {
259+
hrefValue = v;
260+
}
261+
}
262+
});
263+
264+
render(ErrorPage);
265+
await fireEvent.click(screen.getByRole('button', { name: /Go back/i }));
266+
267+
expect(backSpy).not.toHaveBeenCalled();
268+
expect(hrefValue).toBe('/');
269+
270+
Object.defineProperty(window, 'location', {
271+
configurable: true,
272+
value: originalLocation
273+
});
274+
backSpy.mockRestore();
275+
lengthSpy.mockRestore();
276+
});
277+
});
278+
231279
describe('status code branches', () => {
232280
test.each([
233281
[403, 'Access denied'],

0 commit comments

Comments
 (0)