mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
i18n: fix the final pre-merge review findings
Fixes the P1 and the confirmed P2 findings of the final review of the
i18n branch.
- CI: the air-quality model no longer imports i18n (portable module),
so check:boundaries passes again.
- Air quality layer: named in the layer panel, HUD and voice; its
Weather card, bands and errors translate. A test keeps the layer-name
table in step with the layer registry.
- Layer statuses that still reached ru/es in English: GNSS, CelesTrak,
Transit loading and feed errors, launches, perimeters, ALPR, ECCC,
wind "field unavailable", Local ADS-B streaming lines.
- Voice and HUD: preset ids show city names ("Fly to San Francisco"),
the HUD city translates, cockpit refusals cover frame, aircraft and
satellite; empty Transit notes translate on the analyst card.
- tn() keeps a caller's formatted count ("12,345 map objects").
- Cyber HUD: the language button no longer covers the bottom-right
card or the cockpit position readout.
- CCTV buttons wrap only in non-English locales; English layout matches
main again.
- Switching language asks first when Provider Settings holds unsaved
keys.
Catalogs: es/ru 2611 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fada8b161b
commit
3adce33cc6
43 files changed
+1350
-97
No files matched your search
+145
-26
@@ -9,25 +9,42 @@ import {
|
||||
} from './keySetup.js';
|
||||
|
||||
test('setup placement moves the same button only in Cyber and disconnects on teardown', () => {
|
||||
const home = { prepend: (node) => { node.parentNode = home; } };
|
||||
const home = {
|
||||
prepend: (node) => {
|
||||
node.parentNode = home;
|
||||
},
|
||||
};
|
||||
const chip = { parentNode: home, hidden: true };
|
||||
let moves = 0;
|
||||
const toolbar = { append: (node) => { moves++; node.parentNode = toolbar; } };
|
||||
const toolbar = {
|
||||
append: (node) => {
|
||||
moves++;
|
||||
node.parentNode = toolbar;
|
||||
},
|
||||
};
|
||||
const theme = { dataset: { uiTheme: 'tactical' } };
|
||||
let notify, disconnected = false;
|
||||
let notify,
|
||||
disconnected = false;
|
||||
class Observer {
|
||||
constructor(callback) { notify = callback; }
|
||||
constructor(callback) {
|
||||
notify = callback;
|
||||
}
|
||||
observe(target, options) {
|
||||
assert.equal(target, theme);
|
||||
assert.deepEqual(options.attributeFilter, ['data-ui-theme']);
|
||||
}
|
||||
disconnect() { disconnected = true; }
|
||||
disconnect() {
|
||||
disconnected = true;
|
||||
}
|
||||
}
|
||||
const dispose = bindKeySetupPlacement({
|
||||
documentElement: theme,
|
||||
getElementById: () => toolbar,
|
||||
defaultView: { MutationObserver: Observer },
|
||||
}, chip);
|
||||
const dispose = bindKeySetupPlacement(
|
||||
{
|
||||
documentElement: theme,
|
||||
getElementById: () => toolbar,
|
||||
defaultView: { MutationObserver: Observer },
|
||||
},
|
||||
chip,
|
||||
);
|
||||
assert.equal(chip.parentNode, home);
|
||||
for (const variant of ['cyber', 'operator', 'cyber', 'minimal']) {
|
||||
theme.dataset.uiTheme = variant;
|
||||
@@ -42,10 +59,20 @@ test('setup placement moves the same button only in Cyber and disconnects on tea
|
||||
});
|
||||
|
||||
test('the chip counts what is missing, and retires the count at zero', () => {
|
||||
assert.equal(keySetupChipLabel({ setCount: 0, total: 8 }), 'POWER UP · 8 KEYS WAITING');
|
||||
assert.equal(keySetupChipLabel({ setCount: 7, total: 8 }), 'POWER UP · 1 KEY WAITING');
|
||||
assert.equal(
|
||||
keySetupChipLabel({ setCount: 0, total: 8 }),
|
||||
'POWER UP · 8 KEYS WAITING',
|
||||
);
|
||||
assert.equal(
|
||||
keySetupChipLabel({ setCount: 7, total: 8 }),
|
||||
'POWER UP · 1 KEY WAITING',
|
||||
);
|
||||
assert.equal(keySetupChipLabel({ setCount: 8, total: 8 }), 'POWERED UP');
|
||||
assert.equal(keySetupChipLabel(null), 'POWERED UP', 'no status is not a broken label');
|
||||
assert.equal(
|
||||
keySetupChipLabel(null),
|
||||
'POWERED UP',
|
||||
'no status is not a broken label',
|
||||
);
|
||||
});
|
||||
|
||||
test('collectKeyUpdates keeps only non-empty trimmed values', () => {
|
||||
@@ -85,14 +112,27 @@ test('OAuth login polling stops as soon as local ChatGPT auth becomes available'
|
||||
|
||||
test('OAuth login polling stops on login or HTTP failure instead of waiting for timeout', async () => {
|
||||
for (const result of [
|
||||
{ ok: true, payload: { available: false, loginFailed: true, error: 'Sign-in failed.' } },
|
||||
{
|
||||
ok: true,
|
||||
payload: {
|
||||
available: false,
|
||||
loginFailed: true,
|
||||
error: 'Sign-in failed.',
|
||||
},
|
||||
},
|
||||
{ ok: false, payload: { error: 'Sign-in is local only.' } },
|
||||
]) {
|
||||
let checks = 0;
|
||||
await assert.rejects(waitForChatGptOAuth({
|
||||
fetchImpl: async () => { checks += 1; return { ok: result.ok, json: async () => result.payload }; },
|
||||
sleep: () => assert.fail('failed login must stop polling'),
|
||||
}), { message: result.payload.error });
|
||||
await assert.rejects(
|
||||
waitForChatGptOAuth({
|
||||
fetchImpl: async () => {
|
||||
checks += 1;
|
||||
return { ok: result.ok, json: async () => result.payload };
|
||||
},
|
||||
sleep: () => assert.fail('failed login must stop polling'),
|
||||
}),
|
||||
{ message: result.payload.error },
|
||||
);
|
||||
assert.equal(checks, 1);
|
||||
}
|
||||
});
|
||||
@@ -100,12 +140,34 @@ test('OAuth login polling stops on login or HTTP failure instead of waiting for
|
||||
test('OAuth login polling respects timeout and an aborted setup surface', async () => {
|
||||
let clock = 0;
|
||||
let checks = 0;
|
||||
const poll = { fetchImpl: async () => { checks += 1; return { ok: true, json: async () => ({ available: false, pending: true }) }; } };
|
||||
assert.equal(await waitForChatGptOAuth({ ...poll, timeoutMs: 100, pollMs: 100, now: () => clock, sleep: async (ms) => { clock += ms; } }), false);
|
||||
const poll = {
|
||||
fetchImpl: async () => {
|
||||
checks += 1;
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ available: false, pending: true }),
|
||||
};
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await waitForChatGptOAuth({
|
||||
...poll,
|
||||
timeoutMs: 100,
|
||||
pollMs: 100,
|
||||
now: () => clock,
|
||||
sleep: async (ms) => {
|
||||
clock += ms;
|
||||
},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(checks, 2);
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
assert.equal(await waitForChatGptOAuth({ ...poll, signal: controller.signal }), false);
|
||||
assert.equal(
|
||||
await waitForChatGptOAuth({ ...poll, signal: controller.signal }),
|
||||
false,
|
||||
);
|
||||
assert.equal(checks, 2, 'aborted polling does not fetch');
|
||||
});
|
||||
|
||||
@@ -113,14 +175,22 @@ test('OAuth polling ignores a ready response received after the surface was abor
|
||||
const controller = new AbortController();
|
||||
const available = await waitForChatGptOAuth({
|
||||
signal: controller.signal,
|
||||
fetchImpl: async () => ({ ok: true, json: async () => { controller.abort(); return { available: true }; } }),
|
||||
fetchImpl: async () => ({
|
||||
ok: true,
|
||||
json: async () => {
|
||||
controller.abort();
|
||||
return { available: true };
|
||||
},
|
||||
}),
|
||||
sleep: () => assert.fail('aborted polling must not continue'),
|
||||
});
|
||||
assert.equal(available, false);
|
||||
});
|
||||
|
||||
test('the first Google key strips ONLY the keyless OSM basemap from the share hash', () => {
|
||||
const stripped = stripKeylessBasemapFromHash('lat=30.2&lon=-97.7&map=osm&style=normal');
|
||||
const stripped = stripKeylessBasemapFromHash(
|
||||
'lat=30.2&lon=-97.7&map=osm&style=normal',
|
||||
);
|
||||
assert.ok(stripped !== null);
|
||||
const params = new URLSearchParams(stripped);
|
||||
assert.equal(params.get('map'), null, 'osm basemap removed');
|
||||
@@ -128,7 +198,11 @@ test('the first Google key strips ONLY the keyless OSM basemap from the share ha
|
||||
assert.equal(params.get('style'), 'normal', 'style survives');
|
||||
// A stack under any other name was chosen or shared on purpose.
|
||||
assert.equal(stripKeylessBasemapFromHash('map=bing-aerial&lat=1'), null);
|
||||
assert.equal(stripKeylessBasemapFromHash('lat=1&lon=2'), null, 'no stack, nothing to do');
|
||||
assert.equal(
|
||||
stripKeylessBasemapFromHash('lat=1&lon=2'),
|
||||
null,
|
||||
'no stack, nothing to do',
|
||||
);
|
||||
assert.equal(stripKeylessBasemapFromHash(''), null);
|
||||
assert.equal(stripKeylessBasemapFromHash(undefined), null);
|
||||
});
|
||||
@@ -142,11 +216,15 @@ test('aborting pending setup removes its surface and ignores a late response', a
|
||||
let requestSignal;
|
||||
const controller = new AbortController();
|
||||
const pending = initKeySetup({
|
||||
documentRef: { getElementById: (id) => id === 'key-setup-chip' ? chip : root },
|
||||
documentRef: {
|
||||
getElementById: (id) => (id === 'key-setup-chip' ? chip : root),
|
||||
},
|
||||
signal: controller.signal,
|
||||
fetchImpl: (_url, { signal }) => {
|
||||
requestSignal = signal;
|
||||
return new Promise((resolve) => { resolveResponse = resolve; });
|
||||
return new Promise((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
},
|
||||
});
|
||||
controller.abort();
|
||||
@@ -155,3 +233,44 @@ test('aborting pending setup removes its surface and ignores a late response', a
|
||||
resolveResponse({ ok: true, json: async () => ({ keys: [] }) });
|
||||
assert.equal(await pending, null);
|
||||
});
|
||||
|
||||
test('a pasted, unsaved key makes the language switch ask first until teardown', async () => {
|
||||
const { initKeySetup } = await import('./keySetup.js');
|
||||
const { describeUnsavedWork } = await import('./i18n/index.js');
|
||||
const node = () => ({
|
||||
dataset: {},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
setAttribute() {},
|
||||
querySelector: () => null,
|
||||
remove() {},
|
||||
});
|
||||
const chip = node();
|
||||
const input = { dataset: { envVar: 'GOOGLE_MAPS_API_KEY' }, value: '' };
|
||||
const root = {
|
||||
...node(),
|
||||
querySelectorAll: (selector) =>
|
||||
selector === 'input[data-env-var]' ? [input] : [],
|
||||
};
|
||||
const controller = new AbortController();
|
||||
const setup = await initKeySetup({
|
||||
documentRef: {
|
||||
getElementById: (id) => (id === 'key-setup-chip' ? chip : root),
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
},
|
||||
signal: controller.signal,
|
||||
fetchImpl: async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ keys: [], setCount: 0, total: 1 }),
|
||||
}),
|
||||
});
|
||||
assert.ok(setup);
|
||||
assert.deepEqual(describeUnsavedWork(), []);
|
||||
input.value = ' ';
|
||||
assert.deepEqual(describeUnsavedWork(), []);
|
||||
input.value = 'AIza-pasted';
|
||||
assert.deepEqual(describeUnsavedWork(), ['unsaved provider keys']);
|
||||
controller.abort();
|
||||
assert.deepEqual(describeUnsavedWork(), []);
|
||||
});
|
||||
Reference in new issue
Block a user