313 lines
24 KiB
JavaScript
313 lines
24 KiB
JavaScript
'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`);
|