test(web): add middleware + i18n + messages test suites (GOO-60)
Cover frontend middleware auth/locale logic, i18n config/routing/request/navigation, and vi/en message parity — 91 new tests across 6 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
apps/web/i18n/__tests__/config.spec.ts
Normal file
49
apps/web/i18n/__tests__/config.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { locales, defaultLocale } from '../config';
|
||||
import type { Locale } from '../config';
|
||||
|
||||
describe('i18n/config', () => {
|
||||
describe('locales', () => {
|
||||
it('exports an array of supported locales', () => {
|
||||
expect(Array.isArray(locales)).toBe(true);
|
||||
expect(locales.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('includes Vietnamese (vi) as a supported locale', () => {
|
||||
expect(locales).toContain('vi');
|
||||
});
|
||||
|
||||
it('includes English (en) as a supported locale', () => {
|
||||
expect(locales).toContain('en');
|
||||
});
|
||||
|
||||
it('only contains vi and en', () => {
|
||||
expect([...locales]).toEqual(['vi', 'en']);
|
||||
});
|
||||
|
||||
it('is a readonly tuple', () => {
|
||||
// TypeScript const assertion — runtime check that it behaves as expected
|
||||
const copy = [...locales];
|
||||
expect(copy).toEqual(['vi', 'en']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultLocale', () => {
|
||||
it('is set to Vietnamese (vi)', () => {
|
||||
expect(defaultLocale).toBe('vi');
|
||||
});
|
||||
|
||||
it('is one of the supported locales', () => {
|
||||
expect(locales).toContain(defaultLocale);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Locale type', () => {
|
||||
it('accepts valid locale values', () => {
|
||||
const vi: Locale = 'vi';
|
||||
const en: Locale = 'en';
|
||||
expect(vi).toBe('vi');
|
||||
expect(en).toBe('en');
|
||||
});
|
||||
});
|
||||
});
|
||||
51
apps/web/i18n/__tests__/navigation.spec.ts
Normal file
51
apps/web/i18n/__tests__/navigation.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { mockCreateNavigation } = vi.hoisted(() => ({
|
||||
mockCreateNavigation: vi.fn(() => ({
|
||||
Link: 'MockLink',
|
||||
redirect: 'mockRedirect',
|
||||
usePathname: 'mockUsePathname',
|
||||
useRouter: 'mockUseRouter',
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock routing config
|
||||
vi.mock('../routing', () => ({
|
||||
routing: {
|
||||
locales: ['vi', 'en'] as const,
|
||||
defaultLocale: 'vi',
|
||||
localePrefix: 'as-needed',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('next-intl/navigation', () => ({
|
||||
createNavigation: mockCreateNavigation,
|
||||
}));
|
||||
|
||||
import { Link, redirect, usePathname, useRouter } from '../navigation';
|
||||
|
||||
describe('i18n/navigation', () => {
|
||||
it('calls createNavigation with the routing config', () => {
|
||||
expect(mockCreateNavigation).toHaveBeenCalledWith({
|
||||
locales: ['vi', 'en'],
|
||||
defaultLocale: 'vi',
|
||||
localePrefix: 'as-needed',
|
||||
});
|
||||
});
|
||||
|
||||
it('exports Link from createNavigation', () => {
|
||||
expect(Link).toBeDefined();
|
||||
});
|
||||
|
||||
it('exports redirect from createNavigation', () => {
|
||||
expect(redirect).toBeDefined();
|
||||
});
|
||||
|
||||
it('exports usePathname from createNavigation', () => {
|
||||
expect(usePathname).toBeDefined();
|
||||
});
|
||||
|
||||
it('exports useRouter from createNavigation', () => {
|
||||
expect(useRouter).toBeDefined();
|
||||
});
|
||||
});
|
||||
121
apps/web/i18n/__tests__/request.spec.ts
Normal file
121
apps/web/i18n/__tests__/request.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the routing module
|
||||
vi.mock('../routing', () => ({
|
||||
routing: {
|
||||
locales: ['vi', 'en'] as const,
|
||||
defaultLocale: 'vi',
|
||||
localePrefix: 'as-needed',
|
||||
},
|
||||
}));
|
||||
|
||||
// Track getRequestConfig calls
|
||||
let capturedConfigFn: ((params: { requestLocale: Promise<string | undefined> }) => Promise<{
|
||||
locale: string;
|
||||
messages: Record<string, unknown>;
|
||||
}>) | null = null;
|
||||
|
||||
vi.mock('next-intl/server', () => ({
|
||||
getRequestConfig: (fn: typeof capturedConfigFn) => {
|
||||
capturedConfigFn = fn;
|
||||
return fn;
|
||||
},
|
||||
}));
|
||||
|
||||
// Provide message mocks
|
||||
vi.mock('../../messages/vi.json', () => ({
|
||||
default: {
|
||||
common: { goodgo: 'GoodGo', loading: 'Đang tải...' },
|
||||
metadata: { title: 'GoodGo — Nền tảng Bất động sản Việt Nam' },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../messages/en.json', () => ({
|
||||
default: {
|
||||
common: { goodgo: 'GoodGo', loading: 'Loading...' },
|
||||
metadata: { title: 'GoodGo — Vietnam Real Estate Platform' },
|
||||
},
|
||||
}));
|
||||
|
||||
// Import triggers getRequestConfig, which captures the config function
|
||||
await import('../request');
|
||||
|
||||
describe('i18n/request', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('captures a config function via getRequestConfig', () => {
|
||||
expect(capturedConfigFn).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
describe('locale resolution', () => {
|
||||
it('uses the requested locale when it is valid (vi)', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('vi'),
|
||||
});
|
||||
expect(result.locale).toBe('vi');
|
||||
});
|
||||
|
||||
it('uses the requested locale when it is valid (en)', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('en'),
|
||||
});
|
||||
expect(result.locale).toBe('en');
|
||||
});
|
||||
|
||||
it('falls back to default locale (vi) when requestLocale is undefined', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve(undefined),
|
||||
});
|
||||
expect(result.locale).toBe('vi');
|
||||
});
|
||||
|
||||
it('falls back to default locale (vi) when requestLocale is empty string', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve(''),
|
||||
});
|
||||
expect(result.locale).toBe('vi');
|
||||
});
|
||||
|
||||
it('falls back to default locale (vi) for an invalid locale', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('fr'),
|
||||
});
|
||||
expect(result.locale).toBe('vi');
|
||||
});
|
||||
|
||||
it('falls back to default locale (vi) for another invalid locale', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('ja'),
|
||||
});
|
||||
expect(result.locale).toBe('vi');
|
||||
});
|
||||
});
|
||||
|
||||
describe('message loading', () => {
|
||||
it('loads Vietnamese messages for vi locale', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('vi'),
|
||||
});
|
||||
expect(result.messages).toBeDefined();
|
||||
expect((result.messages as Record<string, Record<string, string>>).common.loading).toBe('Đang tải...');
|
||||
});
|
||||
|
||||
it('loads English messages for en locale', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('en'),
|
||||
});
|
||||
expect(result.messages).toBeDefined();
|
||||
expect((result.messages as Record<string, Record<string, string>>).common.loading).toBe('Loading...');
|
||||
});
|
||||
|
||||
it('loads Vietnamese messages when falling back from invalid locale', async () => {
|
||||
const result = await capturedConfigFn!({
|
||||
requestLocale: Promise.resolve('fr'),
|
||||
});
|
||||
expect(result.locale).toBe('vi');
|
||||
expect((result.messages as Record<string, Record<string, string>>).common.loading).toBe('Đang tải...');
|
||||
});
|
||||
});
|
||||
});
|
||||
53
apps/web/i18n/__tests__/routing.spec.ts
Normal file
53
apps/web/i18n/__tests__/routing.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { mockDefineRouting } = vi.hoisted(() => ({
|
||||
mockDefineRouting: vi.fn((config: unknown) => config),
|
||||
}));
|
||||
|
||||
// Mock the config module before importing routing
|
||||
vi.mock('../config', () => ({
|
||||
locales: ['vi', 'en'] as const,
|
||||
defaultLocale: 'vi' as const,
|
||||
}));
|
||||
|
||||
// Mock next-intl/routing
|
||||
vi.mock('next-intl/routing', () => ({
|
||||
defineRouting: mockDefineRouting,
|
||||
}));
|
||||
|
||||
import { routing } from '../routing';
|
||||
|
||||
describe('i18n/routing', () => {
|
||||
it('calls defineRouting with the correct configuration', () => {
|
||||
expect(mockDefineRouting).toHaveBeenCalledWith({
|
||||
locales: ['vi', 'en'],
|
||||
defaultLocale: 'vi',
|
||||
localePrefix: 'as-needed',
|
||||
});
|
||||
});
|
||||
|
||||
it('exports a routing object', () => {
|
||||
expect(routing).toBeDefined();
|
||||
});
|
||||
|
||||
it('includes all supported locales', () => {
|
||||
const routingConfig = mockDefineRouting.mock.calls[0][0] as {
|
||||
locales: readonly string[];
|
||||
};
|
||||
expect(routingConfig.locales).toEqual(['vi', 'en']);
|
||||
});
|
||||
|
||||
it('sets defaultLocale to vi', () => {
|
||||
const routingConfig = mockDefineRouting.mock.calls[0][0] as {
|
||||
defaultLocale: string;
|
||||
};
|
||||
expect(routingConfig.defaultLocale).toBe('vi');
|
||||
});
|
||||
|
||||
it('uses as-needed locale prefix strategy', () => {
|
||||
const routingConfig = mockDefineRouting.mock.calls[0][0] as {
|
||||
localePrefix: string;
|
||||
};
|
||||
expect(routingConfig.localePrefix).toBe('as-needed');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user