'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(/]+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('