import { localizedHtml } from '../test-support/i18n.js'; import { loadI18n } from '../test-support/i18n.js'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import vm from 'node:vm'; import test from 'node:test'; const root = path.resolve(import.meta.dirname, '..'); async function messages() { const context = {}; const filename = path.join(root, 'public/js/user-messages.js'); await loadI18n(context); vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename }); return context.Wegwichtel.UserMessages; } test('visible errors explain file, permission and storage problems without exposing technical replies', async () => { const ui = await messages(); assert.match(ui.error({ message: 'Die GPX-Datei ist kein gültiges XML.' }), /keinen lesbaren Weg/); assert.match(ui.error({ message: JSON.stringify({ message: 'POI nicht gefunden.', details: 'internal' }) }), /Diese Station wurde nicht gefunden/); assert.match(ui.error({ responseJSON: { message: 'Freigegebene Strecken dürfen nur durch einen direkten lokalen Serveraufruf gelöscht werden.' } }), /Person löschen/); assert.match(ui.error({ responseJSON: { message: 'Freigegebene Strecken können nicht mehr verändert werden.' } }), /nicht mehr verändern/); assert.match(ui.error({ message: 'Nicht unterstütztes Bildformat: image/unknown' }), /anderes Bild/); assert.match(ui.error({ message: 'Nicht unterstütztes Audioformat: audio/unknown' }), /andere Aufnahme/); assert.match(ui.error({ message: 'POI-Koordinaten sind ungültig.' }), /Ort dieser Station/); assert.match(ui.error({ status: 413 }), /zu groß/); for (const status of [401, 403]) assert.match(ui.error({ status }), /Du darfst/); assert.match(ui.error({ status: 500 }), /später/); assert.match(ui.error({ name: 'QuotaExceededError' }), /Platz ist voll/); assert.match(ui.error({ cause: { name: 'QuotaExceededError' } }), /Platz ist voll/); assert.equal(ui.error(new Error('Gib deiner Station einen Namen.')), 'Gib deiner Station einen Namen.'); for (const error of [new Error('Failed to fetch'), { message: 'sequence muss eine Ganzzahl sein.' }, { message: 'HTTP 500 /internal/path' }, {}, 'undefined', '{"details":"raw"}']) { assert.equal(ui.error(error, 'Bitte noch einmal versuchen.'), 'Bitte noch einmal versuchen.'); } }); test('public help and spoken labels omit implementation terms and preserve meaningful storage guidance', async () => { const html = await localizedHtml(await fs.readFile(path.join(root, 'public/index.html'), 'utf8')); const text = html.replace(/<[^>]*>/g, ' '); const labels = [...html.matchAll(/(?:aria-label|title|placeholder)="([^"]+)"/g)].map(match => match[1]).join(' '); assert.doesNotMatch(text + labels, /\b(?:GPX|JSON|Cache|API|OPFS|IndexedDB|Local Storage|Server|Token|HTTP|JavaScripts|jQuery)\b/i); assert.match(text, /Für unterwegs speichern/); assert.match(text, /Weg als Datei herunterladen/); assert.match(text, /Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten/); assert.match(text, /Fortschritt.*gelöscht/s); assert.match(text, /Meine Entwürfe/); }); test('English errors keep translated validation hints and hide technical replies', async () => { const context = { navigator: { language: 'en-GB' } }; await loadI18n(context); vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/user-messages.js'), 'utf8'), context); const { I18n, UserMessages } = context.Wegwichtel; assert.equal(UserMessages.error(new Error(I18n.t('editor.error.route-name'))), 'Give your trail a name.'); assert.equal(UserMessages.error({ status: 413 }), 'This file is too large. Choose a smaller file.'); assert.match(UserMessages.error({ message: 'Die GPX-Datei ist kein gültiges XML.' }), /readable path/); assert.equal(UserMessages.error(new Error('Failed to fetch')), 'That did not work. Please try again.'); });