Files
houseplan-card/test/i18n-runtime.test.mjs
T
Claude 82a48ee0ce perf(i18n): словари settings/support/topology — ru/de/fr ленивыми чанками; потолок онбординга (#627)
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на
  пару «пространство × язык»), английский статически как слой отката;
  рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback,
  общий тост отказа), src/i18n/namespace-language.ts.
- Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер
  только после того, как словарь текущего языка осел; гейт хоста учитывает
  словари загруженной на нём поверхности (смена языка на лету — ветка warm).
  Оверлей Zigbee ждёт topology до первой отрисовки.
- bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со
  строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500
  (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки
  ленивости и владения по содержимому.
- Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык
  синхронно, ждут гейт вместо ослабления проверок; пять мутантов.
- monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro
  и export на каждый) и записи манифеста; gzip каждой поверхности при этом
  уменьшился.

Issue: #627
User-Visible: no
2026-09-24 14:51:18 +03:00

252 lines
10 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
LanguageRuntime, languageRenderGate,
} from '../test-build/i18n/language-runtime.js';
const deferred = () => {
let resolve;
let reject;
const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; });
return { promise, resolve, reject };
};
test('lazy locale runtime deduplicates hosts and accepts the matching build', async () => {
const load = deferred();
let calls = 0;
const runtime = new LanguageRuntime([
{ code: 'en', dictionary: { save: 'Save' } },
{ code: 'de', loadDictionary: async () => { calls++; return load.promise; } },
], 'same-build');
const first = runtime.ensure('de');
const second = runtime.ensure('de');
assert.equal(first, second);
assert.equal(calls, 1);
assert.equal(runtime.state('de'), 'pending');
load.resolve({ dictionary: { save: 'Speichern' }, fingerprint: 'same-build' });
await first;
assert.equal(runtime.state('de'), 'ready');
assert.equal(runtime.dictionary('de').save, 'Speichern');
await runtime.ensure('de');
assert.equal(calls, 1);
});
test('lazy locale runtime retries once, warns once and settles on English fallback', async () => {
const attempts = [];
const warnings = [];
const runtime = new LanguageRuntime([
{
code: 'de',
loadDictionary: async (attempt) => {
attempts.push(attempt);
throw new Error(`failure ${attempt}`);
},
},
], 'build', (message, error) => warnings.push([message, error]));
await runtime.ensure('de');
assert.deepEqual(attempts, [0, 1]);
assert.equal(runtime.state('de'), 'fallback');
assert.equal(runtime.dictionary('de'), undefined);
assert.equal(warnings.length, 1);
await runtime.ensure('de');
assert.equal(warnings.length, 1);
assert.deepEqual(attempts, [0, 1]);
});
test('lazy locale runtime rejects a stale fingerprint before accepting retry', async () => {
const runtime = new LanguageRuntime([{
code: 'de',
loadDictionary: async (attempt) => ({
dictionary: { save: attempt ? 'Speichern' : 'Stale' },
fingerprint: attempt ? 'current' : 'old',
}),
}], 'current');
await runtime.ensure('de');
assert.equal(runtime.dictionary('de').save, 'Speichern');
});
class FakeHost {
controllers = [];
inert = false;
isConnected = true;
attrs = new Map();
updates = 0;
addController(controller) { this.controllers.push(controller); }
requestUpdate() { this.updates++; }
hasAttribute(name) { return this.attrs.has(name); }
setAttribute(name, value) { this.attrs.set(name, value); }
removeAttribute(name) { this.attrs.delete(name); }
}
test('locale render gate exposes cold/warm states, inertness and ignores disconnected hosts', async () => {
const load = deferred();
const runtime = new LanguageRuntime([
{ code: 'en', dictionary: {} },
{ code: 'de', loadDictionary: async () => load.promise },
], 'build');
const host = new FakeHost();
let language = 'en';
assert.equal(languageRenderGate(host, runtime, language), 'ready');
assert.equal(host.attrs.get('lang'), 'en');
language = 'de';
assert.equal(languageRenderGate(host, runtime, language), 'warm');
assert.equal(host.inert, true);
assert.equal(host.attrs.get('aria-busy'), 'true');
host.isConnected = false;
load.resolve({ dictionary: {}, fingerprint: 'build' });
await runtime.ensure('de');
await Promise.resolve();
assert.equal(host.updates, 0);
assert.equal(languageRenderGate(host, runtime, language), 'ready');
assert.equal(host.inert, false);
assert.equal(host.hasAttribute('aria-busy'), false);
assert.equal(host.attrs.get('lang'), 'de');
const coldLoad = deferred();
const coldRuntime = new LanguageRuntime([{
code: 'de', loadDictionary: async () => coldLoad.promise,
}], 'build');
const coldHost = new FakeHost();
assert.equal(languageRenderGate(coldHost, coldRuntime, 'de'), 'cold');
coldLoad.resolve({ dictionary: {}, fingerprint: 'build' });
await coldRuntime.ensure('de');
await Promise.resolve();
assert.equal(coldHost.updates, 1);
assert.equal(languageRenderGate(coldHost, coldRuntime, 'de'), 'ready');
assert.equal(coldHost.attrs.get('lang'), 'de');
});
test('locale render gate exposes the fallback language to assistive technology', async () => {
const runtime = new LanguageRuntime([{
code: 'de', loadDictionary: async () => { throw new Error('offline'); },
}], 'build', () => {});
const host = new FakeHost();
assert.equal(languageRenderGate(host, runtime, 'de'), 'cold');
await runtime.ensure('de');
assert.equal(languageRenderGate(host, runtime, 'de'), 'ready');
assert.equal(host.attrs.get('lang'), 'en');
});
test('the production runtime is the tested class, not a handwritten twin (#354)', async () => {
const registry = await import('../test-build/i18n/registry.js');
assert.ok(
registry.LANGUAGE_RUNTIME instanceof LanguageRuntime,
'LANGUAGE_RUNTIME must be an instance of the class this suite proves',
);
const { readFileSync } = await import('node:fs');
const source = readFileSync(new URL('../src/i18n/registry.ts', import.meta.url), 'utf8');
assert.doesNotMatch(
source,
/germanPending|germanFailed|settleGerman/,
'the handwritten duplicate must not return — tests would look past production again',
);
assert.match(
source,
/new LanguageRuntime\(\s*LANGUAGE_REGISTRY,\s*BUILD_FINGERPRINT,\s*console\.warn,\s*notifyLanguageLoadFailures,\s*\)/,
'the runtime must wire its loadFailed hook to the unit-proven delivery function',
);
});
test('a settled dictionary failure reaches the load-failed hook once (#354)', async () => {
const failures = [];
const warnings = [];
const runtime = new LanguageRuntime(
[
{ code: 'en', dictionary: { key: 'value' } },
{ code: 'de', loadDictionary: async () => { throw new Error('net'); } },
],
'fp',
(message, error) => warnings.push([message, String(error)]),
(code) => failures.push(code),
);
await runtime.ensure('de');
assert.deepEqual(failures, ['de']);
assert.equal(warnings.length, 1);
assert.equal(runtime.state('de'), 'fallback');
await runtime.ensure('de');
assert.deepEqual(failures, ['de'], 'a settled fallback never re-fires the hook');
});
test('language load failure subscription delivers codes and unsubscribes (#354)', async () => {
const { subscribeLanguageLoadFailures, notifyLanguageLoadFailures } =
await import('../test-build/i18n/registry.js');
const seen = [];
const unsubscribe = subscribeLanguageLoadFailures((code) => seen.push(code));
const second = subscribeLanguageLoadFailures((code) => seen.push(`2:${code}`));
notifyLanguageLoadFailures('de');
assert.deepEqual(seen, ['de', '2:de'], 'every live subscriber receives the failed code');
unsubscribe();
notifyLanguageLoadFailures('de');
assert.deepEqual(
seen,
['de', '2:de', '2:de'],
'an unsubscribed listener stays silent while the rest keep receiving',
);
second();
notifyLanguageLoadFailures('de');
assert.deepEqual(seen, ['de', '2:de', '2:de'], 'no listeners — no deliveries');
});
test('#627 AC5 составной гейт ждёт словарь пространства и не роняет lang в en при его отказе', async () => {
const { composeLanguageRuntimes } = await import('../test-build/i18n/namespace-language.js');
const primary = new LanguageRuntime([
{ code: 'en', dictionary: {} }, { code: 'ru', dictionary: {} },
], 'build');
const namespaceLoad = deferred();
const loads = [];
const namespace = new LanguageRuntime([
{ code: 'en', dictionary: { title: 'Settings' } },
{ code: 'ru', loadDictionary: async (attempt) => { loads.push(attempt); return namespaceLoad.promise; } },
], 'build', () => {});
const host = new FakeHost();
const composite = composeLanguageRuntimes(primary, [namespace]);
assert.equal(languageRenderGate(host, composite, 'en'), 'ready', 'en of the namespace is static');
assert.deepEqual(loads, [], 'English never requests a namespace chunk');
// Live switch en → ru: the main catalog is ready, the namespace is not.
assert.equal(composite.state('ru'), 'pending');
assert.equal(languageRenderGate(host, composite, 'ru'), 'warm');
assert.equal(host.inert, true);
assert.equal(host.attrs.get('aria-busy'), 'true');
assert.equal(host.attrs.get('lang'), 'en', 'the previous frame keeps its language while pending');
namespaceLoad.resolve({ dictionary: { title: 'Настройки' }, fingerprint: 'build' });
await composite.ensure('ru');
await Promise.resolve();
assert.equal(host.updates, 1, 'the host re-renders once the namespace settled');
assert.equal(languageRenderGate(host, composite, 'ru'), 'ready');
assert.equal(host.inert, false);
assert.equal(host.attrs.get('lang'), 'ru');
assert.equal(namespace.dictionary('ru').title, 'Настройки');
assert.deepEqual(loads, [0], 'one request per page, deduplicated');
// A failed namespace settles as fallback: the host unblocks, and its `lang`
// stays the main catalog language — only the namespace strings are English.
const failing = new LanguageRuntime([
{ code: 'en', dictionary: {} },
{ code: 'ru', loadDictionary: async () => { throw new Error('offline'); } },
], 'build', () => {});
const failedHost = new FakeHost();
const failedComposite = composeLanguageRuntimes(primary, [failing]);
assert.equal(languageRenderGate(failedHost, failedComposite, 'ru'), 'cold');
await failedComposite.ensure('ru');
assert.equal(failing.state('ru'), 'fallback');
assert.equal(failedComposite.state('ru'), 'ready');
assert.equal(languageRenderGate(failedHost, failedComposite, 'ru'), 'ready');
assert.equal(failedHost.attrs.get('lang'), 'ru');
assert.equal(failedHost.hasAttribute('aria-busy'), false);
// The main catalog still decides the fallback `lang` on its own failure.
const mainFailing = new LanguageRuntime([
{ code: 'en', dictionary: {} },
{ code: 'de', loadDictionary: async () => { throw new Error('offline'); } },
], 'build', () => {});
const settledNamespace = new LanguageRuntime([
{ code: 'en', dictionary: {} }, { code: 'de', dictionary: {} },
], 'build');
const mainComposite = composeLanguageRuntimes(mainFailing, [settledNamespace]);
await mainComposite.ensure('de');
assert.equal(mainComposite.state('de'), 'fallback');
});