Files
life-echo/app-expo/tests/i18n/index.test.ts

149 lines
3.5 KiB
TypeScript

type MockLocale = {
languageCode?: string;
languageTag?: string;
};
type I18nModule = typeof import('@/i18n');
async function flushMicrotasks() {
await Promise.resolve();
await Promise.resolve();
}
function loadI18nModule(options?: {
locales?: MockLocale[];
platformOS?: 'android' | 'ios';
}) {
let currentLocales = options?.locales ?? [{ languageTag: 'zh-CN' }];
const platformOS = options?.platformOS ?? 'ios';
const remove = jest.fn();
const addEventListener = jest.fn(
(_type: string, listener: (state: string) => void) => ({
listener,
remove,
}),
);
jest.resetModules();
jest.doMock('expo-localization', () => ({
getLocales: () => currentLocales,
}));
jest.doMock('react-native', () => {
return {
AppState: {
addEventListener,
},
Platform: {
OS: platformOS,
select: (config: Record<string, unknown>) =>
config[platformOS] ?? config.default,
},
};
});
const module = require('@/i18n') as I18nModule;
return {
...module,
addEventListener,
remove,
setLocales: (nextLocales: MockLocale[]) => {
currentLocales = nextLocales;
},
};
}
describe('i18n device synchronization', () => {
let consoleInfoSpy: jest.SpyInstance;
beforeEach(() => {
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
});
afterEach(() => {
consoleInfoSpy.mockRestore();
jest.resetModules();
jest.clearAllMocks();
});
test('resolves zh when the device locale starts with zh', async () => {
const { getDeviceLanguage } = loadI18nModule({
locales: [{ languageTag: 'zh-Hans-CN' }],
});
expect(getDeviceLanguage()).toBe('zh');
});
test('falls back to zh when the device locale is missing', async () => {
const { getDeviceLanguage } = loadI18nModule({
locales: [{}],
});
expect(getDeviceLanguage()).toBe('zh');
});
test('syncLanguageWithDevice updates i18n when the current language differs', async () => {
const { default: i18n, syncLanguageWithDevice } = loadI18nModule({
locales: [{ languageTag: 'en-US' }],
});
await i18n.changeLanguage('zh');
await syncLanguageWithDevice();
expect(i18n.resolvedLanguage).toBe('en');
});
test('startLocaleSync subscribes on Android and refreshes language when the app becomes active', async () => {
const {
addEventListener,
default: i18n,
setLocales,
startLocaleSync,
} = loadI18nModule({
locales: [{ languageTag: 'zh-CN' }],
platformOS: 'android',
});
await i18n.changeLanguage('zh');
const subscription = startLocaleSync();
expect(addEventListener).toHaveBeenCalledWith(
'change',
expect.any(Function),
);
const listener = addEventListener.mock.calls[0][1] as (
state: string,
) => void;
setLocales([{ languageTag: 'en-US' }]);
listener('background');
await flushMicrotasks();
expect(i18n.resolvedLanguage).toBe('zh');
listener('active');
await flushMicrotasks();
expect(i18n.resolvedLanguage).toBe('en');
subscription.remove();
});
test('startLocaleSync is a no-op on iOS', async () => {
const { addEventListener, startLocaleSync } = loadI18nModule({
locales: [{ languageTag: 'en-US' }],
platformOS: 'ios',
});
const subscription = startLocaleSync();
expect(addEventListener).not.toHaveBeenCalled();
expect(subscription.remove).toEqual(expect.any(Function));
subscription.remove();
});
});