From 2b42507f1338462739fa357933bb115a91993c82 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Thu, 1 Oct 2026 10:47:43 +0200 Subject: [PATCH] Updated tests with localization --- test-support/i18n.js | 4 ---- test/bootstrap-start-ui.test.js | 4 ++-- test/i18n.test.js | 10 ++++++--- test/legal-ui.test.js | 2 +- test/mobile-navigation-ui.test.js | 2 +- test/offline-cache.test.js | 36 ++++++++++++++++++++++++++++++- test/route-selection-ui.test.js | 2 +- 7 files changed, 47 insertions(+), 13 deletions(-) diff --git a/test-support/i18n.js b/test-support/i18n.js index fb9721e..457a0b9 100644 --- a/test-support/i18n.js +++ b/test-support/i18n.js @@ -32,9 +32,5 @@ export async function localizedHtml(source) { export async function readAppFile(relativePath) { const source = await fs.readFile(path.join(root, relativePath), 'utf8'); if (relativePath === 'public/index.html') return localizedHtml(source); - if (relativePath.startsWith('public/js/')) { - // Static text assertions read the centralized wording; runtime tests use the real class. - return source + '\n' + await fs.readFile(path.join(root, 'public/js/texts-de.js'), 'utf8'); - } return source; } diff --git a/test/bootstrap-start-ui.test.js b/test/bootstrap-start-ui.test.js index c2147e1..e763a20 100644 --- a/test/bootstrap-start-ui.test.js +++ b/test/bootstrap-start-ui.test.js @@ -59,6 +59,6 @@ test('Android start hint distinguishes Chrome from other Android browsers', asyn assert.match(loader, /getElementById\('android-browser-hint'\)/); assert.match(loader, /\/Android\/i/); assert.match(loader, /\/Chrome\\\//i); - assert.match(loader, /Auf diesem Gerät kannst du Bilder und Tonaufnahmen direkt auswählen/); - assert.match(loader, /Tipp für Android: Mit Chrome/); + assert.match(loader, /I18n\.t\("bootstrap-loader\.auf\.diesem\.gerat\.kannst\.du\.bilder\.und"\)/); + assert.match(loader, /I18n\.t\("bootstrap-loader\.tipp\.fur\.android\.mit\.chrome\.lassen\.sich"\)/); }); diff --git a/test/i18n.test.js b/test/i18n.test.js index 9864bf1..8ca9caa 100644 --- a/test/i18n.test.js +++ b/test/i18n.test.js @@ -26,18 +26,22 @@ test('language changes translate visible and spoken labels through textContent a const i18n = await loadI18n({}); const make = key => ({ textContent: '', attributes: new Map(key), getAttribute(name) { return this.attributes.get(name); }, setAttribute(name, value) { this.attributes.set(name, value); } }); const text = make([['data-i18n', 'app.strecke.wahlen']]); - const label = make([['data-i18n-aria-label', 'app.strecke.wahlen']]); + const attributes = ['aria-label', 'title', 'placeholder', 'alt']; + const labels = new Map(attributes.map(attribute => [attribute, make([[`data-i18n-${attribute}`, 'app.strecke.wahlen']])])); const root = { documentElement: {}, querySelectorAll(selector) { if (selector === '[data-i18n]') return [text]; - if (selector === '[data-i18n-aria-label]') return [label]; + for (const [attribute, label] of labels) { + if (selector === `[data-i18n-${attribute}]`) return [label]; + } return []; } }; i18n.apply(root); assert.equal(text.textContent, 'Strecke wählen'); - assert.equal(label.attributes.get('aria-label'), 'Strecke wählen'); + for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), 'Strecke wählen'); i18n.register('en', { 'app.strecke.wahlen': '' }); i18n.setLanguage('en', root); assert.equal(text.textContent, ''); + for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), ''); assert.equal(root.documentElement.lang, 'en'); assert.equal(text.attributes.get('data-i18n'), 'app.strecke.wahlen'); }); diff --git a/test/legal-ui.test.js b/test/legal-ui.test.js index 2c99861..c086b55 100644 --- a/test/legal-ui.test.js +++ b/test/legal-ui.test.js @@ -23,7 +23,7 @@ test('start screen exposes cookie status, optional local progress and deliberate assert.match(html, /role="switch"[^>]*aria-checked="false"/); assert.match(html, /id="storage-cleanup-status"/); assert.match(html, /id="bootstrap-enter"[^>]*hidden/); - assert.match(loader, /Alten Fortschritt löschen \(\{value1\} \/ \{value2\}\)/); + assert.match(loader, /I18n\.t\("bootstrap-loader\.alten\.fortschritt\.loschen", \{ value1: removed, value2: total \}\)/); assert.match(loader, /showReadyState\(\)/); assert.doesNotMatch(loader, /getElementById\('bootstrap-screen'\)\.hidden = true/); assert.match(loader, /event\.target\.closest\('button, input, label, a, dialog'\)/); diff --git a/test/mobile-navigation-ui.test.js b/test/mobile-navigation-ui.test.js index b7e88e4..1f0c255 100644 --- a/test/mobile-navigation-ui.test.js +++ b/test/mobile-navigation-ui.test.js @@ -153,7 +153,7 @@ test('Stationen have live direction and metre indicators and take navigation pri assert.match(app, /function updatePoiIndicators\(position\)/); assert.match(app, /function nearestPendingPoi\(metrics\)/); assert.match(app, /if \(nextPoi\) showPoiNavigation\(position, nextPoi\)/); - assert.match(app, /Nächste Station:/); + assert.match(app, /I18n\.t\("app\.nachste\.station\.2", \{ value1: metric\.poi\.title \}\)/); assert.match(app, /find\('small'\)\.text\(`\$\{distance\} m`\)/); assert.match(app, /find\('svg'\)\.css\('transform'/); assert.match(css, /\.poi-proximity[\s\S]*grid-column:\s*2/); diff --git a/test/offline-cache.test.js b/test/offline-cache.test.js index 87d532d..3d77acf 100644 --- a/test/offline-cache.test.js +++ b/test/offline-cache.test.js @@ -400,7 +400,10 @@ test('storage gauge uses browser estimate, separates Local Storage and reports q const usage = context.Wegwichtel.StorageUsage; await usage.refresh(); assert.equal(elements.get('[data-storage-site-meter]').value, 95 * 1024 * 1024); - assert.match(elements.get('[data-storage-site-text]').textContent, /95 %.*5,2 MB frei.*Fast voll/); + const freeSpace = (5 * 1024 * 1024 / 1000000).toLocaleString('de', { maximumFractionDigits: 1 }); + const siteText = elements.get('[data-storage-site-text]').textContent; + assert.match(siteText, /95 %.*Fast voll/); + assert.ok(siteText.includes(`${freeSpace} MB frei`), siteText); assert.match(elements.get('[data-storage-local-text]').textContent, /Richtwert.*wirklich hineinpasst/); usage.reportError({ cause: { name: 'QuotaExceededError' } }); await usage.refresh(); @@ -409,3 +412,34 @@ test('storage gauge uses browser estimate, separates Local Storage and reports q await usage.refresh(); assert.equal(elements.get('[data-storage-site-meter]').hidden, true); }); + +test('storage gauge uses the selected language with both localized and fallback number formatting', async () => { + const source = await fs.readFile(path.join(root, 'public/js/storage-usage.js'), 'utf8'); + for (const decimalSeparator of [',', '.']) { + const label = {}; + const calls = []; + const context = { + document: { querySelectorAll: selector => selector === '[data-storage-usage]' ? [{ + querySelector: selector => selector === '[data-storage-site-text]' ? label : null + }] : [] }, + navigator: { storage: { estimate: async () => ({ usage: 95 * 1024 * 1024, quota: 100 * 1024 * 1024 }) } }, + formatNumber(value, language, options) { + calls.push({ value, language, options }); + return value.toFixed(1).replace('.', decimalSeparator); + } + }; + const i18n = await loadI18n(context); + // Model a runner whose Intl data falls back to a dot, without changing host globals. + vm.runInNewContext('Number.prototype.toLocaleString = function (language, options) { return formatNumber(Number(this), language, options); };', context); + vm.runInNewContext(source, context); + await context.Wegwichtel.StorageUsage.refresh(); + assert.ok(label.textContent.includes(`5${decimalSeparator}2 MB frei`)); + assert.equal(calls[0].language, 'de'); + assert.equal(calls[0].options.maximumFractionDigits, 1); + assert.equal(calls[0].value, 5 * 1024 * 1024 / 1000000); + i18n.register('en', {}); + i18n.setLanguage('en', null); + await context.Wegwichtel.StorageUsage.refresh(); + assert.equal(calls.at(-1).language, 'en'); + } +}); diff --git a/test/route-selection-ui.test.js b/test/route-selection-ui.test.js index 993448a..1c7f642 100644 --- a/test/route-selection-ui.test.js +++ b/test/route-selection-ui.test.js @@ -56,7 +56,7 @@ test('persistent route control displays the next unvisited Station and preserves const app = await read('public/js/app.js'); assert.match(app, /function nextPoiMetric\(/); - assert.match(app, /Nächste Station:/); + assert.match(app, /I18n\.t\("app\.nachste\.station", \{ value1: metric\.poi\.title, value2: distance \}\)/); assert.match(app, /state\.triggered\.has\(poi\.id\)/); assert.match(app, /function suspendCurrentRoute\(/); assert.match(app, /persistRouteProgress\(\)/);