Files
luma-home/tests/translations-smoke.mjs
T
Kaloyan DanchevandClaude Opus 4.6 cbea5a322f
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s
Merge Luma launcher fork into monorepo
Move trampoline files (boot-hook, watcher, overlay) into trampoline/
subdirectory. Add upstream Luma v2.3.1 source at repo root with our
additions:

- Page::Ha (4th nav tab) with placeholder empty state
- HA tab config schema and default entity mapping (ha-home/)
- Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings
- All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-24 11:52:55 +03:00

265 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import http from 'node:http';
import { spawn } from 'node:child_process';
import messages from '../assets/messages.json' with { type: 'json' };
const root = path.resolve(import.meta.dirname, '..');
const out = path.join(root, '.build');
await fs.mkdir(out, { recursive: true });
const temporary = await fs.mkdtemp(path.join(out, 'translations-smoke-'));
const preferences = path.join(temporary, 'preferences.json');
const binary = path.resolve(root, process.env.CARGO_TARGET_DIR ?? 'target', 'debug/luma');
const requests = [];
const places = {
en: { name: 'Vienna', region: 'Vienna', country: 'Austria' },
de: { name: 'Wien', region: 'Wien', country: 'Österreich' },
zh: { name: '维也纳', region: '维也纳', country: '奥地利' },
pt: { name: 'Viena', region: 'Viena', country: 'Áustria' },
};
const server = http.createServer((request, response) => {
const url = new URL(request.url, 'http://127.0.0.1');
requests.push({ path: url.pathname, query: Object.fromEntries(url.searchParams) });
response.setHeader('Content-Type', 'application/json');
if (url.pathname === '/v1/search') {
const place = places[url.searchParams.get('language')];
assert.ok(place, 'Geocoding must use a supported UI language');
response.end(
JSON.stringify({
results: [{ ...place, admin1: place.region, latitude: 48.2085, longitude: 16.3721 }],
}),
);
} else if (url.pathname === '/v1/forecast') {
assert.equal(url.searchParams.get('temperature_unit'), 'celsius');
response.end(JSON.stringify({ current: { temperature_2m: 14.2, weather_code: 2, is_day: 1 } }));
} else {
response.writeHead(404);
response.end('{}');
}
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
async function run(command, args) {
await new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd: root, stdio: 'inherit' });
const timer = setTimeout(() => {
child.kill('SIGKILL');
reject(new Error('Translation smoke child timed out'));
}, 45000);
child.on('error', (error) => {
clearTimeout(timer);
reject(error);
});
child.on('exit', (code) => {
clearTimeout(timer);
if (code === 0) resolve();
else reject(new Error(`${command} exited ${code}`));
});
});
}
async function scenario(name, script, extra = []) {
const dump = path.join(temporary, `${name}.json`);
const report = path.join(out, `language-${name}-render.json`);
await run(binary, [
'--preferences',
preferences,
'--weather-test-server',
origin,
'--script',
script,
'--frames',
'120',
'--time',
'2026-10-21T20:41:00+02:00',
'--screenshot',
path.join(out, `language-${name}.png`),
'--dump-state',
dump,
'--diagnostics',
report,
...extra,
]);
const state = JSON.parse(await fs.readFile(dump, 'utf8'));
const render = JSON.parse(await fs.readFile(report, 'utf8'));
assert.equal(state.error, null);
assert.equal(
render.capture.missing_glyphs,
0,
'Translated text must not become missing-character boxes',
);
assert.equal(render.capture.glyph_failures, 0);
assert.equal(
render.capture.missing_images,
0,
'Settings icons and other translated-screen artwork must be present',
);
return state;
}
function contains(state, key, language) {
assert.ok(state.ui_text.includes(messages[key][language]), `Missing rendered ${key}/${language}`);
}
const picker = 'settings,down,enter,down,down,down,enter';
const location = 'settings,down,down,down,enter,down,enter';
try {
await run('cargo', ['build', '--locked', '--package', 'luma']);
await fs.writeFile(
preferences,
JSON.stringify({ language: 'en', show_home_names: true, reduced_motion: true }),
);
const languages = await scenario('picker-en', picker);
assert.equal(languages.subpage, 'Language');
for (const name of ['English', 'Deutsch', '简体中文', 'Español', 'Português (Brasil)'])
assert.ok(languages.ui_text.includes(name));
const german = await scenario('de-home', `${picker},down,enter,home,down`);
assert.equal(german.preferences.language, 'de');
for (const key of [
'home',
'settings',
'favorites',
'inputsHeading',
'evening',
'setWeatherLocation',
])
contains(german, key, 'de');
assert.ok(german.ui_text.includes('Netflix'), 'Installed app names stay intact');
const persisted = await scenario('de-settings', 'settings,down');
assert.equal(persisted.preferences.language, 'de');
contains(persisted, 'language', 'de');
const chinese = await scenario('zh-home', `${picker},down,enter,home,down`);
assert.equal(chinese.preferences.language, 'zh');
for (const key of [
'home',
'apps',
'settings',
'favorites',
'inputsHeading',
'evening',
'setWeatherLocation',
])
contains(chinese, key, 'zh');
assert.ok(chinese.ui_text.includes('Netflix'));
assert.ok(chinese.ui_text.includes('10月21日星期三'));
const chineseSettings = await scenario('zh-settings', 'settings,down');
contains(chineseSettings, 'language', 'zh');
contains(chineseSettings, 'appNames', 'zh');
const selected = await scenario(
'zh-weather',
`${location},type:Vienna,back,enter,await-search,down,enter,home,await-weather`,
);
assert.equal(selected.preferences.weather_location.name, '维也纳');
contains(selected, 'partlyCloudy', 'zh');
assert.equal(requests.find((request) => request.path === '/v1/search').query.language, 'zh');
const forecasts = requests.filter((request) => request.path === '/v1/forecast').length;
const changed = await scenario('de-weather', `${picker},up,enter,home,await-weather`);
assert.equal(changed.preferences.language, 'de');
contains(changed, 'partlyCloudy', 'de');
assert.ok(changed.ui_text.includes('维也纳'), 'The saved city name is data, not a UI label');
assert.equal(
requests.filter((request) => request.path === '/v1/forecast').length,
forecasts,
'Language changes reuse weather cache',
);
const germanPlace = await scenario(
'de-search',
`${location},type:Wien,back,enter,await-search,down,enter`,
);
assert.equal(germanPlace.preferences.weather_location.name, 'Wien');
assert.equal(
requests.filter((request) => request.path === '/v1/search').at(-1).query.language,
'de',
);
const germanDialog = await scenario('de-dialog', 'apps,down,menu');
contains(germanDialog, 'removeFavorite', 'de');
contains(germanDialog, 'hideApp', 'de');
const saved = JSON.parse(await fs.readFile(preferences, 'utf8'));
await fs.writeFile(preferences, JSON.stringify({ ...saved, language: 'zh' }));
const chineseDialog = await scenario('zh-dialog', 'apps,down,menu');
contains(chineseDialog, 'removeFavorite', 'zh');
contains(chineseDialog, 'categoryStreaming', 'zh');
const input = await scenario('zh-input-options', 'settings,down,down,down,down,enter,enter');
for (const key of ['stateOn', 'stateOff', 'stateAuto', 'inputWhenConnected'])
contains(input, key, 'zh');
const background = await scenario('zh-background', 'settings,down,down,enter');
for (const key of ['slate', 'midnight', 'monumentValley', 'northernLights'])
contains(background, key, 'zh');
const keyboard = await scenario('zh-keyboard', 'apps,type:Netflix');
for (const key of ['findApp', 'space', 'delete', 'clear', 'done']) contains(keyboard, key, 'zh');
const weatherSettings = await scenario('zh-weather-settings', 'settings,down,down,down,enter');
for (const key of ['showWidget', 'location', 'temperature']) contains(weatherSettings, key, 'zh');
assert.ok(
!weatherSettings.ui_text.some((label) => label.includes('Open-Meteo')),
'Weather credit is not shown in the UI',
);
await scenario('zh-4k', 'home,down', ['--capture-scale', '2']);
await fs.writeFile(
preferences,
JSON.stringify({ language: 'en', show_home_names: true, reduced_motion: true }),
);
const portuguese = await scenario('pt-home', `${picker},down,down,down,down,enter,home,down`);
assert.equal(portuguese.preferences.language, 'pt');
for (const key of [
'home',
'apps',
'settings',
'favorites',
'inputsHeading',
'evening',
'setWeatherLocation',
])
contains(portuguese, key, 'pt');
assert.ok(portuguese.ui_text.includes('QUARTA-FEIRA, 21 DE OUTUBRO'));
assert.ok(
portuguese.ui_text.includes('Netflix'),
'Installed app names stay intact in Portuguese',
);
const portugueseSettings = await scenario('pt-settings', 'settings,down');
assert.equal(
portugueseSettings.preferences.language,
'pt',
'Portuguese is restored after restart',
);
assert.ok(portugueseSettings.ui_text.includes('Português (Brasil)'));
for (const key of ['language', 'appNames', 'clock']) contains(portugueseSettings, key, 'pt');
assert.equal(JSON.parse(await fs.readFile(preferences, 'utf8')).language, 'pt');
const portugueseDialog = await scenario('pt-dialog', 'apps,down,menu');
for (const key of ['removeFavorite', 'hideApp', 'categoryStreaming'])
contains(portugueseDialog, key, 'pt');
const portugueseKeyboard = await scenario('pt-keyboard', 'apps,type:Netflix');
for (const key of ['findApp', 'space', 'delete', 'clear', 'done'])
contains(portugueseKeyboard, key, 'pt');
const portugueseInputs = await scenario(
'pt-input-options',
'settings,down,down,down,down,enter,enter',
);
for (const key of ['stateOn', 'stateOff', 'stateAuto', 'inputWhenConnected'])
contains(portugueseInputs, key, 'pt');
const portugueseWeather = await scenario(
'pt-weather',
`${location},type:Viena,back,enter,await-search,down,enter,home,await-weather`,
);
assert.equal(portugueseWeather.preferences.language, 'pt');
assert.equal(portugueseWeather.preferences.weather_location.name, 'Viena');
assert.equal(portugueseWeather.preferences.weather_location.country, 'Áustria');
contains(portugueseWeather, 'partlyCloudy', 'pt');
assert.equal(
requests.filter((request) => request.path === '/v1/search').at(-1).query.language,
'pt',
'Geocoding must use the Portuguese locale',
);
console.log(
'Native translation checks passed: saved language selection, shared labels, Portuguese locale formatting and accented glyphs, localized geocoding/weather, unchanged app names, Chinese glyphs and 4K rendering.',
);
} finally {
server.closeAllConnections();
await new Promise((resolve) => server.close(resolve));
await fs.rm(temporary, { recursive: true, force: true });
}