Replace the gear spinner with the thinking-orbs canvas engine (vendored framework-free build, driven by js/orb.js in the "solving" state) and the inline SVG brand icon with a pure CSS book whose cover flips open on page load and while the brand is hovered (js/logo.js).
593 lines
25 KiB
JavaScript
593 lines
25 KiB
JavaScript
/**
|
|
* Comprehensive End-to-End Browser Test Runner.
|
|
* Connects directly to Chrome running on port 9444 via CDP,
|
|
* tests the Web UI against all requirements in docs/implementation/WEBUI.md and CONTRACTS.md.
|
|
*/
|
|
|
|
import http from 'node:http';
|
|
import assert from 'node:assert/strict';
|
|
import { createMockServer } from '../dev/mock-server.js';
|
|
|
|
const MOCK_PORT = 5173;
|
|
const CHROME_PORT = 9444;
|
|
const APP_URL = `http://127.0.0.1:${MOCK_PORT}/`;
|
|
|
|
class CDPClient {
|
|
constructor(wsUrl) {
|
|
this.wsUrl = wsUrl;
|
|
this.ws = null;
|
|
this.msgId = 1;
|
|
this.pending = new Map();
|
|
this.consoleLogs = [];
|
|
this.networkRequests = [];
|
|
}
|
|
|
|
async connect() {
|
|
this.ws = new WebSocket(this.wsUrl);
|
|
await new Promise((resolve, reject) => {
|
|
this.ws.onopen = resolve;
|
|
this.ws.onerror = reject;
|
|
});
|
|
|
|
this.ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.id && this.pending.has(data.id)) {
|
|
const { resolve, reject } = this.pending.get(data.id);
|
|
this.pending.delete(data.id);
|
|
if (data.error) reject(new Error(data.error.message || JSON.stringify(data.error)));
|
|
else resolve(data.result);
|
|
} else if (data.method === 'Runtime.consoleAPICalled') {
|
|
this.consoleLogs.push(data.params);
|
|
} else if (data.method === 'Network.requestWillBeSent') {
|
|
this.networkRequests.push(data.params);
|
|
}
|
|
};
|
|
|
|
// Enable Runtime and Network domains
|
|
await this.send('Runtime.enable');
|
|
await this.send('Network.enable');
|
|
}
|
|
|
|
send(method, params = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const id = this.msgId++;
|
|
this.pending.set(id, { resolve, reject });
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
});
|
|
}
|
|
|
|
async eval(expression) {
|
|
const res = await this.send('Runtime.evaluate', {
|
|
expression,
|
|
returnByValue: true,
|
|
awaitPromise: true
|
|
});
|
|
if (res.exceptionDetails) {
|
|
throw new Error(`Eval error: ${res.exceptionDetails.text || ''} ${res.exceptionDetails.exception?.description || ''}`);
|
|
}
|
|
return res.result ? res.result.value : undefined;
|
|
}
|
|
|
|
async navigate(url) {
|
|
await this.send('Page.navigate', { url });
|
|
// Wait for load event
|
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
}
|
|
|
|
close() {
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('=== Starting Web UI End-to-End Tests with Chrome (port 9444) ===\n');
|
|
|
|
// 1. Start mock server
|
|
const server = createMockServer();
|
|
await new Promise((resolve) => server.listen(MOCK_PORT, '127.0.0.1', resolve));
|
|
console.log(`✓ Mock server started at http://127.0.0.1:${MOCK_PORT}/`);
|
|
|
|
// 2. Discover Chrome tab on port 9444
|
|
const tabsRes = await fetch(`http://127.0.0.1:${CHROME_PORT}/json`);
|
|
const tabs = await tabsRes.json();
|
|
const pageTab = tabs.find((t) => t.type === 'page') || tabs[0];
|
|
if (!pageTab || !pageTab.webSocketDebuggerUrl) {
|
|
throw new Error('No active Chrome page tab found on port 9444');
|
|
}
|
|
|
|
const cdp = new CDPClient(pageTab.webSocketDebuggerUrl);
|
|
await cdp.connect();
|
|
console.log(`✓ Connected to Chrome tab: "${pageTab.title}" via CDP\n`);
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
async function step(name, fn) {
|
|
process.stdout.write(`• Test: ${name}... `);
|
|
try {
|
|
await fn();
|
|
console.log('PASS');
|
|
passed++;
|
|
} catch (err) {
|
|
console.log(`FAIL: ${err.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Reset scenario on mock server
|
|
await fetch(`http://127.0.0.1:${MOCK_PORT}/dev/scenario`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ scenario: 'normal' })
|
|
});
|
|
|
|
// Navigate to Web UI
|
|
await cdp.navigate(APP_URL);
|
|
await cdp.eval('document.cookie = "mock_scenario=normal; path=/";');
|
|
await cdp.navigate(APP_URL);
|
|
|
|
// Test 1: Page Load & Initial View
|
|
await step('Page Load & Initial View', async () => {
|
|
const title = await cdp.eval('document.title');
|
|
assert.equal(title, 'Confluence Research');
|
|
|
|
const isPromptVisible = await cdp.eval('!document.getElementById("view-prompt").classList.contains("hidden")');
|
|
assert.equal(isPromptVisible, true, 'Prompt view must be visible initially');
|
|
|
|
const hasIndicator = await cdp.eval('document.getElementById("cred-indicator").classList.contains("active")');
|
|
assert.equal(hasIndicator, false, 'Credentials indicator must be unset (gray)');
|
|
|
|
const helperText = await cdp.eval('document.querySelector(".prompt-helper").textContent');
|
|
assert.ok(helperText.includes('Press Enter ↵ to send'));
|
|
});
|
|
|
|
// Test 2: Credentials Modal Opening and Focus Trapping
|
|
await step('Credentials Modal & Focus Trapping', async () => {
|
|
// Click key button
|
|
await cdp.eval('document.getElementById("key-btn").click()');
|
|
|
|
const isModalOpen = await cdp.eval('!document.getElementById("modal-backdrop").classList.contains("hidden")');
|
|
assert.equal(isModalOpen, true, 'Modal should be open');
|
|
|
|
const focusedId = await cdp.eval('document.activeElement.id');
|
|
assert.equal(focusedId, 'cred-url', 'URL input should be focused on open');
|
|
|
|
// Test Cancel closes modal and restores focus
|
|
await cdp.eval('document.getElementById("btn-cancel-cred").click()');
|
|
const isModalClosed = await cdp.eval('document.getElementById("modal-backdrop").classList.contains("hidden")');
|
|
assert.equal(isModalClosed, true, 'Modal should close on cancel');
|
|
|
|
const restoredFocus = await cdp.eval('document.activeElement.id');
|
|
assert.equal(restoredFocus, 'key-btn', 'Focus should return to key-btn');
|
|
});
|
|
|
|
// Test 3: Password Visibility Toggle & Test Connection
|
|
await step('Password Toggle & Test Connection', async () => {
|
|
await cdp.eval('document.getElementById("key-btn").click()');
|
|
|
|
// Set input values
|
|
await cdp.eval(`
|
|
document.getElementById("cred-url").value = "https://approved.example.com";
|
|
document.getElementById("cred-pat").value = "dummy-pat-123";
|
|
`);
|
|
|
|
// Toggle password
|
|
const typeBefore = await cdp.eval('document.getElementById("cred-pat").type');
|
|
assert.equal(typeBefore, 'password');
|
|
|
|
await cdp.eval('document.getElementById("toggle-pat-btn").click()');
|
|
const typeAfter = await cdp.eval('document.getElementById("cred-pat").type');
|
|
assert.equal(typeAfter, 'text');
|
|
|
|
await cdp.eval('document.getElementById("toggle-pat-btn").click()');
|
|
|
|
// Click Test Connection
|
|
await cdp.eval('document.getElementById("btn-test-cred").click()');
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
|
|
const feedback = await cdp.eval('document.getElementById("modal-feedback").textContent');
|
|
assert.ok(feedback.includes('Connection successful'), `Feedback should indicate success: ${feedback}`);
|
|
});
|
|
|
|
// Test 4: Save Credentials & Canary Leak Inspection
|
|
await step('Save Credentials & Memory Canary Inspection', async () => {
|
|
// Click Save & Close
|
|
await cdp.eval('document.getElementById("btn-save-cred").click()');
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
const isModalClosed = await cdp.eval('document.getElementById("modal-backdrop").classList.contains("hidden")');
|
|
assert.equal(isModalClosed, true, 'Modal should close after save');
|
|
|
|
const isActive = await cdp.eval('document.getElementById("cred-indicator").classList.contains("active")');
|
|
assert.equal(isActive, true, 'Credential indicator should turn green/active');
|
|
|
|
// Canary inspection: ensure credentials never entered localStorage, sessionStorage, or URL
|
|
const canary = 'dummy-pat-123';
|
|
const localDump = await cdp.eval('JSON.stringify(localStorage)');
|
|
const sessionDump = await cdp.eval('JSON.stringify(sessionStorage)');
|
|
const href = await cdp.eval('window.location.href');
|
|
|
|
assert.ok(!localDump.includes(canary), 'localStorage must not contain credential canary');
|
|
assert.ok(!sessionDump.includes(canary), 'sessionStorage must not contain credential canary');
|
|
assert.ok(!href.includes(canary), 'URL must not contain tokens');
|
|
});
|
|
|
|
// Test 5: Research Query Submission & Loading View
|
|
await step('Query Submission & Loading Thinking Orb', async () => {
|
|
// Set prompt
|
|
await cdp.eval('document.getElementById("prompt-input").value = "How do I deploy service X?"');
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
|
|
// Check loading view
|
|
const isLoadingVisible = await cdp.eval('!document.getElementById("view-loading").classList.contains("hidden")');
|
|
assert.equal(isLoadingVisible, true, 'Loading view must be visible while query executes');
|
|
|
|
const statusText = await cdp.eval('document.querySelector(".loading-status").textContent');
|
|
assert.equal(statusText, 'Agent researching Confluence...');
|
|
|
|
const orbPresent = await cdp.eval('!!document.querySelector("canvas.thinking-orb")');
|
|
assert.equal(orbPresent, true, 'Thinking orb canvas must be present');
|
|
|
|
// Wait for query completion and result view
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
|
|
const isResultVisible = await cdp.eval('!document.getElementById("view-result").classList.contains("hidden")');
|
|
assert.equal(isResultVisible, true, 'Result view must be displayed upon query completion');
|
|
});
|
|
|
|
// Test 6: Result View Rendering, Citation Links, and Security
|
|
await step('Result View, Citation Links & Sanitization', async () => {
|
|
const heading = await cdp.eval('document.querySelector("#output-content h1").textContent');
|
|
assert.ok(heading.includes('Deployment Guide for Service X'));
|
|
|
|
// Verify citation link attributes
|
|
const linkTarget = await cdp.eval('document.querySelector("#output-content a").target');
|
|
const linkRel = await cdp.eval('document.querySelector("#output-content a").rel');
|
|
const linkHref = await cdp.eval('document.querySelector("#output-content a").href');
|
|
|
|
assert.equal(linkTarget, '_blank', 'Citation link must open in new tab');
|
|
assert.equal(linkRel, 'noopener noreferrer', 'Citation link must have rel=noopener noreferrer');
|
|
assert.ok(linkHref.includes('847291'));
|
|
|
|
// Intercept click: verify click targets canonical URL without automatic prefetch
|
|
const citationTarget = await cdp.eval(`
|
|
(() => {
|
|
const link = document.querySelector("#output-content a");
|
|
return {
|
|
target: link.getAttribute('target'),
|
|
rel: link.getAttribute('rel'),
|
|
href: link.href
|
|
};
|
|
})()
|
|
`);
|
|
assert.equal(citationTarget.target, '_blank');
|
|
assert.equal(citationTarget.rel, 'noopener noreferrer');
|
|
assert.ok(citationTarget.href.startsWith('https://approved.example.com'));
|
|
});
|
|
|
|
// Test 7: Export to MD Client-Side Blob and Exact Bytes
|
|
await step('Export to MD Client-Side Blob and Exact Bytes', async () => {
|
|
const exportResult = await cdp.eval(`
|
|
new Promise((resolve) => {
|
|
let capturedBlob = null;
|
|
let revoked = false;
|
|
const origCreate = URL.createObjectURL;
|
|
const origRevoke = URL.revokeObjectURL;
|
|
|
|
URL.createObjectURL = (blob) => {
|
|
capturedBlob = blob;
|
|
return origCreate(blob);
|
|
};
|
|
URL.revokeObjectURL = (url) => {
|
|
revoked = true;
|
|
return origRevoke(url);
|
|
};
|
|
|
|
document.getElementById("export-btn").click();
|
|
|
|
URL.createObjectURL = origCreate;
|
|
URL.revokeObjectURL = origRevoke;
|
|
|
|
if (!capturedBlob) {
|
|
resolve({ error: 'No blob captured' });
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
resolve({
|
|
text: reader.result,
|
|
size: capturedBlob.size,
|
|
type: capturedBlob.type,
|
|
revoked
|
|
});
|
|
};
|
|
reader.readAsText(capturedBlob);
|
|
})
|
|
`);
|
|
|
|
assert.ok(!exportResult.error, exportResult.error);
|
|
assert.ok(exportResult.text.includes('Deployment Guide for Service X'), 'Exported text must match result markdown');
|
|
assert.ok(exportResult.type.includes('text/markdown'), 'Blob type must be text/markdown');
|
|
assert.equal(exportResult.revoked, true, 'Object URL must be revoked after dispatch');
|
|
});
|
|
|
|
// Test 8: Artifact Listing & Exact Download
|
|
await step('Artifact Listing & Attachment Download', async () => {
|
|
const artName = await cdp.eval('document.querySelector(".artifact-name").textContent');
|
|
assert.equal(artName, 'checklist.md');
|
|
|
|
const artMeta = await cdp.eval('document.querySelector(".artifact-meta").textContent');
|
|
assert.ok(artMeta.includes('32 B'), 'Artifact metadata should show 32 B');
|
|
|
|
// Verify download trigger executes without error
|
|
await cdp.eval('document.querySelector(".download-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
|
|
const errorBoxHidden = await cdp.eval('document.getElementById("artifacts-error").classList.contains("hidden")');
|
|
assert.equal(errorBoxHidden, true, 'Artifacts error box must remain hidden on successful download');
|
|
});
|
|
|
|
// Test 9: Sources and Request History Expansion
|
|
await step('Sources & Request History Drawer', async () => {
|
|
const headerTitle = await cdp.eval('document.getElementById("history-toggle-title").textContent');
|
|
assert.ok(headerTitle.includes('1 page accessed'));
|
|
|
|
// Expand history
|
|
await cdp.eval('document.getElementById("history-toggle-btn").click()');
|
|
const isExpanded = await cdp.eval('document.getElementById("history-toggle-btn").getAttribute("aria-expanded")');
|
|
assert.equal(isExpanded, 'true');
|
|
|
|
// Page accessed check
|
|
const pageTitle = await cdp.eval('document.querySelector(".page-title-link").textContent');
|
|
assert.equal(pageTitle, 'Deployment Guide');
|
|
const spaceKey = await cdp.eval('document.querySelector(".badge-space").textContent');
|
|
assert.equal(spaceKey, 'OPS');
|
|
|
|
// Tool call checks (2 calls)
|
|
const toolCardsCount = await cdp.eval('document.querySelectorAll(".tool-card").length');
|
|
assert.equal(toolCardsCount, 2, 'Should display 2 distinct tool call cards');
|
|
|
|
// Expand details on first tool call
|
|
await cdp.eval('document.querySelectorAll(".tool-expand-btn")[0].click()');
|
|
const detailsShown = await cdp.eval('!!document.querySelector(".tool-details")');
|
|
assert.equal(detailsShown, true, 'Tool details should be rendered lazily');
|
|
});
|
|
|
|
// Test 10: Back to Prompt Restores Text
|
|
await step('Back to Prompt Restores Textarea', async () => {
|
|
await cdp.eval('document.getElementById("back-btn").click()');
|
|
|
|
const isPromptVisible = await cdp.eval('!document.getElementById("view-prompt").classList.contains("hidden")');
|
|
assert.equal(isPromptVisible, true);
|
|
|
|
const promptVal = await cdp.eval('document.getElementById("prompt-input").value');
|
|
assert.equal(promptVal, 'How do I deploy service X?', 'Prompt text must be preserved');
|
|
});
|
|
|
|
// Test 11: Query Cancellation in Loading View & Busy Recovery
|
|
await step('Query Cancellation in Loading View & Busy Recovery', async () => {
|
|
// Switch scenario to delayed_cancellation
|
|
await cdp.eval(`
|
|
fetch("/dev/scenario", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ scenario: "delayed_cancellation" })
|
|
})
|
|
`);
|
|
|
|
// Submit query
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
const isLoading = await cdp.eval('!document.getElementById("view-loading").classList.contains("hidden")');
|
|
assert.equal(isLoading, true, 'Should enter loading view');
|
|
|
|
// Click Cancel
|
|
await cdp.eval('document.getElementById("cancel-btn").click()');
|
|
|
|
const isPrompt = await cdp.eval('!document.getElementById("view-prompt").classList.contains("hidden")');
|
|
assert.equal(isPrompt, true, 'Should return to prompt view on cancel');
|
|
|
|
const promptVal = await cdp.eval('document.getElementById("prompt-input").value');
|
|
assert.equal(promptVal, 'How do I deploy service X?', 'Prompt preserved on cancellation');
|
|
|
|
// Wait a tick for the client abort to propagate to server
|
|
await new Promise((r) => setTimeout(r, 80));
|
|
|
|
// Retry while cleanup is still pending (server holds busy for 400ms)
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
const busyText = await cdp.eval('document.getElementById("prompt-error").textContent');
|
|
assert.ok(busyText.includes('busy'), `Should report busy while cleanup pending: ${busyText}`);
|
|
|
|
// Wait for cleanup budget to expire (400ms)
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
|
|
// Reset scenario to normal and retry query
|
|
await cdp.eval(`
|
|
fetch("/dev/scenario", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ scenario: "normal" })
|
|
})
|
|
`);
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
|
|
const isRecovered = await cdp.eval('!document.getElementById("view-result").classList.contains("hidden")');
|
|
assert.equal(isRecovered, true, 'Subsequent query must recover and succeed after cleanup completes');
|
|
|
|
// Return to prompt for next tests
|
|
await cdp.eval('document.getElementById("back-btn").click()');
|
|
});
|
|
|
|
// Test 12: Error Recovery (409 Busy & 504 Timeout)
|
|
await step('Error Recovery (409 Busy & 504 Timeout)', async () => {
|
|
// 409 Busy
|
|
await cdp.eval(`
|
|
fetch("/dev/scenario", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ scenario: "409_busy" })
|
|
})
|
|
`);
|
|
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
|
|
const errorText = await cdp.eval('document.getElementById("prompt-error").textContent');
|
|
assert.ok(errorText.includes('busy'), `Should show busy error: ${errorText}`);
|
|
|
|
// 504 Timeout
|
|
await cdp.eval(`
|
|
fetch("/dev/scenario", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ scenario: "504_timeout" })
|
|
})
|
|
`);
|
|
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
|
|
const timeoutText = await cdp.eval('document.getElementById("prompt-error").textContent');
|
|
assert.ok(timeoutText.includes('timed out'), `Should show timeout error: ${timeoutText}`);
|
|
});
|
|
|
|
// Test 13: Malicious Input, CSP Defense & History Field Sanitization
|
|
await step('Malicious Markdown, CSP Defense & History Field Sanitization', async () => {
|
|
await cdp.eval(`
|
|
fetch("/dev/scenario", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ scenario: "malicious_content" })
|
|
})
|
|
`);
|
|
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
|
|
// Assert no <script> executed in document
|
|
const scriptTagsInOutput = await cdp.eval('document.querySelectorAll("#output-content script").length');
|
|
assert.equal(scriptTagsInOutput, 0, 'No executable script tags should be present in output');
|
|
|
|
// Assert no <img> or <iframe> or <form> tags survived sanitization
|
|
const imgCount = await cdp.eval('document.querySelectorAll("#output-content img").length');
|
|
const iframeCount = await cdp.eval('document.querySelectorAll("#output-content iframe").length');
|
|
const formCount = await cdp.eval('document.querySelectorAll("#output-content form").length');
|
|
|
|
assert.equal(imgCount, 0, 'No img tags allowed');
|
|
assert.equal(iframeCount, 0, 'No iframe tags allowed');
|
|
assert.equal(formCount, 0, 'No form tags allowed');
|
|
|
|
// Assert malicious javascript: link had href removed
|
|
const jsLinkHasHref = await cdp.eval(`
|
|
Array.from(document.querySelectorAll("#output-content a")).some(a => a.href && a.href.startsWith("javascript:"))
|
|
`);
|
|
assert.equal(jsLinkHasHref, false, 'No javascript: href links permitted');
|
|
|
|
// History field sanitization check
|
|
await cdp.eval('document.getElementById("history-toggle-btn").click()');
|
|
await cdp.eval('document.querySelectorAll(".tool-expand-btn")[0].click()');
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
|
|
const historyScripts = await cdp.eval('document.querySelectorAll("#history-content script").length');
|
|
const historyImgs = await cdp.eval('document.querySelectorAll("#history-content img").length');
|
|
assert.equal(historyScripts, 0, 'No script tags in history content');
|
|
assert.equal(historyImgs, 0, 'No img tags in history content');
|
|
|
|
const toolParamText = await cdp.eval('document.querySelectorAll(".tool-result-box")[0].textContent');
|
|
assert.ok(toolParamText.includes('xss-param'), 'Parameters rendered safely as plain text');
|
|
});
|
|
|
|
// Test 14: Large Output & Bounded Section Navigation
|
|
await step('Large Output & Bounded Section Navigation', async () => {
|
|
await cdp.eval(`
|
|
fetch("/dev/scenario", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ scenario: "large_output" })
|
|
})
|
|
`);
|
|
|
|
await cdp.eval('document.getElementById("back-btn").click()');
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
|
|
// Check section navigation banner
|
|
const isNavVisible = await cdp.eval('!document.getElementById("section-nav").classList.contains("hidden")');
|
|
assert.equal(isNavVisible, true, 'Section nav should be visible for large document');
|
|
|
|
const navText = await cdp.eval('document.querySelector(".section-nav-banner").textContent');
|
|
assert.ok(navText.includes('Showing section 1'), `Should show section 1: ${navText}`);
|
|
|
|
// Click Next
|
|
await cdp.eval('document.querySelectorAll(".section-btn")[1].click()');
|
|
const nextNavText = await cdp.eval('document.querySelector(".section-nav-banner").textContent');
|
|
assert.ok(nextNavText.includes('Showing section 2'), `Should show section 2: ${nextNavText}`);
|
|
|
|
// Click Previous
|
|
await cdp.eval('document.querySelectorAll(".section-btn")[0].click()');
|
|
const prevNavText = await cdp.eval('document.querySelector(".section-nav-banner").textContent');
|
|
assert.ok(prevNavText.includes('Showing section 1'), `Should return to section 1: ${prevNavText}`);
|
|
});
|
|
|
|
// Test 15: Clear Credentials
|
|
await step('Clear Credentials Memory & Guard', async () => {
|
|
await cdp.eval('document.getElementById("key-btn").click()');
|
|
await cdp.eval('document.getElementById("btn-clear-cred").click()');
|
|
|
|
const isActive = await cdp.eval('document.getElementById("cred-indicator").classList.contains("active")');
|
|
assert.equal(isActive, false, 'Indicator should be deactivated after clear');
|
|
|
|
await cdp.eval('document.getElementById("modal-close-btn").click()');
|
|
await cdp.eval('document.getElementById("back-btn").click()');
|
|
|
|
// Attempt submit without credentials
|
|
await cdp.eval('document.getElementById("submit-btn").click()');
|
|
const promptErr = await cdp.eval('document.getElementById("prompt-error").textContent');
|
|
assert.ok(promptErr.includes('credentials required'), 'Submitting without credentials must show error');
|
|
|
|
const modalReopened = await cdp.eval('!document.getElementById("modal-backdrop").classList.contains("hidden")');
|
|
assert.equal(modalReopened, true, 'Modal should open when attempting query without credentials');
|
|
});
|
|
|
|
// Test 16: Zero Automatic External Requests Network Assertion
|
|
await step('Zero Automatic External Requests Network Assertion', async () => {
|
|
// Filter any requests initiated to destinations other than loopback mock server or internal data/blob URLs
|
|
const externalRequests = cdp.networkRequests.filter((r) => {
|
|
const url = r.request.url;
|
|
if (url.startsWith(`http://127.0.0.1:${MOCK_PORT}/`) || url.startsWith('data:') || url.startsWith('blob:')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
assert.equal(
|
|
externalRequests.length,
|
|
0,
|
|
`Zero automatic external requests permitted! Found: ${externalRequests.map((r) => r.request.url).join(', ')}`
|
|
);
|
|
});
|
|
|
|
} finally {
|
|
cdp.close();
|
|
server.close();
|
|
}
|
|
|
|
console.log(`\n=== E2E Test Results: ${passed} passed, ${failed} failed ===`);
|
|
if (failed > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
runTests().catch((err) => {
|
|
console.error('Fatal E2E error:', err);
|
|
process.exit(1);
|
|
});
|