Files
openless/openless-all/app/scripts/locale-lifecycle.test.mjs
T

205 lines
6.9 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
async function exercise(scenario) {
const entries = new Map();
if (scenario.stored) entries.set('ol.locale', scenario.stored);
const storage = {
getItem: (key) => entries.get(key) ?? null,
setItem: (key, value) => entries.set(key, String(value)),
removeItem: (key) => entries.delete(key),
clear: () => entries.clear(),
};
// The provider/probe tree renders no HTML. These DOM surfaces let React run
// its real mount/update effects without adding a browser emulation dependency.
class Element extends EventTarget {
ownerDocument;
tagName;
nodeType = 1;
namespaceURI = 'http://www.w3.org/1999/xhtml';
style = {};
dataset = {};
textContent = '';
constructor(ownerDocument, tagName) {
super();
this.ownerDocument = ownerDocument;
this.tagName = tagName;
}
get nodeName() {
return this.tagName;
}
setAttribute() {}
contains(value) {
return value === this;
}
}
class TestDocument extends EventTarget {
nodeType = 9;
documentElement = new Element(this, 'HTML');
body = new Element(this, 'BODY');
activeElement = this.body;
createElement(tag) {
return new Element(this, tag.toUpperCase());
}
}
const doc = new TestDocument();
const browser = Object.assign(new EventTarget(), {
document: doc,
localStorage: storage,
sessionStorage: storage,
HTMLElement: Element,
HTMLIFrameElement: class extends Element {},
matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
setTimeout,
clearTimeout,
});
Object.defineProperty(globalThis, 'window', { configurable: true, value: browser });
Object.defineProperty(globalThis, 'document', { configurable: true, value: doc });
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: { language: scenario.system, platform: 'MacIntel', userAgent: 'Macintosh' },
});
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
configurable: true,
value: true,
});
const locale = await import('../src/i18n/index.ts');
await locale.i18nReady;
const boot = locale.default.resolvedLanguage;
const { getSettings, setSettings } = await import('../src/lib/ipc/index.ts');
const speech = {
chineseScriptPreference: 'traditional',
outputLanguagePreference: 'ko',
translationTargetLanguage: '日本語',
workingLanguages: ['한국어'],
};
await setSettings({
...(await getSettings()),
...speech,
workingLanguages: [...speech.workingLanguages],
});
const { act, createElement } = await import('react');
let root;
let current;
if (scenario.renderPreferences) {
const { createRoot } = await import('react-dom/client');
const { HotkeySettingsProvider, useHotkeySettings } =
await import('../src/state/HotkeySettingsContext.tsx');
const { useTranslation } = await import('react-i18next');
function Probe() {
current = useHotkeySettings();
useTranslation();
return null;
}
const mounted = createRoot(doc.createElement('div'));
root = mounted;
await act(async () => {
mounted.render(createElement(HotkeySettingsProvider, { children: createElement(Probe) }));
});
assert.ok(current?.prefs, 'the real preference provider has finished loading');
}
await act(async () => {
if (scenario.action === 'switch') await locale.setLocalePreference('fr');
if (scenario.action === 'system') await locale.setLocalePreference('system');
if (scenario.action === 'rapid')
await Promise.all([
locale.setLocalePreference('de'),
locale.setLocalePreference('es'),
locale.setLocalePreference('fr'),
]);
if (scenario.action === 'storage') {
const changed = new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('auxiliary window did not follow the storage event')),
3000,
);
locale.default.once('languageChanged', () => {
clearTimeout(timer);
resolve();
});
});
storage.setItem('ol.locale', 'ko');
browser.dispatchEvent(
Object.assign(new Event('storage'), {
key: 'ol.locale',
newValue: 'ko',
storageArea: storage,
}),
);
await changed;
}
});
if (current?.prefs) {
await act(async () => {
await current.updatePrefs((prefs) => ({ ...prefs, startMinimized: !prefs.startMinimized }));
});
await act(async () => {
await current.refresh();
});
}
const settings = await getSettings();
for (const [key, value] of Object.entries(speech)) {
assert.deepEqual(settings[key], value, `UI locale must not overwrite ${key}`);
}
if (root) await act(async () => root.unmount());
console.log(
JSON.stringify({
boot,
active: locale.default.resolvedLanguage,
saved: storage.getItem('ol.locale'),
preference: locale.getLocalePreference(),
title: locale.default.t('settings.title'),
documentLanguage: doc.documentElement.lang,
}),
);
}
const input = process.env.OPENLESS_LOCALE_TEST_SCENARIO;
if (input) {
await exercise(JSON.parse(input));
} else {
const run = (scenario) => {
const result = spawnSync(
process.execPath,
['--import', 'tsx', fileURLToPath(import.meta.url)],
{
env: { ...process.env, OPENLESS_LOCALE_TEST_SCENARIO: JSON.stringify(scenario) },
encoding: 'utf8',
timeout: 10000,
},
);
assert.equal(result.status, 0, result.stderr || result.error?.message);
return JSON.parse(result.stdout.trim());
};
assert.equal(run({ system: 'de-DE' }).boot, 'de');
assert.equal(run({ system: 'zh-HK' }).boot, 'zh-TW');
assert.equal(run({ system: 'pt-BR' }).boot, 'en');
assert.equal(run({ system: 'ko-KR', stored: 'es' }).boot, 'es');
assert.equal(run({ system: 'fr-CA', stored: 'not-supported' }).boot, 'fr');
const switched = run({
system: 'de-DE',
stored: 'es',
action: 'switch',
renderPreferences: true,
});
assert.equal(switched.active, 'fr');
assert.equal(switched.saved, 'fr');
assert.equal(switched.documentLanguage, 'fr');
assert.equal(
run({ system: 'ja-JP', stored: switched.saved }).boot,
'fr',
'restart preserves the explicit language',
);
const system = run({ system: 'de-DE', stored: 'fr', action: 'system', renderPreferences: true });
assert.equal(system.active, 'de');
assert.equal(system.saved, null);
assert.equal(system.preference, 'system');
assert.equal(run({ system: 'es-MX' }).boot, 'es', 'follow-system is resolved again on restart');
assert.equal(
run({ system: 'en-US', stored: 'de', action: 'storage', renderPreferences: true }).active,
'ko',
);
const rapid = run({ system: 'en-US', action: 'rapid', renderPreferences: true });
assert.equal(rapid.active, 'fr');
assert.equal(rapid.saved, 'fr');
}