import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
// Reference only: a fresh Chromium profile, exact source files, no running user
// browser, website database, website document or QEMU instance is modified.
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const website = process.env.XFE_WEBSITE_ROOT || path.resolve(root, '../XFEStudioWebSite');
const codeReference = process.argv.includes('--code-reference');
const outputBase = path.join(root, 'build/CSharpDocuments');
fs.mkdirSync(outputBase, { recursive: true });
const output = fs.mkdtempSync(path.join(outputBase, 'WebsiteReference-'));
const fixtures = path.join(output, 'Fixtures');
const artifacts = path.join(output, 'Artifacts');
const sha = value => createHash('sha256').update(value).digest('hex');
const exported = spawnSync('dotnet', ['run', '--project', path.join(root, 'Tests/XFESystem.Documents.Tests/WebsiteReferenceHost/WebsiteReferenceHost.csproj'),
'-c', 'Release', '--artifacts-path', artifacts, '--', fixtures, ...(codeReference ? ['--code-reference'] : [])], { cwd: root, encoding: 'utf8', windowsHide: true, timeout: 60000 });
fs.writeFileSync(path.join(output, 'Export.log'), exported.stdout + exported.stderr);
assert.equal(exported.status, 0, exported.stdout + exported.stderr);
const manifest = JSON.parse(fs.readFileSync(path.join(fixtures, 'SourceManifest.json'), 'utf8'));
const sources = new Map(manifest.Entries.filter(e => e.UrlPath).map(e => [e.UrlPath, e]));
const verified = [];
for (const entry of manifest.Entries) {
const source = path.join(website, entry.SourcePath);
assert.equal(sha(fs.readFileSync(source)), entry.SourceSha256, 'Website source changed: ' + entry.SourcePath);
assert.equal(entry.SourceSha256, entry.SnapshotSha256, 'Snapshot transformed: ' + entry.SourcePath);
verified.push({ SourcePath: entry.SourcePath, Sha256: entry.SourceSha256 });
}
// The real site's _Layout points to this fixed CDN/version; its icon font is
// absent from the website repository. Cache it for the reference only and
// retain its URL/hash, without changing the exact CSS or the native assets.
const supplements = new Map(), supplementReceipts = [];
for (const extension of ['woff2', 'woff']) {
const name = 'bootstrap-icons.' + extension;
const sourceUrl = 'https://lib.baomitu.com/bootstrap-icons/1.11.3/font/fonts/' + name;
try {
const response = await fetch(sourceUrl, { signal: AbortSignal.timeout(10000) });
assert.equal(response.status, 200, sourceUrl);
const bytes = Buffer.from(await response.arrayBuffer());
const cached = path.join(output, name);
fs.writeFileSync(cached, bytes);
supplements.set('/css/fonts/' + name, { bytes, mime: 'font/' + extension, source: sourceUrl });
supplementReceipts.push({ SourceUrl: sourceUrl, CachedPath: cached, Sha256: sha(bytes), Bytes: bytes.length,
License: 'MIT; Bootstrap Icons 1.11.3; original CSS license header retained' });
} catch (error) { supplementReceipts.push({ SourceUrl: sourceUrl, Error: String(error) }); }
}
const executable = process.env.XFE_DOCUMENT_BROWSER || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
assert.ok(fs.existsSync(executable));
const options = {};
for (let index = 2; index < process.argv.length; index++) {
const name = process.argv[index];
if (name === '--code-reference') continue;
assert.ok(['--width', '--height', '--device-scale'].includes(name) && process.argv[index + 1], 'Use --width N --height N --device-scale N.');
options[name] = Number(process.argv[++index]);
}
const width = options['--width'] || Number(process.env.XFE_REFERENCE_WIDTH || 1100), height = options['--height'] || Number(process.env.XFE_REFERENCE_HEIGHT || 900);
const deviceScale = options['--device-scale'] || Number(process.env.XFE_REFERENCE_DEVICE_SCALE || 1);
assert.ok(width >= 320 && width <= 3840 && height >= 240 && height <= 2160 && deviceScale >= 1 && deviceScale <= 3);
const profile = path.join(output, 'Profile');
const errorLog = fs.openSync(path.join(output, 'Browser.log'), 'a');
const browser = spawn(executable, ['--headless=new', '--disable-gpu', '--disable-extensions', '--disable-background-networking',
'--no-first-run', '--no-default-browser-check', '--remote-debugging-address=127.0.0.1', '--remote-debugging-port=0',
`--window-size=${width},${height}`, `--user-data-dir=${profile}`, 'about:blank'], { windowsHide: true, stdio: ['ignore', 'ignore', errorLog] });
fs.closeSync(errorLog);
const exited = new Promise(resolve => browser.once('exit', (code, signal) => resolve({ code, signal })));
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
async function until(condition, name, timeout = 30000) {
const start = Date.now();
while (Date.now() - start < timeout) { const result = await condition(); if (result) return result; await pause(50); }
throw new Error('Timed out: ' + name);
}
let socket, nextId = 0, failure;
const pending = new Map(), requests = [], exceptions = [], warnings = [], states = [], screenshots = [], consoleOutput = [];
const result = { Kind: 'ChromiumWebsiteArticleReference', GuestAcceptance: false, FullMvcSiteStarted: false,
HtmlOrigin: 'http://xfesystem.local', WebsiteRoot: website, SourceCommit: manifest.SourceCommit,
WebsiteManifestSha256: sha(fs.readFileSync(path.join(fixtures, 'SourceManifest.json'))),
FixtureSha256: sha(fs.readFileSync(path.join(fixtures, 'Fixture.md'))), SourceChecks: verified,
Width: width, Height: height, DeviceScaleFactor: deviceScale, Requests: requests, ScriptExceptions: exceptions, BrowserWarnings: warnings,
States: states, Screenshots: screenshots, BrowserConsole: consoleOutput, ReferenceSupplementalFonts: supplementReceipts, Output: output };
if (codeReference) {
result.Component = 'WebsiteStaticCSharpCodeBlock';
result.ProductionSample = JSON.parse(fs.readFileSync(path.join(fixtures, 'CSharpSample.json'), 'utf8'));
result.FixtureSha256 = sha(fs.readFileSync(path.join(fixtures, 'CSharpSample.cs')));
assert.equal(result.FixtureSha256, result.ProductionSample.CodeSha256);
}
const command = (method, params = {}) => new Promise((resolve, reject) => {
const id = ++nextId;
const timer = setTimeout(() => { pending.delete(id); reject(new Error('CDP timeout: ' + method)); }, 30000);
pending.set(id, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: error => { clearTimeout(timer); reject(error); } });
socket.send(JSON.stringify({ id, method, params }));
});
async function evaluate(expression) {
const response = await command('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
if (response.exceptionDetails) throw new Error(response.exceptionDetails.exception?.description || response.exceptionDetails.text);
return response.result.value;
}
async function serve(request) {
const url = new URL(request.request.url);
let bytes, mime = 'application/octet-stream', sourcePath;
if (url.origin === 'http://xfesystem.local') {
if (['/Dark.html', '/Light.html', '/CodeDark.html', '/CodeLight.html'].includes(url.pathname)) {
bytes = fs.readFileSync(path.join(fixtures, url.pathname.slice(1))); mime = 'text/html; charset=utf-8';
} else if (url.pathname === '/favicon.ico') {
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: 204 }); return;
} else if (sources.has(url.pathname)) {
const entry = sources.get(url.pathname); sourcePath = entry.SourcePath;
bytes = fs.readFileSync(path.join(website, entry.SourcePath));
mime = ({ '.js': 'application/javascript', '.css': 'text/css', '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf' })[path.extname(url.pathname)] || mime;
} else if (supplements.has(url.pathname)) {
const supplement = supplements.get(url.pathname); bytes = supplement.bytes; mime = supplement.mime; sourcePath = supplement.source;
}
}
requests.push({ Url: url.href, SourcePath: sourcePath, Sha256: bytes && sha(bytes), Bytes: bytes?.length, Status: bytes ? 200 : 404 });
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: bytes ? 200 : 404,
responseHeaders: [{ name: 'Content-Type', value: mime }, { name: 'X-Content-Type-Options', value: 'nosniff' }],
body: (bytes || Buffer.from('Not present in website source snapshot.')).toString('base64') });
}
const metrics = `(() => {
const bounds = element => { if(!element)return null;const r=element.getBoundingClientRect(),s=getComputedStyle(element);return {
Tag:element.tagName,Class:element.className,X:r.x,Y:r.y,Width:r.width,Height:r.height,Font:s.fontFamily,FontSize:s.fontSize,
LineHeight:s.lineHeight,Color:s.color,Background:s.backgroundColor,Border:s.borderColor,Padding:s.padding,Margin:s.margin}; };
const code=document.querySelector('pre.code-block-enhanced'),body=document.querySelector('.markdown-body');
const highlighted=document.querySelector('.language-csharp');
const token = element => {const scopes=[];let ancestor=element;
while(ancestor&&ancestor!==highlighted){scopes.unshift(...[...ancestor.classList].filter(name=>name.startsWith('hljs-')||name.endsWith('_')));ancestor=ancestor.parentElement;}
const range=document.createRange();range.setStart(highlighted,0);range.setEndBefore(element);const start=range.toString().length,style=getComputedStyle(element);
return {Class:element.className,Scopes:scopes,Text:element.textContent,Utf16Start:start,Utf16End:start+element.textContent.length,
Color:style.color,Background:style.backgroundColor,FontStyle:style.fontStyle,FontWeight:style.fontWeight};};
return {Theme:document.documentElement.getAttribute('data-bs-theme'),Viewport:{Width:innerWidth,Height:innerHeight},
Article:bounds(body),Code:bounds(code),Heading:bounds(document.querySelector('h1')),Quote:bounds(document.querySelector('blockquote')),
Table:bounds(document.querySelector('table')),CodeText:highlighted.textContent,Tokens:[...document.querySelectorAll('.language-csharp span[class]')].map(token),
CodeTools:document.querySelectorAll('[data-code-action]').length,Lines:document.querySelectorAll('.code-line-numbers>span').length,
Math:document.querySelectorAll('.katex').length,Diagrams:document.querySelectorAll('.mermaid-wrapper svg').length,
Css:[...document.styleSheets].map(s=>({Href:s.href,Rules:s.cssRules.length})),Scripts:[...document.scripts].map(s=>s.src),
DocumentHeight:document.documentElement.scrollHeight};
})()`;
try {
await until(() => fs.existsSync(path.join(profile, 'DevToolsActivePort')), 'isolated browser port');
const [port] = fs.readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').trim().split(/\r?\n/);
result.BrowserVersion = (await (await fetch(`http://127.0.0.1:${port}/json/version`)).json()).Browser;
result.BrowserPid = browser.pid;
const page = await (await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' })).json();
socket = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => { socket.addEventListener('open', resolve, { once: true }); socket.addEventListener('error', reject, { once: true }); });
socket.addEventListener('message', event => {
const message = JSON.parse(event.data);
if (message.id) { const task = pending.get(message.id); if (!task) return; pending.delete(message.id); message.error ? task.reject(new Error(message.error.message)) : task.resolve(message.result); }
else if (message.method === 'Fetch.requestPaused') serve(message.params).catch(error => { failure = error; });
else if (message.method === 'Runtime.exceptionThrown') exceptions.push(message.params.exceptionDetails);
else if (message.method === 'Log.entryAdded') warnings.push(message.params.entry);
else if (message.method === 'Runtime.consoleAPICalled') consoleOutput.push(message.params);
});
await command('Page.enable'); await command('Runtime.enable'); await command('Log.enable');
await command('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: deviceScale, mobile: false });
await command('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
for (const theme of ['Dark', 'Light']) {
const prefix = codeReference ? 'Code' : '';
await command('Page.navigate', { url: `http://xfesystem.local/${prefix}${theme}.html` });
const ready = `document.readyState==='complete'&&document.querySelectorAll('.language-csharp .hljs-keyword').length>0` +
(codeReference ? `&&document.querySelectorAll('[data-code-action]').length===4` : `&&document.querySelectorAll('.katex').length===2&&document.querySelectorAll('.mermaid-wrapper svg').length===1`);
await until(async () => await evaluate(ready), theme + ' real website initializers');
await evaluate('document.fonts.ready.then(()=>true)');
const state = await evaluate(metrics);
assert.equal(state.CodeTools, 4); assert.ok(state.Lines > 8);
if (!codeReference) { assert.equal(state.Math, 2); assert.equal(state.Diagrams, 1); }
else assert.equal(sha(Buffer.from(state.CodeText, 'utf8')), result.ProductionSample.CodeSha256);
state.OriginalCssOrder = state.Css.map(sheet => { const entry=sources.get(new URL(sheet.Href).pathname);return { UrlPath:entry.UrlPath,SourceSha256:entry.SourceSha256 }; });
state.OriginalCssOrderSha256 = sha(Buffer.from(JSON.stringify(state.OriginalCssOrder), 'utf8'));
state.OriginalScriptOrder = state.Scripts.map(url => { const entry=sources.get(new URL(url).pathname);return { UrlPath:entry.UrlPath,SourceSha256:entry.SourceSha256 }; });
state.OriginalScriptOrderSha256 = sha(Buffer.from(JSON.stringify(state.OriginalScriptOrder), 'utf8'));
states.push(state);
fs.writeFileSync(path.join(output, theme + 'Dom.html'), await evaluate('document.documentElement.outerHTML'));
const viewport = await command('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
fs.writeFileSync(path.join(output, theme + '.png'), Buffer.from(viewport.data, 'base64'));
const screenshotPath = path.join(output, theme + '.png');
const screenshotReceipt = { Kind: 'WebsiteBrowserReferenceScreenshot', ScreenshotPath: screenshotPath,
ScreenshotSha256: sha(fs.readFileSync(screenshotPath)), Theme: theme.toLowerCase(),
ViewportCss: { Width: width, Height: height, DeviceScaleFactor: deviceScale },
WebsiteSourceManifestSha256: result.WebsiteManifestSha256, FixtureSha256: result.FixtureSha256,
Component: codeReference ? 'WebsiteStaticCSharpCodeBlock' : 'WebsiteMarkdownArticle',
OriginalCssOrderSha256: state.OriginalCssOrderSha256, OriginalScriptOrderSha256: state.OriginalScriptOrderSha256,
ProductionSample: result.ProductionSample };
screenshots.push(screenshotReceipt);
fs.writeFileSync(path.join(output, theme + 'Screenshot.json'), JSON.stringify(screenshotReceipt, null, 2));
const full = await command('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true,
clip: { x: 0, y: 0, width, height: Math.min(16000, state.DocumentHeight), scale: 1 } });
fs.writeFileSync(path.join(output, theme + 'Full.png'), Buffer.from(full.data, 'base64'));
const lineToggle = await evaluate(`(() => {const p=document.querySelector('pre.code-block-enhanced'),b=p.querySelector('[data-code-action=lines]'),before=p.classList.contains('code-lines-visible');b.click();const after=p.classList.contains('code-lines-visible');b.click();return before&&!after&&p.classList.contains('code-lines-visible');})()`);
assert.equal(lineToggle, true); state.LineToggleWorked = true;
if (failure) throw failure;
}
assert.equal(exceptions.length, 0, JSON.stringify(exceptions));
result.Passed = true;
result.KnownMissingWebsiteResources = requests.filter(r => r.Status === 404);
} catch (error) { result.Passed = false; result.Error = error.stack; process.exitCode = 1; }
finally {
if (socket?.readyState === WebSocket.OPEN) { try { await command('Browser.close'); } catch {} socket.close(); }
if (browser.exitCode === null) { await Promise.race([exited, pause(5000)]); if (browser.exitCode === null) browser.kill(); }
result.BrowserExited = await Promise.race([exited, pause(3000).then(() => null)]);
for (const task of pending.values()) task.reject(new Error('Reference capture finished.'));
pending.clear();
fs.writeFileSync(path.join(output, 'Result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ Passed: result.Passed, Result: path.join(output, 'Result.json'), Error: result.Error }, null, 2));
}
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
// Reference only: a fresh Chromium profile, exact source files, no running user
// browser, website database, website document or QEMU instance is modified.
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const website = process.env.XFE_WEBSITE_ROOT || path.resolve(root, '../XFEStudioWebSite');
const codeReference = process.argv.includes('--code-reference');
const outputBase = path.join(root, 'build/CSharpDocuments');
fs.mkdirSync(outputBase, { recursive: true });
const output = fs.mkdtempSync(path.join(outputBase, 'WebsiteReference-'));
const fixtures = path.join(output, 'Fixtures');
const artifacts = path.join(output, 'Artifacts');
const sha = value => createHash('sha256').update(value).digest('hex');
const exported = spawnSync('dotnet', ['run', '--project', path.join(root, 'Tests/XFESystem.Documents.Tests/WebsiteReferenceHost/WebsiteReferenceHost.csproj'),
'-c', 'Release', '--artifacts-path', artifacts, '--', fixtures, ...(codeReference ? ['--code-reference'] : [])], { cwd: root, encoding: 'utf8', windowsHide: true, timeout: 60000 });
fs.writeFileSync(path.join(output, 'Export.log'), exported.stdout + exported.stderr);
assert.equal(exported.status, 0, exported.stdout + exported.stderr);
const manifest = JSON.parse(fs.readFileSync(path.join(fixtures, 'SourceManifest.json'), 'utf8'));
const sources = new Map(manifest.Entries.filter(e => e.UrlPath).map(e => [e.UrlPath, e]));
const verified = [];
for (const entry of manifest.Entries) {
const source = path.join(website, entry.SourcePath);
assert.equal(sha(fs.readFileSync(source)), entry.SourceSha256, 'Website source changed: ' + entry.SourcePath);
assert.equal(entry.SourceSha256, entry.SnapshotSha256, 'Snapshot transformed: ' + entry.SourcePath);
verified.push({ SourcePath: entry.SourcePath, Sha256: entry.SourceSha256 });
}
// The real site's _Layout points to this fixed CDN/version; its icon font is
// absent from the website repository. Cache it for the reference only and
// retain its URL/hash, without changing the exact CSS or the native assets.
const supplements = new Map(), supplementReceipts = [];
for (const extension of ['woff2', 'woff']) {
const name = 'bootstrap-icons.' + extension;
const sourceUrl = 'https://lib.baomitu.com/bootstrap-icons/1.11.3/font/fonts/' + name;
try {
const response = await fetch(sourceUrl, { signal: AbortSignal.timeout(10000) });
assert.equal(response.status, 200, sourceUrl);
const bytes = Buffer.from(await response.arrayBuffer());
const cached = path.join(output, name);
fs.writeFileSync(cached, bytes);
supplements.set('/css/fonts/' + name, { bytes, mime: 'font/' + extension, source: sourceUrl });
supplementReceipts.push({ SourceUrl: sourceUrl, CachedPath: cached, Sha256: sha(bytes), Bytes: bytes.length,
License: 'MIT; Bootstrap Icons 1.11.3; original CSS license header retained' });
} catch (error) { supplementReceipts.push({ SourceUrl: sourceUrl, Error: String(error) }); }
}
const executable = process.env.XFE_DOCUMENT_BROWSER || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
assert.ok(fs.existsSync(executable));
const options = {};
for (let index = 2; index < process.argv.length; index++) {
const name = process.argv[index];
if (name === '--code-reference') continue;
assert.ok(['--width', '--height', '--device-scale'].includes(name) && process.argv[index + 1], 'Use --width N --height N --device-scale N.');
options[name] = Number(process.argv[++index]);
}
const width = options['--width'] || Number(process.env.XFE_REFERENCE_WIDTH || 1100), height = options['--height'] || Number(process.env.XFE_REFERENCE_HEIGHT || 900);
const deviceScale = options['--device-scale'] || Number(process.env.XFE_REFERENCE_DEVICE_SCALE || 1);
assert.ok(width >= 320 && width <= 3840 && height >= 240 && height <= 2160 && deviceScale >= 1 && deviceScale <= 3);
const profile = path.join(output, 'Profile');
const errorLog = fs.openSync(path.join(output, 'Browser.log'), 'a');
const browser = spawn(executable, ['--headless=new', '--disable-gpu', '--disable-extensions', '--disable-background-networking',
'--no-first-run', '--no-default-browser-check', '--remote-debugging-address=127.0.0.1', '--remote-debugging-port=0',
`--window-size=${width},${height}`, `--user-data-dir=${profile}`, 'about:blank'], { windowsHide: true, stdio: ['ignore', 'ignore', errorLog] });
fs.closeSync(errorLog);
const exited = new Promise(resolve => browser.once('exit', (code, signal) => resolve({ code, signal })));
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
async function until(condition, name, timeout = 30000) {
const start = Date.now();
while (Date.now() - start < timeout) { const result = await condition(); if (result) return result; await pause(50); }
throw new Error('Timed out: ' + name);
}
let socket, nextId = 0, failure;
const pending = new Map(), requests = [], exceptions = [], warnings = [], states = [], screenshots = [], consoleOutput = [];
const result = { Kind: 'ChromiumWebsiteArticleReference', GuestAcceptance: false, FullMvcSiteStarted: false,
HtmlOrigin: 'http://xfesystem.local', WebsiteRoot: website, SourceCommit: manifest.SourceCommit,
WebsiteManifestSha256: sha(fs.readFileSync(path.join(fixtures, 'SourceManifest.json'))),
FixtureSha256: sha(fs.readFileSync(path.join(fixtures, 'Fixture.md'))), SourceChecks: verified,
Width: width, Height: height, DeviceScaleFactor: deviceScale, Requests: requests, ScriptExceptions: exceptions, BrowserWarnings: warnings,
States: states, Screenshots: screenshots, BrowserConsole: consoleOutput, ReferenceSupplementalFonts: supplementReceipts, Output: output };
if (codeReference) {
result.Component = 'WebsiteStaticCSharpCodeBlock';
result.ProductionSample = JSON.parse(fs.readFileSync(path.join(fixtures, 'CSharpSample.json'), 'utf8'));
result.FixtureSha256 = sha(fs.readFileSync(path.join(fixtures, 'CSharpSample.cs')));
assert.equal(result.FixtureSha256, result.ProductionSample.CodeSha256);
}
const command = (method, params = {}) => new Promise((resolve, reject) => {
const id = ++nextId;
const timer = setTimeout(() => { pending.delete(id); reject(new Error('CDP timeout: ' + method)); }, 30000);
pending.set(id, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: error => { clearTimeout(timer); reject(error); } });
socket.send(JSON.stringify({ id, method, params }));
});
async function evaluate(expression) {
const response = await command('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
if (response.exceptionDetails) throw new Error(response.exceptionDetails.exception?.description || response.exceptionDetails.text);
return response.result.value;
}
async function serve(request) {
const url = new URL(request.request.url);
let bytes, mime = 'application/octet-stream', sourcePath;
if (url.origin === 'http://xfesystem.local') {
if (['/Dark.html', '/Light.html', '/CodeDark.html', '/CodeLight.html'].includes(url.pathname)) {
bytes = fs.readFileSync(path.join(fixtures, url.pathname.slice(1))); mime = 'text/html; charset=utf-8';
} else if (url.pathname === '/favicon.ico') {
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: 204 }); return;
} else if (sources.has(url.pathname)) {
const entry = sources.get(url.pathname); sourcePath = entry.SourcePath;
bytes = fs.readFileSync(path.join(website, entry.SourcePath));
mime = ({ '.js': 'application/javascript', '.css': 'text/css', '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf' })[path.extname(url.pathname)] || mime;
} else if (supplements.has(url.pathname)) {
const supplement = supplements.get(url.pathname); bytes = supplement.bytes; mime = supplement.mime; sourcePath = supplement.source;
}
}
requests.push({ Url: url.href, SourcePath: sourcePath, Sha256: bytes && sha(bytes), Bytes: bytes?.length, Status: bytes ? 200 : 404 });
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: bytes ? 200 : 404,
responseHeaders: [{ name: 'Content-Type', value: mime }, { name: 'X-Content-Type-Options', value: 'nosniff' }],
body: (bytes || Buffer.from('Not present in website source snapshot.')).toString('base64') });
}
const metrics = `(() => {
const bounds = element => { if(!element)return null;const r=element.getBoundingClientRect(),s=getComputedStyle(element);return {
Tag:element.tagName,Class:element.className,X:r.x,Y:r.y,Width:r.width,Height:r.height,Font:s.fontFamily,FontSize:s.fontSize,
LineHeight:s.lineHeight,Color:s.color,Background:s.backgroundColor,Border:s.borderColor,Padding:s.padding,Margin:s.margin}; };
const code=document.querySelector('pre.code-block-enhanced'),body=document.querySelector('.markdown-body');
const highlighted=document.querySelector('.language-csharp');
const token = element => {const scopes=[];let ancestor=element;
while(ancestor&&ancestor!==highlighted){scopes.unshift(...[...ancestor.classList].filter(name=>name.startsWith('hljs-')||name.endsWith('_')));ancestor=ancestor.parentElement;}
const range=document.createRange();range.setStart(highlighted,0);range.setEndBefore(element);const start=range.toString().length,style=getComputedStyle(element);
return {Class:element.className,Scopes:scopes,Text:element.textContent,Utf16Start:start,Utf16End:start+element.textContent.length,
Color:style.color,Background:style.backgroundColor,FontStyle:style.fontStyle,FontWeight:style.fontWeight};};
return {Theme:document.documentElement.getAttribute('data-bs-theme'),Viewport:{Width:innerWidth,Height:innerHeight},
Article:bounds(body),Code:bounds(code),Heading:bounds(document.querySelector('h1')),Quote:bounds(document.querySelector('blockquote')),
Table:bounds(document.querySelector('table')),CodeText:highlighted.textContent,Tokens:[...document.querySelectorAll('.language-csharp span[class]')].map(token),
CodeTools:document.querySelectorAll('[data-code-action]').length,Lines:document.querySelectorAll('.code-line-numbers>span').length,
Math:document.querySelectorAll('.katex').length,Diagrams:document.querySelectorAll('.mermaid-wrapper svg').length,
Css:[...document.styleSheets].map(s=>({Href:s.href,Rules:s.cssRules.length})),Scripts:[...document.scripts].map(s=>s.src),
DocumentHeight:document.documentElement.scrollHeight};
})()`;
try {
await until(() => fs.existsSync(path.join(profile, 'DevToolsActivePort')), 'isolated browser port');
const [port] = fs.readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').trim().split(/\r?\n/);
result.BrowserVersion = (await (await fetch(`http://127.0.0.1:${port}/json/version`)).json()).Browser;
result.BrowserPid = browser.pid;
const page = await (await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' })).json();
socket = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => { socket.addEventListener('open', resolve, { once: true }); socket.addEventListener('error', reject, { once: true }); });
socket.addEventListener('message', event => {
const message = JSON.parse(event.data);
if (message.id) { const task = pending.get(message.id); if (!task) return; pending.delete(message.id); message.error ? task.reject(new Error(message.error.message)) : task.resolve(message.result); }
else if (message.method === 'Fetch.requestPaused') serve(message.params).catch(error => { failure = error; });
else if (message.method === 'Runtime.exceptionThrown') exceptions.push(message.params.exceptionDetails);
else if (message.method === 'Log.entryAdded') warnings.push(message.params.entry);
else if (message.method === 'Runtime.consoleAPICalled') consoleOutput.push(message.params);
});
await command('Page.enable'); await command('Runtime.enable'); await command('Log.enable');
await command('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: deviceScale, mobile: false });
await command('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
for (const theme of ['Dark', 'Light']) {
const prefix = codeReference ? 'Code' : '';
await command('Page.navigate', { url: `http://xfesystem.local/${prefix}${theme}.html` });
const ready = `document.readyState==='complete'&&document.querySelectorAll('.language-csharp .hljs-keyword').length>0` +
(codeReference ? `&&document.querySelectorAll('[data-code-action]').length===4` : `&&document.querySelectorAll('.katex').length===2&&document.querySelectorAll('.mermaid-wrapper svg').length===1`);
await until(async () => await evaluate(ready), theme + ' real website initializers');
await evaluate('document.fonts.ready.then(()=>true)');
const state = await evaluate(metrics);
assert.equal(state.CodeTools, 4); assert.ok(state.Lines > 8);
if (!codeReference) { assert.equal(state.Math, 2); assert.equal(state.Diagrams, 1); }
else assert.equal(sha(Buffer.from(state.CodeText, 'utf8')), result.ProductionSample.CodeSha256);
state.OriginalCssOrder = state.Css.map(sheet => { const entry=sources.get(new URL(sheet.Href).pathname);return { UrlPath:entry.UrlPath,SourceSha256:entry.SourceSha256 }; });
state.OriginalCssOrderSha256 = sha(Buffer.from(JSON.stringify(state.OriginalCssOrder), 'utf8'));
state.OriginalScriptOrder = state.Scripts.map(url => { const entry=sources.get(new URL(url).pathname);return { UrlPath:entry.UrlPath,SourceSha256:entry.SourceSha256 }; });
state.OriginalScriptOrderSha256 = sha(Buffer.from(JSON.stringify(state.OriginalScriptOrder), 'utf8'));
states.push(state);
fs.writeFileSync(path.join(output, theme + 'Dom.html'), await evaluate('document.documentElement.outerHTML'));
const viewport = await command('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
fs.writeFileSync(path.join(output, theme + '.png'), Buffer.from(viewport.data, 'base64'));
const screenshotPath = path.join(output, theme + '.png');
const screenshotReceipt = { Kind: 'WebsiteBrowserReferenceScreenshot', ScreenshotPath: screenshotPath,
ScreenshotSha256: sha(fs.readFileSync(screenshotPath)), Theme: theme.toLowerCase(),
ViewportCss: { Width: width, Height: height, DeviceScaleFactor: deviceScale },
WebsiteSourceManifestSha256: result.WebsiteManifestSha256, FixtureSha256: result.FixtureSha256,
Component: codeReference ? 'WebsiteStaticCSharpCodeBlock' : 'WebsiteMarkdownArticle',
OriginalCssOrderSha256: state.OriginalCssOrderSha256, OriginalScriptOrderSha256: state.OriginalScriptOrderSha256,
ProductionSample: result.ProductionSample };
screenshots.push(screenshotReceipt);
fs.writeFileSync(path.join(output, theme + 'Screenshot.json'), JSON.stringify(screenshotReceipt, null, 2));
const full = await command('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true,
clip: { x: 0, y: 0, width, height: Math.min(16000, state.DocumentHeight), scale: 1 } });
fs.writeFileSync(path.join(output, theme + 'Full.png'), Buffer.from(full.data, 'base64'));
const lineToggle = await evaluate(`(() => {const p=document.querySelector('pre.code-block-enhanced'),b=p.querySelector('[data-code-action=lines]'),before=p.classList.contains('code-lines-visible');b.click();const after=p.classList.contains('code-lines-visible');b.click();return before&&!after&&p.classList.contains('code-lines-visible');})()`);
assert.equal(lineToggle, true); state.LineToggleWorked = true;
if (failure) throw failure;
}
assert.equal(exceptions.length, 0, JSON.stringify(exceptions));
result.Passed = true;
result.KnownMissingWebsiteResources = requests.filter(r => r.Status === 404);
} catch (error) { result.Passed = false; result.Error = error.stack; process.exitCode = 1; }
finally {
if (socket?.readyState === WebSocket.OPEN) { try { await command('Browser.close'); } catch {} socket.close(); }
if (browser.exitCode === null) { await Promise.race([exited, pause(5000)]); if (browser.exitCode === null) browser.kill(); }
result.BrowserExited = await Promise.race([exited, pause(3000).then(() => null)]);
for (const task of pending.values()) task.reject(new Error('Reference capture finished.'));
pending.clear();
fs.writeFileSync(path.join(output, 'Result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ Passed: result.Passed, Result: path.join(output, 'Result.json'), Error: result.Error }, null, 2));
}