265 lines
10 KiB
JavaScript
265 lines
10 KiB
JavaScript
import { readAppFile } 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 test from 'node:test';
|
|
import vm from 'node:vm';
|
|
|
|
const root = path.resolve(import.meta.dirname, '..');
|
|
|
|
async function read(relativePath) {
|
|
return readAppFile(relativePath);
|
|
}
|
|
|
|
test('start screen exposes cookie status, optional local progress and deliberate entry', async () => {
|
|
const html = await read('public/index.html');
|
|
const loader = await read('public/js/bootstrap-loader.js');
|
|
const storage = await read('public/js/privacy-storage.js');
|
|
|
|
assert.match(html, /Wegwichtel verwendet keine Cookies/);
|
|
assert.match(html, /class="cookie-toggle"[^>]*disabled/);
|
|
assert.match(html, /class="progress-storage-toggle"/);
|
|
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, /I18n\.t\("bootstrap-loader\.cleanup-progress", \{ 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'\)/);
|
|
assert.match(storage, /MAX_PROGRESS_AGE_MS = 24 \* 60 \* 60 \* 1000/);
|
|
assert.match(storage, /cookiesEnabled: \(\) => false/);
|
|
});
|
|
|
|
test('information button opens scrollable overlay pages for privacy, imprint, terms and technology', async () => {
|
|
const html = await read('public/index.html');
|
|
const css = await read('public/css/app.css');
|
|
const ui = await read('public/js/legal-ui.js');
|
|
|
|
assert.match(html, /id="info-button"/);
|
|
assert.match(html, /id="info-dropdown"/);
|
|
assert.match(html, /<dialog id="info-dialog"/);
|
|
assert.match(html, /data-info-target="privacy"/);
|
|
assert.match(html, /data-info-target="imprint"/);
|
|
assert.match(html, /data-info-target="terms"/);
|
|
assert.match(html, /data-info-target="technology"/);
|
|
assert.match(html, /<h3>Technisch verantwortlich<\/h3>\s*<p>Florian Zumpe<br>/);
|
|
assert.match(html, /\[Anschrift von Florian Zumpe ergänzen\]/);
|
|
assert.match(html, /E-Mail: <a href="mailto:info@wegwichtel\.de">info@wegwichtel\.de<\/a>/);
|
|
assert.doesNotMatch(html, /\[E-Mail-Adresse von Florian Zumpe ergänzen\]|\[Kontakt für Rechtefragen ergänzen\]/);
|
|
const storage = await read('public/js/privacy-storage.js');
|
|
assert.match(storage, /wegwichtel\.privacy\.v1/);
|
|
assert.match(storage, /wegwichtel\.route-progress\.v1/);
|
|
assert.match(html, /Speichern und Berechtigungen/);
|
|
assert.doesNotMatch(html, /Local Storage|GPX|jQuery 4\.0\.0/);
|
|
assert.match(css, /\.info-dialog-content[\s\S]*overflow-y:\s*auto/);
|
|
assert.match(css, /\.info-dialog-content[\s\S]*touch-action:\s*pan-y/);
|
|
assert.match(ui, /pointerup/);
|
|
assert.match(ui, /keydown/);
|
|
assert.doesNotMatch(ui, /addEventListener\(['"]click['"]/);
|
|
});
|
|
|
|
test('application persists current route, viewed stations and played audio only through privacy storage', async () => {
|
|
const app = await read('public/js/app.js');
|
|
|
|
assert.match(app, /PrivacyStorage\.saveRouteState/);
|
|
assert.match(app, /PrivacyStorage\.activateRoute/);
|
|
assert.match(app, /state\.viewed\.add\(state\.activePoiId\)/);
|
|
assert.match(app, /\$\('#poi-audio'\)\.on\('play'/);
|
|
assert.match(app, /state\.playedAudio\.add\(state\.activePoiId\)/);
|
|
assert.match(app, /globalThis\.addEventListener\('pagehide', persistRouteProgress\)/);
|
|
assert.match(app, /await initializeLocation\(context\)/);
|
|
});
|
|
|
|
class FakeElement extends EventTarget {
|
|
constructor(id = '') {
|
|
super();
|
|
this.id = id;
|
|
this.hidden = false;
|
|
this.checked = false;
|
|
this.disabled = false;
|
|
this.open = false;
|
|
this.textContent = '';
|
|
this.scrollTop = 0;
|
|
this.dataset = {};
|
|
this.attributes = new Map();
|
|
this.focused = false;
|
|
this.classes = new Set();
|
|
this.classList = {
|
|
toggle: (name, enabled) => enabled ? this.classes.add(name) : this.classes.delete(name)
|
|
};
|
|
}
|
|
|
|
focus() {
|
|
this.focused = true;
|
|
}
|
|
|
|
setAttribute(name, value) {
|
|
this.attributes.set(name, String(value));
|
|
if (name === 'open') this.open = true;
|
|
}
|
|
|
|
removeAttribute(name) {
|
|
this.attributes.delete(name);
|
|
if (name === 'open') this.open = false;
|
|
}
|
|
|
|
showModal() {
|
|
this.open = true;
|
|
}
|
|
|
|
close() {
|
|
this.open = false;
|
|
}
|
|
}
|
|
|
|
function pointerEvent() {
|
|
const event = new Event('pointerup', { bubbles: true, cancelable: true });
|
|
Object.defineProperties(event, {
|
|
isPrimary: { value: true },
|
|
pointerType: { value: 'touch' },
|
|
button: { value: 0 }
|
|
});
|
|
return event;
|
|
}
|
|
|
|
test('legal overlay handlers synchronize switches without advancing the start screen', async () => {
|
|
const sourcePath = path.join(root, 'public/js/legal-ui.js');
|
|
const source = await fs.readFile(sourcePath, 'utf8');
|
|
const elements = new Map([
|
|
['info-dialog', new FakeElement('info-dialog')],
|
|
['info-dialog-title', new FakeElement('info-dialog-title')],
|
|
['info-menu', new FakeElement('info-menu')],
|
|
['info-back', new FakeElement('info-back')],
|
|
['info-close', new FakeElement('info-close')],
|
|
['info-button', new FakeElement('info-button')],
|
|
['info-dropdown', new FakeElement('info-dropdown')]
|
|
]);
|
|
const content = new FakeElement();
|
|
const privacyPage = new FakeElement();
|
|
const imprintPage = new FakeElement();
|
|
const technologyPage = new FakeElement();
|
|
const termsPage = new FakeElement();
|
|
const pages = { privacy: privacyPage, imprint: imprintPage, terms: termsPage, technology: technologyPage };
|
|
const targetButtons = Object.keys(pages).map(name => {
|
|
const button = new FakeElement();
|
|
button.dataset.infoTarget = name;
|
|
return button;
|
|
});
|
|
const progressToggles = [new FakeElement(), new FakeElement()];
|
|
const cookieToggles = [new FakeElement()];
|
|
let enabled = false;
|
|
const changes = [];
|
|
const frames = [];
|
|
function flushFrames() {
|
|
while (frames.length) frames.shift()();
|
|
}
|
|
|
|
class TestCustomEvent extends Event {
|
|
constructor(type, options = {}) {
|
|
super(type);
|
|
this.detail = options.detail;
|
|
}
|
|
}
|
|
|
|
const document = new EventTarget();
|
|
document.readyState = 'complete';
|
|
document.getElementById = id => elements.get(id) || null;
|
|
document.querySelectorAll = selector => {
|
|
if (selector === '.progress-storage-toggle') return progressToggles;
|
|
if (selector === '.cookie-toggle') return cookieToggles;
|
|
if (selector === '[data-info-page]') return Object.values(pages);
|
|
if (selector === '[data-info-target]') return targetButtons;
|
|
return [];
|
|
};
|
|
document.querySelector = selector => {
|
|
if (selector === '.info-dialog-content') return content;
|
|
const match = selector.match(/^\[data-info-page="(.+)"\]$/);
|
|
return match ? pages[match[1]] : null;
|
|
};
|
|
document.addEventListener('wegwichtel:progress-storage-changed', event => changes.push(event.detail.enabled));
|
|
|
|
const context = {
|
|
Wegwichtel: {
|
|
PrivacyStorage: {
|
|
isProgressStorageEnabled: () => enabled,
|
|
setProgressStorageEnabled: value => {
|
|
enabled = value;
|
|
return enabled;
|
|
}
|
|
}
|
|
},
|
|
document,
|
|
Event,
|
|
EventTarget,
|
|
CustomEvent: TestCustomEvent,
|
|
requestAnimationFrame: callback => frames.push(callback)
|
|
};
|
|
context.globalThis = context;
|
|
await loadI18n(context);
|
|
vm.runInNewContext(source, context, { filename: sourcePath });
|
|
|
|
elements.get('info-dialog-title').dataset.i18n = 'info.title';
|
|
context.Wegwichtel.I18n.setLanguage('en');
|
|
|
|
elements.get('info-button').dispatchEvent(pointerEvent());
|
|
assert.equal(elements.get('info-dropdown').hidden, false);
|
|
assert.equal(elements.get('info-button').attributes.get('aria-expanded'), 'true');
|
|
assert.equal(elements.get('info-dialog').open, false);
|
|
assert.equal(elements.get('info-dropdown').classes.has('is-open'), false);
|
|
frames.shift()();
|
|
assert.equal(elements.get('info-dropdown').classes.has('is-open'), false);
|
|
frames.shift()();
|
|
assert.equal(elements.get('info-dropdown').classes.has('is-open'), true);
|
|
|
|
elements.get('info-button').dispatchEvent(pointerEvent());
|
|
assert.equal(elements.get('info-dropdown').classes.has('is-open'), false);
|
|
elements.get('info-button').dispatchEvent(pointerEvent());
|
|
elements.get('info-button').dispatchEvent(pointerEvent());
|
|
flushFrames();
|
|
assert.equal(elements.get('info-dropdown').hidden, true);
|
|
assert.equal(elements.get('info-dropdown').classes.has('is-open'), false);
|
|
|
|
elements.get('info-button').dispatchEvent(pointerEvent());
|
|
flushFrames();
|
|
|
|
targetButtons[0].dispatchEvent(pointerEvent());
|
|
assert.equal(elements.get('info-dialog').open, true);
|
|
assert.equal(elements.get('info-dropdown').hidden, true);
|
|
assert.equal(elements.get('info-menu').hidden, true);
|
|
assert.equal(elements.get('info-back').hidden, true);
|
|
assert.equal(privacyPage.hidden, false);
|
|
assert.equal(elements.get('info-dialog-title').textContent, 'Datenschutzerklärung');
|
|
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'de');
|
|
assert.equal(Object.hasOwn(elements.get('info-dialog-title').dataset, 'i18n'), false);
|
|
|
|
targetButtons[1].dispatchEvent(pointerEvent());
|
|
assert.equal(elements.get('info-dialog-title').textContent, 'Impressum');
|
|
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'de');
|
|
|
|
targetButtons[2].dispatchEvent(pointerEvent());
|
|
assert.equal(termsPage.hidden, false);
|
|
assert.equal(imprintPage.hidden, true);
|
|
assert.equal(elements.get('info-dialog-title').textContent, 'Nutzungsvereinbarung');
|
|
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'de');
|
|
assert.equal(Object.hasOwn(elements.get('info-dialog-title').dataset, 'i18n'), false);
|
|
|
|
targetButtons[3].dispatchEvent(pointerEvent());
|
|
assert.equal(technologyPage.hidden, false);
|
|
assert.equal(elements.get('info-dialog-title').textContent, 'Saving and permissions');
|
|
assert.equal(elements.get('info-dialog-title').attributes.get('lang'), 'en');
|
|
|
|
progressToggles[0].checked = true;
|
|
progressToggles[0].dispatchEvent(new Event('change'));
|
|
assert.deepEqual(changes, [true]);
|
|
assert.equal(progressToggles[0].checked, true);
|
|
assert.equal(progressToggles[1].checked, true);
|
|
assert.equal(progressToggles[0].attributes.get('aria-checked'), 'true');
|
|
assert.equal(progressToggles[1].attributes.get('aria-checked'), 'true');
|
|
assert.equal(cookieToggles[0].checked, false);
|
|
assert.equal(cookieToggles[0].attributes.get('aria-checked'), 'false');
|
|
|
|
elements.get('info-close').dispatchEvent(pointerEvent());
|
|
assert.equal(elements.get('info-dialog').open, false);
|
|
});
|