Files

313 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const contract = require('../renderer/modules/navigation-routes');
const root = path.join(__dirname, '..');
const html = fs.readFileSync(path.join(root, 'renderer', 'index.html'), 'utf8');
const navigationSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'navigation.js'), 'utf8');
const homeSource = `${navigationSource}\n${html}`;
const i18nSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'i18n.js'), 'utf8');
const initSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'init.js'), 'utf8');
const bootstrapSource = fs.readFileSync(path.join(root, 'renderer', 'bootstrap.js'), 'utf8');
const navigationCss = fs.readFileSync(path.join(root, 'renderer', 'styles', 'navigation.css'), 'utf8');
const mediaSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'media.js'), 'utf8');
const transfersSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'transfers.js'), 'utf8');
const systemViewSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'system-view.js'), 'utf8');
const gamesSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'games.js'), 'utf8');
const cssCoverSource = fs.readFileSync(path.join(root, 'renderer', 'styles', 'game-covers.css'), 'utf8');
const premiumCss = fs.readFileSync(path.join(root, 'renderer', 'styles', 'premium.css'), 'utf8');
const premiumIllustrationsSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'premium-illustrations.js'), 'utf8');
const mobileCompanionCss = fs.readFileSync(path.join(root, 'renderer', 'styles', 'mobile-companion.css'), 'utf8');
const trophiesSource = fs.readFileSync(path.join(root, 'src', 'renderer', 'trophies.js'), 'utf8');
const modalSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'modal.js'), 'utf8');
const payloadSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'payload.js'), 'utf8');
const ecosystemSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'ecosystem.js'), 'utf8');
const stateSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'state.js'), 'utf8');
const webShimSource = fs.readFileSync(path.join(root, 'renderer', 'pork-web-shim.js'), 'utf8');
const english = JSON.parse(fs.readFileSync(path.join(root, 'build', 'lang', 'en.json'), 'utf8'));
const dutch = JSON.parse(fs.readFileSync(path.join(root, 'build', 'lang', 'nl.json'), 'utf8'));
for (const platform of ['all', 'ps4', 'ps5', 'unknown']) {
assert(html.includes(`data-game-platform="${platform}"`), `gamebibliotheek mist platformfilter ${platform}`);
}
assert(stateSource.includes("'chimera.games.platform'"), 'platformkeuze moet tussen sessies bewaard blijven');
assert(gamesSource.includes("state.platformFilter !== 'all'"), 'platformfilter moet met de overige gamefilters combineren');
assert(gamesSource.includes("return 'unknown'"), 'onbekende toekomstige title-ID-formaten mogen niet stil als PS4 of PS5 worden gelabeld');
assert(!modalSource.includes('remoteGamePaths.length'), 'Plan install mag niet afhangen van de routegebonden Settings-globaal');
assert(modalSource.includes('Array.isArray(s.remoteGamePaths)'), 'Plan install moet Remote Game Paths rechtstreeks uit instellingen lezen');
assert(gamesSource.includes("const coverUrl = g.prospero_cover_url || ''"), 'gamecards moeten de afzonderlijk verrijkte portretcover gebruiken');
assert(gamesSource.includes('versionedArtworkUrl(coverUrl, g.cover_fetched_at)'), 'gamecovers moeten na verrijking browsercache omzeilen');
assert(gamesSource.includes("game-row-icon${renderedCoverUrl ? ' is-portrait' : ''}"), 'lijstweergave moet de portretcover gebruiken wanneer die beschikbaar is');
assert(gamesSource.includes("'--game-cover-ratio'"), 'iedere echte cover moet zijn natuurlijke gevalideerde verhouding bepalen');
assert(gamesSource.includes('game-card-cover-case has-native-cover'), 'echte portretcovers moeten beeldvullend in de verticale case staan');
assert(gamesSource.includes('game-card-cover-backdrop'), 'de vierkante Store-icon mag alleen als tijdelijke fallback beschikbaar blijven');
assert(gamesSource.includes('data-fallback-src'), 'een mislukte portretcover moet eerst naar de beschikbare Store-icon terugvallen');
assert(stateSource.includes('fallbackAttempted'), 'de globale image-handler moet coverfallbacks maximaal eenmaal proberen');
assert(gamesSource.includes('game-card-cover-fallback'), 'games zonder bruikbare artwork moeten een titelplaceholder tonen');
// De cascade hing aan de volgorde in index.html zonder dat iets die bewaakte.
// Nu een afdwingbaar contract: elke laag komt na de laag die zij corrigeert,
// en de correctielaag sluit de rij.
const styleOrder = [...html.matchAll(/<link[^>]+rel=\"stylesheet\"[^>]+href=\"(styles\/[a-z0-9-]+\.css)\"/gi)].map(m => m[1]);
const styleIndex = name => styleOrder.indexOf(`styles/${name}`);
for (const sheet of ['base.css', 'stitch.css', 'game-covers.css', 'premium.css', 'chimera-system.css']) {
assert(styleIndex(sheet) >= 0, `stijllaag ${sheet} moet in index.html geladen worden`);
}
assert(styleIndex('game-covers.css') > styleIndex('stitch.css'), 'de portretcoverlaag moet na de Stitch-laag laden');
assert(styleIndex('premium.css') > styleIndex('game-covers.css'), 'de premium afwerklaag moet na de portretcoverlaag laden');
assert(styleIndex('chimera-system.css') === styleOrder.length - 1, 'de correctielaag moet als laatste laden: zij corrigeert alle lagen erboven');
// Een :focus-visible-regel die samen met :hover de outline op 0 zet, wist de
// toetsenbord-focusindicator. Dat gebeurde in zes componentfamilies tegelijk.
const styleFiles = ['base.css', 'components.css', 'features.css', 'navigation.css', 'pages.css',
'premium.css', 'stitch.css', 'protection-copies.css', 'chimera-system.css'];
for (const sheet of styleFiles) {
const cssPath = path.join(root, 'renderer', 'styles', sheet);
if (!fs.existsSync(cssPath)) continue;
const css = fs.readFileSync(cssPath, 'utf8');
for (const rule of css.match(/[^{}]*:focus-visible[^{]*\{[^}]*\}/g) || []) {
assert(!/outline\s*:\s*(0|none)\b/.test(rule),
`${sheet} zet de focusring uit op een :focus-visible-regel: ${rule.slice(0, 90)}`);
}
}
assert(premiumCss.includes('.game-card-cover-fallback'), 'de premium laag moet de coverplaceholder vormgeven');
assert(
premiumCss.includes('body.chimera-shell-v2 #page-trophies>.trophy-page-header{display:none}')
&& premiumCss.includes('body.chimera-shell-v2[data-chimera-route="protection/trophies"] #chimera-route-refresh{display:inline-flex}')
&& premiumCss.includes('#chimera-route-refresh::before{content:"↻"')
&& trophiesSource.includes("getElementById('chimera-route-refresh')"),
'Trophies moet de shellheader en refreshactie hergebruiken zonder een tweede losse Refresh-knop',
);
assert(bootstrapSource.includes("'modules/premium-illustrations.js?v=sol-ultra-1'"), 'interactieve projectillustraties moeten met een cacheveilige kernshell-URL laden');
for (const target of ['games-empty', 'backups-empty', 'transfers-empty', 'media-empty']) {
assert(premiumIllustrationsSource.includes(target), `premium illustraties missen doel ${target}`);
}
for (const variant of ['library', 'protection', 'transfer', 'media', 'trophies']) {
assert(
premiumIllustrationsSource.includes(`'${variant}':`),
`premium illustraties missen een eigen projectspecifiek template voor ${variant}`,
);
}
assert(
premiumIllustrationsSource.includes("querySelector('.trophy-library-empty')"),
'de PS4 trophybibliotheek moet een projectspecifieke lege-state-illustratie krijgen',
);
assert(
premiumIllustrationsSource.includes("document.getElementById('content') || document.body")
&& premiumIllustrationsSource.includes('touchesTrophyLibrary'),
'de Trophy-illustratie moet ook na late rendering of vervanging van de featurepagina mounten',
);
assert(
premiumIllustrationsSource.includes("document.addEventListener('visibilitychange'"),
'projectillustraties moeten animatie pauzeren wanneer het document niet zichtbaar is',
);
assert(
premiumIllustrationsSource.includes("reducedMotion.addEventListener('change'"),
'projectillustraties moeten live reageren wanneer reduced motion verandert',
);
assert(premiumIllustrationsSource.includes('prefers-reduced-motion'), 'projectillustraties moeten reduced motion respecteren');
assert(premiumCss.includes('.premium-project-illustration'), 'de premium laag moet projectillustraties vormgeven');
assert(premiumCss.includes('.premium-project-packet'), 'de premium laag moet semantische datastroomanimatie vormgeven');
assert(
premiumCss.includes('.premium-project-illustration.is-active:not(.is-paused) :is(.library-proof,.premium-project-shield,.media-play,.trophy-earned)'),
'de bevestigingsanimatie moet de SVG-kinderen selecteren en niet foutief de illustratieroot',
);
assert(
premiumIllustrationsSource.includes('if (!element.isConnected)')
&& premiumIllustrationsSource.includes('mounted.delete(element)'),
'vervangen illustraties moeten uit de langlevende animatieregistratie verdwijnen',
);
assert(
trophiesSource.includes("querySelector('#trophy-library-search')?.addEventListener('input'")
&& trophiesSource.includes('librarySearchTimer = setTimeout'),
'de trophybibliotheek moet zoeken tijdens het typen met een korte debounce',
);
assert(cssCoverSource.includes('.game-card-cover-case.has-native-cover > .game-card-cover:not(.game-card-cover-empty)'), 'native cover override moet specifieker zijn dan de vierkante fallbackregel');
assert(html.includes('styles/mobile-companion.css'), 'de mobiele companion-compositie moet als laatste responsieve laag laden');
assert(navigationSource.includes('mountCompanionDock'), 'de shell moet mobiele companion-snelkoppelingen opbouwen');
assert(navigationSource.includes('chimera-companion-actions'), 'Action Center moet rechtstreeks vanuit de companion-dock bereikbaar zijn');
assert(mobileCompanionCss.includes('grid-template-columns:repeat(5,minmax(0,1fr))'), 'de companion-dock moet vijf voorspelbare duimzones bieden');
assert(mobileCompanionCss.includes('env(safe-area-inset-bottom)'), 'de companion-shell moet rekening houden met mobiele safe areas');
assert(
/body\.mobile-nav-open #mobile-nav-toggle\s*\{[^}]*position:fixed[^}]*z-index:2400[^}]*pointer-events:auto/s.test(mobileCompanionCss)
&& /body\.mobile-nav-open #chimera-shell-toolbar\s*\{[^}]*z-index:2350[^}]*pointer-events:none/s.test(mobileCompanionCss),
'de mobiele navigatieknop moet boven de geopende sidebar als bereikbare sluitactie blijven staan',
);
assert(
stateSource.includes("companionDock.toggleAttribute('inert', isOpen)")
&& stateSource.includes("companionDock.setAttribute('aria-hidden', 'true')"),
'de visueel verborgen mobiele dock moet ook uit toetsenbord- en accessibilitynavigatie verdwijnen',
);
assert(mobileCompanionCss.includes('.game-library-command-deck'), 'Games moet op mobiel opnieuw gecomponeerd worden');
assert(
/#game-catalog-health\s*\{[^}]*display:grid[^}]*grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/s.test(mobileCompanionCss),
'mobiele catalogusmetrics moeten als volledig zichtbare 2x2-compositie renderen',
);
assert(premiumCss.includes('.chimera-import-launcher'), 'Managed import moet al vóór het openen safe-area-aware shellstijlen krijgen');
assert(
premiumIllustrationsSource.includes('sidebarDonate.after(launcher)'),
'Managed import moet in de vaste sidebarfooter docken in plaats van content te bedekken',
);
assert(
/\.chimera-import-launcher\s*\{[^}]*position:static!important/s.test(premiumCss),
'de gedockte importactie moet zijn inline zweefpositie veilig overschrijven',
);
assert.strictEqual(contract.version, 1, 'routecontractversie moet expliciet zijn');
assert.deepStrictEqual(contract.groups.map(group => group.id), ['home', 'library', 'console', 'payloads', 'protection', 'tools', 'settings'], 'de shell moet exact zeven domeinen hebben');
const ids = contract.routes.map(route => route.id);
assert.strictEqual(new Set(ids).size, ids.length, 'canonieke route-ID’s moeten uniek zijn');
assert(contract.routes.length >= 40, 'alle bestaande functionele oppervlakken moeten bereikbaar blijven');
assert.strictEqual(contract.groups.find(group => group.id === 'library').label, 'Library', 'Engels moet de bron- en standaardtaal van het routecontract zijn');
assert.strictEqual(contract.routes.find(route => route.id === 'home').label, 'Overview', 'Home moet zonder taalbestand Engels tonen');
const rendererSources = [];
function collectRendererSources(directory) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) collectRendererSources(file);
else if (/\.(?:html|js)$/.test(entry.name)) rendererSources.push(file);
}
}
collectRendererSources(path.join(root, 'renderer'));
for (const file of rendererSources) {
const source = fs.readFileSync(file, 'utf8');
for (const match of source.matchAll(/data-route=(["'])([^"'$<>]+)\1/g)) {
assert(ids.includes(match[2]), `${path.relative(root, file)} bevat onbekende route ${match[2]}`);
}
for (const match of source.matchAll(/data-(?:focus|payload)-target=(["'])([^"'$<>]+)\1/g)) {
assert(html.includes(`id="${match[2]}"`), `${path.relative(root, file)} verwijst naar ontbrekend focusdoel ${match[2]}`);
}
}
for (const group of contract.groups) {
assert(group.routes.length > 0, `domein ${group.id} mag niet leeg zijn`);
assert(group.routes.some(route => route.id === group.defaultRoute), `standaardroute van ${group.id} moet in het domein bestaan`);
for (const route of group.routes) assert.strictEqual(route.group, undefined, 'bronroutes mogen niet muteren tijdens contractopbouw');
}
for (const route of contract.routes) {
const key = `navigation.routes.${route.id.replace(/\//g, '.')}`;
assert(dutch.strings[`${key}.label`], `Nederlandse routenaam ontbreekt voor ${route.id}`);
assert(dutch.strings[`${key}.description`], `Nederlandse routebeschrijving ontbreekt voor ${route.id}`);
}
for (const key of ['navigation.search.open', 'home.next.connectTitle', 'settingsNavigator.title', 'games.platform.label', 'games.platform.all', 'games.empty.clearFilters', 'ecosystem.saves.notScanned', 'transfers.policy.title', 'operations.storage.title']) {
assert(english.strings[key], `Engelse kernvertaling ontbreekt: ${key}`);
assert(dutch.strings[key], `Nederlandse kernvertaling ontbreekt: ${key}`);
}
const pageIds = new Set([...html.matchAll(/id="page-([^"]+)"/g)].map(match => match[1]));
for (const route of contract.routes) {
assert(pageIds.has(route.page), `${route.id} verwijst naar ontbrekende pagina ${route.page}`);
if (route.page === 'ecosystem') assert(route.tab, `${route.id} mist een ecosysteempaneel`);
if (route.page === 'operations') assert(route.panel, `${route.id} mist een operationspaneel`);
}
const ecosystemTabs = new Set(['overview', 'components', 'updates', 'game-updates', 'etahen', 'live', 'remote-play', 'files', 'homebrew', 'sources', 'plugins', 'cheats', 'saves', 'itemzflow', 'research', 'timeline']);
const coveredEcosystemTabs = new Set(contract.routes.filter(route => route.page === 'ecosystem').map(route => route.tab));
assert.deepStrictEqual([...coveredEcosystemTabs].sort(), [...ecosystemTabs].sort(), 'elk voormalig Hub-paneel moet één canonieke route hebben');
const operationsPanels = new Set([...html.matchAll(/data-operations-panel="([^"]+)"/g)].map(match => match[1]));
const coveredOperationsPanels = new Set(contract.routes.filter(route => route.page === 'operations').map(route => route.panel));
assert.deepStrictEqual([...coveredOperationsPanels].sort(), [...operationsPanels].sort(), 'elk Operations-paneel moet via de geïntegreerde IA bereikbaar zijn');
for (const [legacy, target] of Object.entries(contract.legacyAliases)) {
assert(legacy, 'lege legacy alias');
assert(ids.includes(target), `legacy alias ${legacy} verwijst naar ontbrekende route ${target}`);
}
for (const requiredId of ['payload-local-section', 'payload-sender-section', 'payload-autoloader-section', 'operations-replication-card', 'operations-monitoring-card', 'operations-sessions-card', 'operations-password-card']) {
assert(html.includes(`id="${requiredId}"`), `focusanker ${requiredId} ontbreekt`);
}
assert(html.includes('src="bootstrap.js"'), 'de routegewijze shellbootstrap moet actief zijn');
assert(
bootstrapSource.indexOf("'modules/navigation-routes.js'") < bootstrapSource.indexOf("'modules/navigation.js'"),
'routecontract moet vóór de router laden',
);
assert(bootstrapSource.includes('ensureForPage'), 'featuremodules moeten per route geladen worden');
assert(
bootstrapSource.includes("settings: ['modules/payload.js', 'modules/settings.js']"),
'Settings moet de gedeelde payloadbronrenderer vóór settings.js laden',
);
assert(bootstrapSource.includes("transfers: ['modules/transfers.js']"), 'Transfers moet zijn eigen volledige featuremodule laden');
assert(transfersSource.includes('async function loadTransfers()'), 'de Transfers-featuremodule moet zijn eigen loader exporteren');
assert(transfersSource.includes("'btn-transfer-pause'"), 'de Transfers-featuremodule moet zijn eigen bediening binden');
assert(!mediaSource.includes("'btn-transfer-pause'"), 'Media mag geen bediening van de Transfers-pagina bezitten');
assert(!systemViewSource.includes('function loadTransfers'), 'System View mag de Transfers-loader niet bezitten');
assert(navigationSource.includes('window.chimeraNavigate'), 'router moet een expliciete canonieke navigatie-API publiceren');
assert(navigationSource.includes('window.history.pushState'), 'router moet browsergeschiedenis ondersteunen');
assert(navigationSource.includes("event.key.toLowerCase() === 'k'"), 'command palette moet Ctrl/Cmd+K ondersteunen');
assert(navigationSource.includes('chimera-command-status'), 'command palette moet de zoekstatus toegankelijk terugkoppelen');
assert(navigationSource.includes('mountSettingsNavigator'), 'lange instellingenpagina moet een zoek- en springfunctie hebben');
assert(navigationSource.includes('chimera-home-command-center'), 'Home moet één geïntegreerd commandocentrum hebben');
assert(navigationSource.includes('chimera:language-changed'), 'dynamische navigatie moet live op taalwissels reageren');
assert(navigationSource.includes('await window.chimeraI18nReady'), 'de eerste routedata mag niet vóór het opgeslagen taalbestand renderen');
assert(i18nSource.includes("document.documentElement.lang = this.current || 'en'"), 'i18n moet de documenttaal toegankelijk synchroniseren');
assert(i18nSource.includes('window.chimeraI18nReady = new Promise'), 'i18n moet een eenmalige startupbarrière publiceren');
assert(i18nSource.includes("new CustomEvent('chimera:language-changed'"), 'i18n moet dynamische modules na een taalwissel verwittigen');
assert(initSource.includes("window.i18n.load(s.language || 'en')"), 'de opgeslagen taal moet tijdens startup worden geladen met Engels als fallback');
for (const route of ['console/status', 'library/games', 'protection/backups', 'payloads/updates']) {
assert(homeSource.includes(`data-route="${route}"`), `Home mist de prioritaire route ${route}`);
}
assert(homeSource.includes('Chimera Server is the control plane'), 'Home moet de server expliciet als centraal besturingsvlak uitleggen');
assert(homeSource.includes('node-server" type="button" data-route="tools/diagnostics"'), 'de centrale servernode moet naar diagnostiek leiden');
assert(!homeSource.includes('node-host'), 'dezelfde PS5 mag niet als afzonderlijk doel en console worden getekend');
assert(!homeSource.includes('home-host-indicator'), 'Home mag geen tweede PS5-statusindicator onderhouden');
assert(
bootstrapSource.includes("'modules/cheats.js', 'modules/modal.js'"),
'Games moet de Cheats-renderer laden voordat het gedeelde detailvenster opent',
);
assert(ecosystemSource.includes('Promise.allSettled([') && ecosystemSource.includes('window.pork.ecosystemSaveVault()'), 'Save Vault moet gedeeltelijke backenduitval als een bruikbare state renderen');
assert(ecosystemSource.includes('gamesById.get(item.titleId)'), 'Save Vault moet gametitels boven interne title-ID-codes tonen');
assert(ecosystemSource.includes('No SMB sources configured'), 'Files moet een bruikbare SMB-bron-empty-state tonen');
assert(ecosystemSource.includes("loadEcosystemOverview().catch(error =>"), 'directe Hub-routes moeten de gedeelde healthstatus onafhankelijk laden');
assert(/\[data-route-hidden=(?:"true"|true)\]/.test(navigationCss), 'focusprofielen moeten omkeerbaar via een data-attribuut werken');
assert(/flex-direction:\s*row/.test(navigationCss), 'interactieve zoek- en schakelrijen mogen niet de globale verticale labelrichting erven');
assert(navigationCss.includes('[data-settings-filter-hidden]'), 'instellingenfilter moet resultaten omkeerbaar verbergen');
assert(/const badge = \$\('nav-media-badge'\);\s*\/\/[\s\S]{0,300}if \(badge\)/.test(mediaSource), 'Media warmup moet veilig blijven nadat de v2-navigatie de legacy badge vervangt');
assert(
/document\.getElementById\((?:'|")sidebar(?:'|")\)\s*\|\|\s*document\.body/.test(webShimSource),
'serverlogout moet in de responsieve Chimera-shell landen',
);
assert(
(payloadSource.match(/await renderLocalPayloads\(\);\s*await renderPayloadSender\(\);/g) || []).length >= 4,
'payloaddownloads, updates, deletes en autoloaderimports moeten lokale lijst en TCP-sender atomisch verversen',
);
for (const [overlayId, titleId] of [
['payload-settings-overlay', 'payload-settings-title'],
['install-overlay', 'install-dialog-title'],
['xavatar-thumb-modal', 'xavatar-modal-name'],
['ps4-packages-overlay', 'ps4-packages-title'],
['ps5-native-packages-overlay', 'ps5-native-packages-title'],
]) {
assert(new RegExp(`id="${overlayId}"[^>]*hidden[^>]*aria-hidden="true"`).test(html), `${overlayId} moet verborgen uit de accessibility tree starten`);
assert(html.includes(`aria-labelledby="${titleId}"`), `${overlayId} mist een toegankelijke dialoognaam`);
assert(html.includes(`id="${titleId}"`), `${overlayId} verwijst naar een ontbrekende dialoogtitel`);
}
assert(transfersSource.includes("openAccessibleDialog($('payload-settings-overlay')"), 'Payload Settings moet het gedeelde focusbeheer gebruiken');
assert(transfersSource.includes("closeAccessibleDialog($('payload-settings-overlay'))"), 'Payload Settings moet focus bij sluiten herstellen');
assert(modalSource.includes("openAccessibleDialog($('install-overlay')"), 'Installeren moet het gedeelde focusbeheer gebruiken');
assert(modalSource.includes("closeAccessibleDialog($('install-overlay'))"), 'Installeren moet focus bij sluiten herstellen');
const xavatarSource = fs.readFileSync(path.join(root, 'renderer', 'modules', 'xavatar.js'), 'utf8');
assert(xavatarSource.includes('openAccessibleDialog(modal'), 'XAvatar-preview moet het gedeelde focusbeheer gebruiken');
assert(xavatarSource.includes('event.key === \'Escape\''), 'XAvatar-preview moet met Escape sluiten');
const splashHtml = fs.readFileSync(path.join(root, 'renderer', 'splash.html'), 'utf8');
const donationHtml = fs.readFileSync(path.join(root, 'renderer', 'donation.html'), 'utf8');
const offlineHtml = fs.readFileSync(path.join(root, 'renderer', 'offline.html'), 'utf8');
const standaloneSystemView = fs.readFileSync(path.join(root, 'renderer', 'system-view.html'), 'utf8');
assert(donationHtml.includes('<title>Support Chimera</title>'), 'donatievenster mist een documenttitel');
assert(splashHtml.includes('<title>Starting Chimera</title>') && splashHtml.includes('role="progressbar"'), 'splashvenster mist titel of voortgangssemantiek');
assert(offlineHtml.includes('<html lang="en">') && offlineHtml.includes('The secure offline shell'), 'offlinevenster moet de Engelse standaardtaal volgen');
for (const controlId of ['video-select', 'audio-select', 'res-select']) {
assert(standaloneSystemView.includes(`for="${controlId}"`), `System View mist een label voor ${controlId}`);
}
assert(standaloneSystemView.includes('aria-label="GIF frame rate"'), 'System View mist een label voor GIF-framerate');
assert(standaloneSystemView.includes("btnFs.setAttribute('aria-label'"), 'System View moet fullscreenstatus toegankelijk bijwerken');
console.log(`navigation contract ok: ${contract.groups.length} domeinen, ${contract.routes.length} routes, ${ecosystemTabs.size} geïntegreerde Hub-panelen`);