Files
gods-eye-view/src/ui/localeSwitchReload.test.mjs
T
Sameh Khamis 6cb01ff975 feat(ui): render the language picker as a Vue component
LocalePicker renders the existing picker model with the same markup and
listbox behaviour; ProviderSettingsView shows it while the shell publishes
the active language and its choice handler to the gev-locale store. A host
passing localePicker: false publishes nothing. A choice reaches the shell
after the list is closed on screen, and every opening keeps the active
option in view.
2026-10-31 18:03:25 -07:00

165 lines
4.9 KiB
JavaScript

// The language picker reloads the page, so it asks first while the
// reload would lose work (unsaved CCTV calibration, the open cockpit, anything
// a module registers). Choosing the language already active does nothing.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { StyleManager } from './applicationShell.js';
import { useLocaleStore } from './stores/locale.js';
import { applicationPinia } from './vueRuntime.js';
import {
describeUnsavedWork,
msg,
registerUnsavedWork,
setLocale,
} from '../i18n/index.js';
/** Wire the shell's language choice with the given shell state. No picker
* markup here: the picker's own behaviour is covered by localePicker.test
* and localePickerView.test. */
function renderSwitch({ calDirty = false, cockpit = false } = {}) {
const previous = {
document: globalThis.document,
location: globalThis.location,
history: globalThis.history,
confirm: globalThis.confirm,
};
const reloads = [];
const prompts = [];
let answer = true;
globalThis.document = { getElementById: () => null };
globalThis.location = {
href: 'https://gev.example/#v=2',
assign() {},
reload: () => reloads.push(true),
};
globalThis.history = { replaceState() {} };
globalThis.confirm = (text) => {
prompts.push(text);
return answer;
};
const shell = {
services: {
cctvLayer: {
getUIState: () => ({ cameras: [{ calDirty: false }, { calDirty }] }),
},
},
cockpitView: { active: cockpit },
_confirmLocaleReload: StyleManager.prototype._confirmLocaleReload,
_chooseLocale: StyleManager.prototype._chooseLocale,
};
StyleManager.prototype._initLocaleSelector.call(shell);
return {
choose: (locale) => shell._chooseLocale(locale),
reloads,
prompts,
answer: (value) => (answer = value),
restore() {
for (const unregister of shell._unsavedWorkChecks) unregister();
Object.assign(globalThis, previous);
},
};
}
const storage = globalThis.localStorage;
test.before(() => {
globalThis.localStorage = { setItem() {}, getItem: () => null };
});
test.after(() => {
globalThis.localStorage = storage;
});
test('nothing to lose: the switch reloads without asking', () => {
const view = renderSwitch();
try {
view.choose('es');
assert.deepEqual(view.prompts, []);
assert.equal(view.reloads.length, 1);
} finally {
view.restore();
}
});
test('unsaved calibration or the open cockpit: the switch asks first', () => {
const view = renderSwitch({ calDirty: true, cockpit: true });
try {
view.answer(false);
view.choose('ru');
assert.equal(view.prompts.length, 1);
assert.match(view.prompts[0], /unsaved CCTV calibration/);
assert.match(view.prompts[0], /the open cockpit/);
assert.equal(view.reloads.length, 0, 'declined: no reload');
view.answer(true);
view.choose('ru');
assert.equal(view.reloads.length, 1, 'confirmed: reload');
} finally {
view.restore();
}
});
test('modules register their own unsaved work', () => {
const unregister = registerUnsavedWork(() => msg('drawings on the map'));
const failing = registerUnsavedWork(() => {
throw new Error('broken check');
});
const view = renderSwitch();
try {
view.answer(false);
view.choose('es');
assert.match(view.prompts[0], /drawings on the map/);
assert.equal(view.reloads.length, 0);
} finally {
view.restore();
unregister();
failing();
}
assert.deepEqual(describeUnsavedWork(), []);
});
test('the prompt reads in the active language', () => {
setLocale('es');
const view = renderSwitch({ calDirty: true });
try {
view.answer(false);
view.choose('en');
assert.equal(view.prompts.length, 1);
assert.doesNotMatch(view.prompts[0], /Switching language/);
} finally {
view.restore();
setLocale('en');
}
});
test('the language already active neither asks nor reloads', () => {
const view = renderSwitch({ calDirty: true });
try {
assert.equal(view.choose('en'), 'unchanged');
assert.deepEqual(view.prompts, []);
assert.equal(view.reloads.length, 0);
} finally {
view.restore();
}
});
test('a host without Core locales shows no picker; otherwise the shell publishes one', () => {
const store = useLocaleStore(applicationPinia());
store.publishPicker(null);
const disabled = { services: {}, _localePickerEnabled: false };
const enabled = {
services: {},
_chooseLocale: (locale) => chosen.push(locale),
};
const chosen = [];
try {
StyleManager.prototype._initLocaleSelector.call(disabled);
assert.equal(store.picker, null);
StyleManager.prototype._initLocaleSelector.call(enabled);
assert.equal(store.picker.current, 'en');
store.picker.onSelect('fr');
assert.deepEqual(chosen, ['fr']);
} finally {
store.publishPicker(null);
for (const shell of [disabled, enabled])
for (const unregister of shell._unsavedWorkChecks ?? []) unregister();
}
});