Updated tests with localization
Sonarqube Scanner / Prepare environment (push) Successful in 9s
Sonarqube Scanner / Build and test (push) Successful in 25s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m16s
Sonarqube Scanner / Deploy to Server (push) Successful in 9s

This commit is contained in:
Florian Zumpe
2026-10-01 10:47:43 +02:00
parent aff88eba1b
commit 2b42507f13
7 changed files with 47 additions and 13 deletions
-4
View File
@@ -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;
}
+2 -2
View File
@@ -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"\)/);
});
+7 -3
View File
@@ -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': '<Choose a trail>' });
i18n.setLanguage('en', root);
assert.equal(text.textContent, '<Choose a trail>');
for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), '<Choose a trail>');
assert.equal(root.documentElement.lang, 'en');
assert.equal(text.attributes.get('data-i18n'), 'app.strecke.wahlen');
});
+1 -1
View File
@@ -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'\)/);
+1 -1
View File
@@ -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/);
+35 -1
View File
@@ -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');
}
});
+1 -1
View File
@@ -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\(\)/);