import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
// Host-only integration evidence. This never controls the user's existing browser or QEMU.
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputRoot = path.join(root, 'build', 'CSharpDocuments');
fs.mkdirSync(outputRoot, { recursive: true });
const output = fs.mkdtempSync(path.join(outputRoot, 'Browser-'));
const fixtures = path.join(output, 'Fixtures');
const artifacts = path.join(output, 'Artifacts');
const exportResult = spawnSync('dotnet', ['run', '--project', path.join(root, 'Tests/XFESystem.Documents.PreviewHost'),
'--configuration', 'Release', `-p:ArtifactsPath=${artifacts}`, '--', fixtures],
{ cwd: root, encoding: 'utf8', windowsHide: true, timeout: 60000 });
fs.writeFileSync(path.join(output, 'Export.log'), exportResult.stdout + exportResult.stderr);
assert.equal(exportResult.status, 0, exportResult.stdout + exportResult.stderr);
const executable = process.env.XFE_DOCUMENT_BROWSER || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
assert.ok(fs.existsSync(executable), 'Set XFE_DOCUMENT_BROWSER to a Chromium browser executable.');
const profile = path.join(output, 'Profile');
const stderr = 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=1280,1000', `--user-data-dir=${profile}`, 'about:blank'],
{ windowsHide: true, stdio: ['ignore', 'ignore', stderr] });
fs.closeSync(stderr);
const browserExit = new Promise(resolve => browser.once('exit', (code, signal) => resolve({ code, signal })));
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const until = async (condition, description, timeout = 20000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const value = await condition();
if (value) return value;
await delay(30);
}
throw new Error(`Timed out: ${description}`);
};
let socket;
let id = 0;
const pending = new Map();
const contexts = new Map();
const resourceLoads = [];
const rejectedRequests = [];
const scriptExceptions = [];
const browserWarnings = [];
let interceptionFailure;
const result = { Kind: 'WindowsHostOnly', GuestAcceptance: false, Output: output, Checks: [], ResourceLoads: resourceLoads,
RejectedRequests: rejectedRequests, ScriptExceptions: scriptExceptions, BrowserWarnings: browserWarnings };
const sha256 = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const source = path.join(root, 'Source/XFESystem.Documents');
result.SourceHashes = Object.fromEntries(fs.readdirSync(source).filter(name => /\.(cs|csproj|json)$/.test(name)).map(name => [name, sha256(path.join(source, name))]));
result.AssemblySha256 = sha256(path.join(artifacts, 'bin/XFESystem.Documents.PreviewHost/release/XFESystem.Documents.dll'));
result.WebsiteManifestSha256 = sha256(path.join(fixtures, 'Assets', 'SourceManifest.json'));
const command = (method, params = {}, sessionId) => new Promise((resolve, reject) => {
const requestId = ++id;
const timer = setTimeout(() => { pending.delete(requestId); reject(new Error(`CDP timeout: ${method}`)); }, 20000);
pending.set(requestId, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: error => { clearTimeout(timer); reject(error); } });
socket.send(JSON.stringify({ id: requestId, method, params, ...(sessionId ? { sessionId } : {}) }));
});
const mime = name => ({ '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf' })[path.extname(name)] || 'application/octet-stream';
const serve = async request => {
const url = new URL(request.request.url);
let file;
let resource;
if (url.origin === 'https://xfe-preview.invalid' && url.pathname === '/favicon.ico') {
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: 204 });
return;
}
if (url.origin === 'https://xfe-preview.invalid' && /^\/(Markdown|Code|JavaScript)\.html$/.test(url.pathname)) {
file = path.join(fixtures, url.pathname.slice(1));
} else if (url.origin === 'https://xfe-resources.invalid' && url.pathname.startsWith('/Documents/')) {
resource = decodeURIComponent(url.pathname.slice('/Documents/'.length));
assert.ok(!resource.includes('\\') && !resource.split('/').some(segment => !segment || segment === '.' || segment === '..'));
file = path.join(fixtures, 'Assets', resource);
}
if (!file || !fs.existsSync(file)) {
rejectedRequests.push(request.request.url);
await command('Fetch.failRequest', { requestId: request.requestId, errorReason: 'BlockedByClient' });
return;
}
const bytes = fs.readFileSync(file);
if (resource) resourceLoads.push({ Name: resource, Bytes: bytes.length, Mime: mime(file) });
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: 200,
responseHeaders: [{ name: 'Content-Type', value: mime(file) }, { name: 'X-Content-Type-Options', value: 'nosniff' },
{ name: 'Access-Control-Allow-Origin', value: '*' }],
body: bytes.toString('base64') });
};
const evaluate = async (expression, contextId, sessionId) => {
const value = await command('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true,
...(contextId ? { contextId } : {}) }, sessionId);
if (value.exceptionDetails) throw new Error(value.exceptionDetails.text + ': ' + value.exceptionDetails.exception?.description);
return value.result.value;
};
const navigate = async name => {
const url = `https://xfe-preview.invalid/${name}.html`;
await command('Page.navigate', { url });
await until(async () => await evaluate(`location.href === ${JSON.stringify(url)} && document.readyState === 'complete'`), `${name} document complete`);
if (interceptionFailure) throw interceptionFailure;
};
const screenshot = async name => {
const shot = await command('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
fs.writeFileSync(path.join(output, name + '.png'), Buffer.from(shot.data, 'base64'));
};
try {
await until(() => fs.existsSync(path.join(profile, 'DevToolsActivePort')), 'isolated Edge DevTools port');
const [port] = fs.readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').trim().split(/\r?\n/);
const version = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
result.BrowserVersion = version.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 request = pending.get(message.id);
if (!request) return;
pending.delete(message.id);
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result);
} else if (message.method === 'Fetch.requestPaused') {
serve(message.params).catch(error => { interceptionFailure = error; });
} else if (message.method === 'Runtime.executionContextCreated') {
contexts.set((message.sessionId || '') + ':' + message.params.context.id,
{ ...message.params.context, sessionId: message.sessionId });
} else if (message.method === 'Runtime.executionContextDestroyed') {
contexts.delete((message.sessionId || '') + ':' + message.params.executionContextId);
} else if (message.method === 'Runtime.exceptionThrown') {
scriptExceptions.push(message.params.exceptionDetails);
} else if (message.method === 'Log.entryAdded') {
browserWarnings.push(message.params.entry);
}
});
await command('Page.enable');
await command('Runtime.enable');
await command('Log.enable');
await command('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
await navigate('Markdown');
await until(async () => await evaluate('Boolean(document.querySelector(".katex") && document.querySelector(".mermaid-wrapper svg") && document.querySelector(".hljs-keyword"))'), 'actual website highlighting, KaTeX and Mermaid DOM');
const markdownState = await evaluate(`({ Heading: document.querySelector('h1').textContent, TableRows: document.querySelectorAll('table tr').length,
Highlighted: document.querySelectorAll('code .hljs-keyword').length, CodeTools: document.querySelectorAll('[data-code-action]').length,
Math: document.querySelectorAll('.katex').length, Diagrams: document.querySelectorAll('.mermaid-wrapper svg').length,
ScriptRan: globalThis.MarkdownScriptRan === true, EventRan: globalThis.MarkdownEventRan === true,
Width: document.querySelector('main').getBoundingClientRect().width, ScrollWidth: document.documentElement.scrollWidth,
ViewWidth: innerWidth })`);
assert.equal(markdownState.Heading, 'XFESystem 文档');
assert.equal(markdownState.TableRows, 2);
assert.ok(markdownState.Highlighted > 0 && markdownState.CodeTools > 0 && markdownState.Math > 0 && markdownState.Diagrams > 0);
assert.equal(markdownState.ScriptRan, false);
assert.equal(markdownState.EventRan, false);
assert.ok(markdownState.Width > 100);
assert.ok(markdownState.ScrollWidth <= markdownState.ViewWidth + 1);
result.Checks.push({ Name: 'MarkdownWebsiteDom', Passed: true, State: markdownState });
await screenshot('Markdown');
await command('Emulation.setDeviceMetricsOverride', { width: 375, height: 900, deviceScaleFactor: 1, mobile: false });
await navigate('Markdown');
await until(async () => await evaluate('Boolean(document.querySelector(".mermaid-wrapper svg"))'), 'narrow-screen Mermaid render');
const narrowState = await evaluate(`({ ViewWidth: innerWidth, ScrollWidth: document.documentElement.scrollWidth,
ButtonText: [...document.querySelectorAll('[data-tool-label]')].map(b=>b.dataset.toolLabel) })`);
assert.equal(narrowState.ViewWidth, 375);
assert.ok(narrowState.ScrollWidth <= narrowState.ViewWidth + 1);
assert.ok(narrowState.ButtonText.includes('复制') && narrowState.ButtonText.includes('缩小'));
result.Checks.push({ Name: 'NarrowMarkdownWebsiteDom', Passed: true, State: narrowState });
await screenshot('MarkdownNarrow');
await command('Emulation.clearDeviceMetricsOverride');
await navigate('Code');
await until(async () => await evaluate('document.querySelectorAll("code .hljs-keyword").length > 0'), 'CSharp token coloring');
const codeState = await evaluate(`({ Content: document.querySelector('code').textContent,
KeywordTokens: document.querySelectorAll('code .hljs-keyword').length, StringTokens: document.querySelectorAll('code .hljs-string').length })`);
assert.ok(codeState.KeywordTokens > 0 && codeState.StringTokens > 0);
assert.ok(codeState.Content.includes('Sample'));
result.Checks.push({ Name: 'CodeWebsiteDom', Passed: true, State: codeState });
await screenshot('Code');
await navigate('JavaScript');
const iframeTarget = await until(async () => (await command('Target.getTargets')).targetInfos.find(item =>
item.type === 'iframe' && item.url === 'about:srcdoc'), 'sandboxed JavaScript out-of-process iframe');
const { sessionId: iframeSession } = await command('Target.attachToTarget', { targetId: iframeTarget.targetId, flatten: true });
await command('Runtime.enable', {}, iframeSession);
const contextId = await until(() => [...contexts.values()].find(item => item.sessionId === iframeSession && item.auxData?.isDefault)?.id,
'isolated iframe execution context');
await until(async () => (await evaluate('document.getElementById("Output").textContent', contextId, iframeSession)).includes('Network blocked'), 'JavaScript execution and blocked network');
const jsState = await evaluate(`({ Output: document.getElementById('Output').textContent, Executed: document.body.dataset.Executed,
ParentAccessible: (() => { try { return Boolean(parent.document); } catch { return false; } })() })`, contextId, iframeSession);
assert.ok(jsState.Output.includes('Result 42'));
assert.ok(jsState.Output.includes('Host bridge undefined'));
assert.equal(jsState.Executed, 'yes');
assert.equal(jsState.ParentAccessible, false);
assert.equal(rejectedRequests.length, 0, JSON.stringify(rejectedRequests));
result.Checks.push({ Name: 'JavaScriptBrowserHostOnly', Passed: true, State: jsState,
OutOfProcessIframe: true, BudgetEnforcementTested: false,
Note: 'Safe fixture only; process/timeout/memory requirements are not claimed as an OS renderer implementation.' });
await screenshot('JavaScript');
if (interceptionFailure) throw interceptionFailure;
assert.equal(scriptExceptions.length, 0, JSON.stringify(scriptExceptions));
const unexpectedWarnings = browserWarnings.filter(entry => entry.level === 'error' &&
!entry.text.includes('https://outside.invalid/Blocked.png') &&
!entry.text.includes('https://outside.invalid/Blocked'));
assert.equal(unexpectedWarnings.length, 0, JSON.stringify(unexpectedWarnings));
result.Passed = true;
} catch (error) {
result.Passed = false;
result.Error = error.stack;
process.exitCode = 1;
} finally {
if (socket?.readyState === WebSocket.OPEN) {
try { await command('Browser.close'); } catch { /* Edge can close the CDP socket before responding. */ }
socket.close();
}
if (browser.exitCode === null) {
await Promise.race([browserExit, delay(5000)]);
if (browser.exitCode === null) browser.kill();
}
result.BrowserExited = await Promise.race([browserExit, delay(3000).then(() => null)]);
for (const request of pending.values()) request.reject(new Error('Browser test ended.'));
pending.clear();
fs.writeFileSync(path.join(output, 'Result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ Passed: result.Passed, Checks: result.Checks.length, 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 { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
// Host-only integration evidence. This never controls the user's existing browser or QEMU.
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputRoot = path.join(root, 'build', 'CSharpDocuments');
fs.mkdirSync(outputRoot, { recursive: true });
const output = fs.mkdtempSync(path.join(outputRoot, 'Browser-'));
const fixtures = path.join(output, 'Fixtures');
const artifacts = path.join(output, 'Artifacts');
const exportResult = spawnSync('dotnet', ['run', '--project', path.join(root, 'Tests/XFESystem.Documents.PreviewHost'),
'--configuration', 'Release', `-p:ArtifactsPath=${artifacts}`, '--', fixtures],
{ cwd: root, encoding: 'utf8', windowsHide: true, timeout: 60000 });
fs.writeFileSync(path.join(output, 'Export.log'), exportResult.stdout + exportResult.stderr);
assert.equal(exportResult.status, 0, exportResult.stdout + exportResult.stderr);
const executable = process.env.XFE_DOCUMENT_BROWSER || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
assert.ok(fs.existsSync(executable), 'Set XFE_DOCUMENT_BROWSER to a Chromium browser executable.');
const profile = path.join(output, 'Profile');
const stderr = 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=1280,1000', `--user-data-dir=${profile}`, 'about:blank'],
{ windowsHide: true, stdio: ['ignore', 'ignore', stderr] });
fs.closeSync(stderr);
const browserExit = new Promise(resolve => browser.once('exit', (code, signal) => resolve({ code, signal })));
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const until = async (condition, description, timeout = 20000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const value = await condition();
if (value) return value;
await delay(30);
}
throw new Error(`Timed out: ${description}`);
};
let socket;
let id = 0;
const pending = new Map();
const contexts = new Map();
const resourceLoads = [];
const rejectedRequests = [];
const scriptExceptions = [];
const browserWarnings = [];
let interceptionFailure;
const result = { Kind: 'WindowsHostOnly', GuestAcceptance: false, Output: output, Checks: [], ResourceLoads: resourceLoads,
RejectedRequests: rejectedRequests, ScriptExceptions: scriptExceptions, BrowserWarnings: browserWarnings };
const sha256 = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const source = path.join(root, 'Source/XFESystem.Documents');
result.SourceHashes = Object.fromEntries(fs.readdirSync(source).filter(name => /\.(cs|csproj|json)$/.test(name)).map(name => [name, sha256(path.join(source, name))]));
result.AssemblySha256 = sha256(path.join(artifacts, 'bin/XFESystem.Documents.PreviewHost/release/XFESystem.Documents.dll'));
result.WebsiteManifestSha256 = sha256(path.join(fixtures, 'Assets', 'SourceManifest.json'));
const command = (method, params = {}, sessionId) => new Promise((resolve, reject) => {
const requestId = ++id;
const timer = setTimeout(() => { pending.delete(requestId); reject(new Error(`CDP timeout: ${method}`)); }, 20000);
pending.set(requestId, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: error => { clearTimeout(timer); reject(error); } });
socket.send(JSON.stringify({ id: requestId, method, params, ...(sessionId ? { sessionId } : {}) }));
});
const mime = name => ({ '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf' })[path.extname(name)] || 'application/octet-stream';
const serve = async request => {
const url = new URL(request.request.url);
let file;
let resource;
if (url.origin === 'https://xfe-preview.invalid' && url.pathname === '/favicon.ico') {
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: 204 });
return;
}
if (url.origin === 'https://xfe-preview.invalid' && /^\/(Markdown|Code|JavaScript)\.html$/.test(url.pathname)) {
file = path.join(fixtures, url.pathname.slice(1));
} else if (url.origin === 'https://xfe-resources.invalid' && url.pathname.startsWith('/Documents/')) {
resource = decodeURIComponent(url.pathname.slice('/Documents/'.length));
assert.ok(!resource.includes('\\') && !resource.split('/').some(segment => !segment || segment === '.' || segment === '..'));
file = path.join(fixtures, 'Assets', resource);
}
if (!file || !fs.existsSync(file)) {
rejectedRequests.push(request.request.url);
await command('Fetch.failRequest', { requestId: request.requestId, errorReason: 'BlockedByClient' });
return;
}
const bytes = fs.readFileSync(file);
if (resource) resourceLoads.push({ Name: resource, Bytes: bytes.length, Mime: mime(file) });
await command('Fetch.fulfillRequest', { requestId: request.requestId, responseCode: 200,
responseHeaders: [{ name: 'Content-Type', value: mime(file) }, { name: 'X-Content-Type-Options', value: 'nosniff' },
{ name: 'Access-Control-Allow-Origin', value: '*' }],
body: bytes.toString('base64') });
};
const evaluate = async (expression, contextId, sessionId) => {
const value = await command('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true,
...(contextId ? { contextId } : {}) }, sessionId);
if (value.exceptionDetails) throw new Error(value.exceptionDetails.text + ': ' + value.exceptionDetails.exception?.description);
return value.result.value;
};
const navigate = async name => {
const url = `https://xfe-preview.invalid/${name}.html`;
await command('Page.navigate', { url });
await until(async () => await evaluate(`location.href === ${JSON.stringify(url)} && document.readyState === 'complete'`), `${name} document complete`);
if (interceptionFailure) throw interceptionFailure;
};
const screenshot = async name => {
const shot = await command('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
fs.writeFileSync(path.join(output, name + '.png'), Buffer.from(shot.data, 'base64'));
};
try {
await until(() => fs.existsSync(path.join(profile, 'DevToolsActivePort')), 'isolated Edge DevTools port');
const [port] = fs.readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').trim().split(/\r?\n/);
const version = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
result.BrowserVersion = version.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 request = pending.get(message.id);
if (!request) return;
pending.delete(message.id);
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result);
} else if (message.method === 'Fetch.requestPaused') {
serve(message.params).catch(error => { interceptionFailure = error; });
} else if (message.method === 'Runtime.executionContextCreated') {
contexts.set((message.sessionId || '') + ':' + message.params.context.id,
{ ...message.params.context, sessionId: message.sessionId });
} else if (message.method === 'Runtime.executionContextDestroyed') {
contexts.delete((message.sessionId || '') + ':' + message.params.executionContextId);
} else if (message.method === 'Runtime.exceptionThrown') {
scriptExceptions.push(message.params.exceptionDetails);
} else if (message.method === 'Log.entryAdded') {
browserWarnings.push(message.params.entry);
}
});
await command('Page.enable');
await command('Runtime.enable');
await command('Log.enable');
await command('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
await navigate('Markdown');
await until(async () => await evaluate('Boolean(document.querySelector(".katex") && document.querySelector(".mermaid-wrapper svg") && document.querySelector(".hljs-keyword"))'), 'actual website highlighting, KaTeX and Mermaid DOM');
const markdownState = await evaluate(`({ Heading: document.querySelector('h1').textContent, TableRows: document.querySelectorAll('table tr').length,
Highlighted: document.querySelectorAll('code .hljs-keyword').length, CodeTools: document.querySelectorAll('[data-code-action]').length,
Math: document.querySelectorAll('.katex').length, Diagrams: document.querySelectorAll('.mermaid-wrapper svg').length,
ScriptRan: globalThis.MarkdownScriptRan === true, EventRan: globalThis.MarkdownEventRan === true,
Width: document.querySelector('main').getBoundingClientRect().width, ScrollWidth: document.documentElement.scrollWidth,
ViewWidth: innerWidth })`);
assert.equal(markdownState.Heading, 'XFESystem 文档');
assert.equal(markdownState.TableRows, 2);
assert.ok(markdownState.Highlighted > 0 && markdownState.CodeTools > 0 && markdownState.Math > 0 && markdownState.Diagrams > 0);
assert.equal(markdownState.ScriptRan, false);
assert.equal(markdownState.EventRan, false);
assert.ok(markdownState.Width > 100);
assert.ok(markdownState.ScrollWidth <= markdownState.ViewWidth + 1);
result.Checks.push({ Name: 'MarkdownWebsiteDom', Passed: true, State: markdownState });
await screenshot('Markdown');
await command('Emulation.setDeviceMetricsOverride', { width: 375, height: 900, deviceScaleFactor: 1, mobile: false });
await navigate('Markdown');
await until(async () => await evaluate('Boolean(document.querySelector(".mermaid-wrapper svg"))'), 'narrow-screen Mermaid render');
const narrowState = await evaluate(`({ ViewWidth: innerWidth, ScrollWidth: document.documentElement.scrollWidth,
ButtonText: [...document.querySelectorAll('[data-tool-label]')].map(b=>b.dataset.toolLabel) })`);
assert.equal(narrowState.ViewWidth, 375);
assert.ok(narrowState.ScrollWidth <= narrowState.ViewWidth + 1);
assert.ok(narrowState.ButtonText.includes('复制') && narrowState.ButtonText.includes('缩小'));
result.Checks.push({ Name: 'NarrowMarkdownWebsiteDom', Passed: true, State: narrowState });
await screenshot('MarkdownNarrow');
await command('Emulation.clearDeviceMetricsOverride');
await navigate('Code');
await until(async () => await evaluate('document.querySelectorAll("code .hljs-keyword").length > 0'), 'CSharp token coloring');
const codeState = await evaluate(`({ Content: document.querySelector('code').textContent,
KeywordTokens: document.querySelectorAll('code .hljs-keyword').length, StringTokens: document.querySelectorAll('code .hljs-string').length })`);
assert.ok(codeState.KeywordTokens > 0 && codeState.StringTokens > 0);
assert.ok(codeState.Content.includes('Sample'));
result.Checks.push({ Name: 'CodeWebsiteDom', Passed: true, State: codeState });
await screenshot('Code');
await navigate('JavaScript');
const iframeTarget = await until(async () => (await command('Target.getTargets')).targetInfos.find(item =>
item.type === 'iframe' && item.url === 'about:srcdoc'), 'sandboxed JavaScript out-of-process iframe');
const { sessionId: iframeSession } = await command('Target.attachToTarget', { targetId: iframeTarget.targetId, flatten: true });
await command('Runtime.enable', {}, iframeSession);
const contextId = await until(() => [...contexts.values()].find(item => item.sessionId === iframeSession && item.auxData?.isDefault)?.id,
'isolated iframe execution context');
await until(async () => (await evaluate('document.getElementById("Output").textContent', contextId, iframeSession)).includes('Network blocked'), 'JavaScript execution and blocked network');
const jsState = await evaluate(`({ Output: document.getElementById('Output').textContent, Executed: document.body.dataset.Executed,
ParentAccessible: (() => { try { return Boolean(parent.document); } catch { return false; } })() })`, contextId, iframeSession);
assert.ok(jsState.Output.includes('Result 42'));
assert.ok(jsState.Output.includes('Host bridge undefined'));
assert.equal(jsState.Executed, 'yes');
assert.equal(jsState.ParentAccessible, false);
assert.equal(rejectedRequests.length, 0, JSON.stringify(rejectedRequests));
result.Checks.push({ Name: 'JavaScriptBrowserHostOnly', Passed: true, State: jsState,
OutOfProcessIframe: true, BudgetEnforcementTested: false,
Note: 'Safe fixture only; process/timeout/memory requirements are not claimed as an OS renderer implementation.' });
await screenshot('JavaScript');
if (interceptionFailure) throw interceptionFailure;
assert.equal(scriptExceptions.length, 0, JSON.stringify(scriptExceptions));
const unexpectedWarnings = browserWarnings.filter(entry => entry.level === 'error' &&
!entry.text.includes('https://outside.invalid/Blocked.png') &&
!entry.text.includes('https://outside.invalid/Blocked'));
assert.equal(unexpectedWarnings.length, 0, JSON.stringify(unexpectedWarnings));
result.Passed = true;
} catch (error) {
result.Passed = false;
result.Error = error.stack;
process.exitCode = 1;
} finally {
if (socket?.readyState === WebSocket.OPEN) {
try { await command('Browser.close'); } catch { /* Edge can close the CDP socket before responding. */ }
socket.close();
}
if (browser.exitCode === null) {
await Promise.race([browserExit, delay(5000)]);
if (browser.exitCode === null) browser.kill();
}
result.BrowserExited = await Promise.race([browserExit, delay(3000).then(() => null)]);
for (const request of pending.values()) request.reject(new Error('Browser test ended.'));
pending.clear();
fs.writeFileSync(path.join(output, 'Result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ Passed: result.Passed, Checks: result.Checks.length, Result: path.join(output, 'Result.json'), Error: result.Error }, null, 2));
}