474 lines
21 KiB
TypeScript
474 lines
21 KiB
TypeScript
/**
|
|
* Comprehensive Browser-based E2E Verification & Screenshot Audit for Diablo II Skill Calculator.
|
|
*/
|
|
|
|
import { spawn, type ChildProcess } from 'child_process'
|
|
import { createServer } from 'http'
|
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, copyFileSync } from 'fs'
|
|
import { join, extname } from 'path'
|
|
|
|
async function sleep(ms: number) {
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|
|
|
|
const MIME_TYPES: Record<string, string> = {
|
|
'.html': 'text/html',
|
|
'.js': 'application/javascript',
|
|
'.css': 'text/css',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.json': 'application/json',
|
|
}
|
|
|
|
async function runAudit() {
|
|
console.log('=== Starting Diablo II Skill Calculator Browser Audit ===')
|
|
|
|
// 1. Start simple static server on port 8899 serving dist/
|
|
const PORT = 8899
|
|
const baseDir = join(process.cwd(), 'dist')
|
|
if (!existsSync(baseDir)) {
|
|
throw new Error('dist/ directory does not exist! Run npm run build first.')
|
|
}
|
|
|
|
const server = createServer((req, res) => {
|
|
let reqPath = req.url?.split('?')[0]?.split('#')[0] || '/'
|
|
if (reqPath === '/') reqPath = '/skills.html'
|
|
const filePath = join(baseDir, reqPath)
|
|
|
|
if (existsSync(filePath)) {
|
|
const ext = extname(filePath)
|
|
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' })
|
|
res.end(readFileSync(filePath))
|
|
} else {
|
|
// Check public/ folder for skill images
|
|
const publicPath = join(process.cwd(), 'public', reqPath)
|
|
if (existsSync(publicPath)) {
|
|
const ext = extname(publicPath)
|
|
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' })
|
|
res.end(readFileSync(publicPath))
|
|
} else {
|
|
res.writeHead(404)
|
|
res.end('Not found: ' + reqPath)
|
|
}
|
|
}
|
|
})
|
|
|
|
await new Promise<void>(resolve => server.listen(PORT, '127.0.0.1', () => resolve()))
|
|
console.log(`1. Local HTTP server listening at http://127.0.0.1:${PORT}/`)
|
|
|
|
// 2. Launch headless Chrome with CDP
|
|
console.log('2. Starting Chrome headless on port 9223...')
|
|
const chromeProc: ChildProcess = spawn('/usr/bin/google-chrome', [
|
|
'--headless=new',
|
|
'--remote-debugging-port=9223',
|
|
'--no-sandbox',
|
|
'--disable-gpu',
|
|
'--disable-extensions',
|
|
'--window-size=1280,900',
|
|
`http://127.0.0.1:${PORT}/skills.html`,
|
|
])
|
|
|
|
let wsUrl: string | null = null
|
|
for (let i = 0; i < 40; i++) {
|
|
await sleep(300)
|
|
try {
|
|
const res = await fetch('http://127.0.0.1:9223/json/list')
|
|
const pages = await res.json()
|
|
const page = pages.find((p: any) => p.type === 'page' && p.url.includes('skills.html'))
|
|
if (page && page.webSocketDebuggerUrl) {
|
|
wsUrl = page.webSocketDebuggerUrl
|
|
break
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if (!wsUrl) {
|
|
chromeProc.kill()
|
|
server.close()
|
|
throw new Error('Failed to connect to Chrome headless on port 9223')
|
|
}
|
|
|
|
console.log('3. Connected to Chrome WebSocket:', wsUrl)
|
|
const ws = new WebSocket(wsUrl)
|
|
await new Promise(resolve => ws.onopen = resolve)
|
|
|
|
let idCounter = 1
|
|
function sendCommand(method: string, params: any = {}): Promise<any> {
|
|
return new Promise((resolve, reject) => {
|
|
const id = idCounter++
|
|
const handler = (event: any) => {
|
|
const msg = JSON.parse(event.data)
|
|
if (msg.id === id) {
|
|
ws.removeEventListener('message', handler)
|
|
if (msg.error) reject(msg.error)
|
|
else resolve(msg.result)
|
|
}
|
|
}
|
|
ws.addEventListener('message', handler)
|
|
ws.send(JSON.stringify({ id, method, params }))
|
|
})
|
|
}
|
|
|
|
await sendCommand('Runtime.enable')
|
|
await sendCommand('Page.enable')
|
|
await sleep(1500)
|
|
|
|
// Test 1: Check initial Amazon render
|
|
console.log('4. Auditing initial page render (Amazon)...')
|
|
const initialData = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const titles = Array.from(document.querySelectorAll('.tree-title')).map(el => el.textContent.trim());
|
|
const sockets = document.querySelectorAll('.skill-socket');
|
|
const reqLvl = document.getElementById('val-req-level')?.textContent;
|
|
const spent = document.getElementById('val-points-spent')?.textContent;
|
|
return { titles, socketCount: sockets.length, reqLvl, spent };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' Initial render state:', initialData.result.value)
|
|
if (initialData.result.value.socketCount !== 30) {
|
|
throw new Error(`Expected 30 skill sockets for Amazon, found: ${initialData.result.value.socketCount}`)
|
|
}
|
|
if (initialData.result.value.titles.length !== 3) {
|
|
throw new Error(`Expected 3 tree titles, found: ${initialData.result.value.titles.length}`)
|
|
}
|
|
|
|
// Test 2: Left-click point allocation on Jab (id 10)
|
|
console.log('5. Auditing Left-click allocation on Jab (id 10)...')
|
|
const allocData = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const jabSocket = document.querySelector('.skill-socket[data-skill-id="10"]');
|
|
if (!jabSocket) return { error: 'Jab socket not found' };
|
|
// Left click
|
|
jabSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
const newJab = document.querySelector('.skill-socket[data-skill-id="10"]');
|
|
const badge = newJab?.querySelector('.skill-pts-badge')?.textContent;
|
|
const reqLvl = document.getElementById('val-req-level')?.textContent;
|
|
const spent = document.getElementById('val-points-spent')?.textContent;
|
|
return { badge, reqLvl, spent };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' After 1 click on Jab:', allocData.result.value)
|
|
if (allocData.result.value.badge !== '1') {
|
|
throw new Error(`Expected Jab badge to be 1, got ${allocData.result.value.badge}`)
|
|
}
|
|
|
|
// Shift+Click allocation (+5 points)
|
|
console.log('6. Auditing Shift+Left-click allocation on Jab (+5 points)...')
|
|
const shiftAllocData = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const jabSocket = document.querySelector('.skill-socket[data-skill-id="10"]');
|
|
jabSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
const newJab = document.querySelector('.skill-socket[data-skill-id="10"]');
|
|
const badge = newJab?.querySelector('.skill-pts-badge')?.textContent;
|
|
const spent = document.getElementById('val-points-spent')?.textContent;
|
|
return { badge, spent };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' After Shift+Click on Jab:', shiftAllocData.result.value)
|
|
if (shiftAllocData.result.value.badge !== '6') {
|
|
throw new Error(`Expected Jab badge to be 6, got ${shiftAllocData.result.value.badge}`)
|
|
}
|
|
|
|
// Max out Jab to 20 points
|
|
await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const jabSocket = document.querySelector('.skill-socket[data-skill-id="10"]');
|
|
for (let i = 0; i < 3; i++) {
|
|
jabSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
}
|
|
})()`,
|
|
})
|
|
|
|
// Test 3: Prerequisite chain unlocking
|
|
console.log('7. Auditing Prerequisite chain: Power Strike (id 14) and Impale (id 19)...')
|
|
const prereqCheck = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const psSocket = document.querySelector('.skill-socket[data-skill-id="14"]');
|
|
const impSocket = document.querySelector('.skill-socket[data-skill-id="19"]');
|
|
const psLocked = psSocket?.classList.contains('locked');
|
|
const impLocked = impSocket?.classList.contains('locked');
|
|
// Allocate 5 points to Power Strike
|
|
psSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
const newPs = document.querySelector('.skill-socket[data-skill-id="14"]');
|
|
const psBadge = newPs?.querySelector('.skill-pts-badge')?.textContent;
|
|
return { psLocked, impLocked, psBadge };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' Prerequisite unlocking result:', prereqCheck.result.value)
|
|
if (prereqCheck.result.value.psLocked || prereqCheck.result.value.impLocked) {
|
|
throw new Error('Power Strike or Impale remained locked after allocating prerequisite Jab!')
|
|
}
|
|
if (prereqCheck.result.value.psBadge !== '5') {
|
|
throw new Error(`Expected Power Strike badge to be 5, got ${prereqCheck.result.value.psBadge}`)
|
|
}
|
|
|
|
// Test 4: Tooltip verification on Charged Strike (id 24)
|
|
console.log('8. Auditing Hover Tooltip on Charged Strike (id 24)...')
|
|
const tooltipCheck = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const csSocket = document.querySelector('.skill-socket[data-skill-id="24"]');
|
|
const rect = csSocket.getBoundingClientRect();
|
|
csSocket.dispatchEvent(new MouseEvent('mouseenter', {
|
|
bubbles: true,
|
|
clientX: rect.left + 20,
|
|
clientY: rect.top + 20
|
|
}));
|
|
const tt = document.getElementById('skill-tooltip');
|
|
const display = tt?.style.display;
|
|
const title = tt?.querySelector('.tt-title')?.textContent;
|
|
const damage = tt?.querySelector('.tt-damage-main')?.textContent?.trim();
|
|
const synergies = Array.from(tt?.querySelectorAll('.tt-syn-item') || []).map(el => el.textContent.trim());
|
|
return { display, title, damage, synergies };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' Hover Tooltip data:', tooltipCheck.result.value)
|
|
if (tooltipCheck.result.value.display !== 'block') {
|
|
throw new Error('Tooltip was not displayed on mouseenter!')
|
|
}
|
|
if (!tooltipCheck.result.value.title?.includes('充能') && !tooltipCheck.result.value.title?.includes('Charged Strike')) {
|
|
throw new Error(`Unexpected tooltip title: ${tooltipCheck.result.value.title}`)
|
|
}
|
|
|
|
// Test 5: Right-click deallocation and Orphan Protection
|
|
console.log('9. Auditing Orphan Protection on Jab and Power Strike...')
|
|
const orphanCheck = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
// Allocate prerequisites for Charged Strike (24): Poison Javelin (15) -> Lightning Bolt (20) -> Charged Strike (24)
|
|
document.querySelector('.skill-socket[data-skill-id="15"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
document.querySelector('.skill-socket[data-skill-id="20"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
document.querySelector('.skill-socket[data-skill-id="24"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
|
|
// Now attempt to right-click Power Strike (14) down to 0 points with shiftKey
|
|
document.querySelector('.skill-socket[data-skill-id="14"]')?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
|
|
const newPs = document.querySelector('.skill-socket[data-skill-id="14"]');
|
|
const psBadge = newPs?.querySelector('.skill-pts-badge')?.textContent;
|
|
const toastText = document.getElementById('toast')?.textContent;
|
|
return { psBadge, toastText };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' Orphan protection result:', orphanCheck.result.value)
|
|
if (orphanCheck.result.value.psBadge === '0') {
|
|
throw new Error('Orphan protection failed! Power Strike was reduced to 0 while Charged Strike still depends on it!')
|
|
}
|
|
|
|
// Test 6: Class switcher to Sorceress, URL Hash, and bilingual toggle
|
|
console.log('10. Auditing Class Switcher to Sorceress (sor)...')
|
|
const sorCheck = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const sorBtn = document.querySelector('.class-btn[data-class-code="sor"]');
|
|
sorBtn.click();
|
|
const titles = Array.from(document.querySelectorAll('.tree-title')).map(el => el.textContent.trim());
|
|
const hash = window.location.hash;
|
|
return { titles, hash };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' Sorceress tabs:', sorCheck.result.value)
|
|
if (!sorCheck.result.value.titles[0]?.includes('火焰法术') && !sorCheck.result.value.titles[0]?.includes('Fire Spells')) {
|
|
throw new Error('Failed to switch to Sorceress trees!')
|
|
}
|
|
|
|
// Test 7: Bilingual toggle
|
|
console.log('11. Auditing Language Toggle...')
|
|
const langCheck = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const btnLang = document.getElementById('btn-lang');
|
|
btnLang.click(); // Switch to EN
|
|
const titlesEn = Array.from(document.querySelectorAll('.tree-title')).map(el => el.textContent.trim());
|
|
const respecText = document.getElementById('btn-respec')?.textContent;
|
|
return { titlesEn, respecText };
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
console.log(' English switch result:', langCheck.result.value)
|
|
if (!langCheck.result.value.titlesEn[0]?.includes('Fire Spells')) {
|
|
throw new Error(`Expected English tab 'Fire Spells', got: ${langCheck.result.value.titlesEn[0]}`)
|
|
}
|
|
|
|
// Test 8: Non-Damage Skills Tooltip Display Audit (Warmth, Raise Skeleton, Battle Orders, Fanaticism)
|
|
console.log('12. Auditing Non-Damage Skills (Warmth, Raise Skeleton, Battle Orders, Fanaticism)...')
|
|
const nonDamageAudit = await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
document.getElementById('btn-lang').click(); // Back to Chinese
|
|
|
|
// 1. Warmth (37)
|
|
const warmthSocket = document.querySelector('.skill-socket[data-skill-id="37"]');
|
|
warmthSocket.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
const tt = document.getElementById('skill-tooltip');
|
|
const warmthHasDmg = Boolean(tt.querySelector('.tt-damage-box'));
|
|
const warmthEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent);
|
|
|
|
// 2. Switch to Necromancer (nec) for Raise Skeleton (70)
|
|
document.querySelector('.class-btn[data-class-code="nec"]').click();
|
|
const skelSocket = document.querySelector('.skill-socket[data-skill-id="70"]');
|
|
const smSocket = document.querySelector('.skill-socket[data-skill-id="69"]');
|
|
// Allocate 20 Raise Skeleton & 20 Skeleton Mastery
|
|
for (let i = 0; i < 4; i++) skelSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
for (let i = 0; i < 4; i++) smSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
|
|
const skelRect = skelSocket.getBoundingClientRect();
|
|
skelSocket.dispatchEvent(new MouseEvent('mouseenter', {
|
|
bubbles: true,
|
|
clientX: skelRect.left + 24,
|
|
clientY: skelRect.top + 24
|
|
}));
|
|
const skelHasDmg = Boolean(tt.querySelector('.tt-damage-box'));
|
|
const skelEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent);
|
|
|
|
// 3. Switch to Barbarian (bar) for Battle Orders (149)
|
|
document.querySelector('.class-btn[data-class-code="bar"]').click();
|
|
const boSocket = document.querySelector('.skill-socket[data-skill-id="149"]');
|
|
boSocket.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
const boHasDmg = Boolean(tt.querySelector('.tt-damage-box'));
|
|
const boEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent);
|
|
|
|
// 4. Switch to Paladin (pal) for Fanaticism (122)
|
|
document.querySelector('.class-btn[data-class-code="pal"]').click();
|
|
const fanSocket = document.querySelector('.skill-socket[data-skill-id="122"]');
|
|
fanSocket.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
const fanHasDmg = Boolean(tt.querySelector('.tt-damage-box'));
|
|
const fanEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent);
|
|
|
|
return {
|
|
warmth: { hasDmg: warmthHasDmg, effects: warmthEffects },
|
|
raiseSkeleton: { hasDmg: skelHasDmg, effects: skelEffects },
|
|
battleOrders: { hasDmg: boHasDmg, effects: boEffects },
|
|
fanaticism: { hasDmg: fanHasDmg, effects: fanEffects },
|
|
};
|
|
})()`,
|
|
returnByValue: true,
|
|
})
|
|
|
|
console.log(' Non-Damage Audit Results:', JSON.stringify(nonDamageAudit.result.value, null, 2))
|
|
const nd = nonDamageAudit.result.value
|
|
if (nd.warmth.hasDmg) throw new Error('Warmth MUST NOT display direct damage!')
|
|
if (!nd.warmth.effects.some((t: string) => t.includes('法力回復速度'))) {
|
|
throw new Error('Warmth missing mana recovery rate effect!')
|
|
}
|
|
if (nd.raiseSkeleton.hasDmg) throw new Error('Raise Skeleton MUST NOT display direct damage!')
|
|
if (!nd.raiseSkeleton.effects.some((t: string) => t.includes('骷髏總數'))) {
|
|
throw new Error('Raise Skeleton missing skeleton count effect!')
|
|
}
|
|
if (nd.battleOrders.hasDmg) throw new Error('Battle Orders MUST NOT display direct damage!')
|
|
if (!nd.battleOrders.effects.some((t: string) => t.includes('生命最大值'))) {
|
|
throw new Error('Battle Orders missing max life effect!')
|
|
}
|
|
if (nd.fanaticism.hasDmg) throw new Error('Fanaticism MUST NOT display direct damage!')
|
|
if (!nd.fanaticism.effects.some((t: string) => t.includes('攻擊速度'))) {
|
|
throw new Error('Fanaticism missing attack speed effect!')
|
|
}
|
|
console.log('✓ Non-Damage Skills Tooltip Audit Passed: No fake damage, all authentic mechanics displayed!')
|
|
|
|
// Capture Necromancer Raise Skeleton Screenshot
|
|
console.log('12.1 Capturing Necromancer Raise Skeleton Tooltip Screenshot...')
|
|
await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
document.querySelector('.class-btn[data-class-code="nec"]').click();
|
|
const skelSocket = document.querySelector('.skill-socket[data-skill-id="70"]');
|
|
const smSocket = document.querySelector('.skill-socket[data-skill-id="69"]');
|
|
for (let i = 0; i < 4; i++) skelSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
for (let i = 0; i < 4; i++) smSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
|
|
const skelRect = skelSocket.getBoundingClientRect();
|
|
skelSocket.dispatchEvent(new MouseEvent('mouseenter', {
|
|
bubbles: true,
|
|
clientX: skelRect.left + 24,
|
|
clientY: skelRect.top + 24
|
|
}));
|
|
})()`,
|
|
})
|
|
await sleep(500)
|
|
const necShot = await sendCommand('Page.captureScreenshot', { format: 'png', quality: 100, fromSurface: true })
|
|
const necBuf = Buffer.from(necShot.data, 'base64')
|
|
const necPathLocal = join(process.cwd(), 'tests', 'skills_calculator_nec_audit.png')
|
|
const necPathArtifact = '/usr/local/google/home/taodao/.gemini/jetski/brain/d8366262-b464-4b0a-bff5-16918852fb1a/skills_calculator_nec_audit.png'
|
|
const necPathX20 = '/google/data/rw/users/ta/taodao/skills_calculator_nec_audit.png'
|
|
writeFileSync(necPathLocal, necBuf)
|
|
writeFileSync(necPathArtifact, necBuf)
|
|
try { copyFileSync(necPathLocal, necPathX20) } catch {}
|
|
console.log(`✓ Saved Necromancer screenshot to: ${necPathArtifact} and x20`)
|
|
|
|
// Switch back to Sorceress and allocate Fire Ball build for final screenshot
|
|
console.log('13. Configuring Fire Sorceress build for visual audit screenshot...')
|
|
await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
document.querySelector('.class-btn[data-class-code="sor"]').click();
|
|
|
|
// Allocate 20 Fire Bolt (36)
|
|
const bolt = document.querySelector('.skill-socket[data-skill-id="36"]');
|
|
for (let i = 0; i < 4; i++) bolt.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
|
|
// Allocate 20 Fire Ball (47)
|
|
const ball = document.querySelector('.skill-socket[data-skill-id="47"]');
|
|
for (let i = 0; i < 4; i++) ball.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
|
|
// Allocate 20 Fire Mastery (61)
|
|
const fm = document.querySelector('.skill-socket[data-skill-id="61"]');
|
|
for (let i = 0; i < 4; i++) fm.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
|
|
|
|
// Add +3 All Skills
|
|
const incBtn = document.getElementById('btn-all-skills-inc');
|
|
incBtn.click(); incBtn.click(); incBtn.click();
|
|
|
|
// Trigger hover tooltip on Fire Ball (47)
|
|
const ballRect = ball.getBoundingClientRect();
|
|
ball.dispatchEvent(new MouseEvent('mouseenter', {
|
|
bubbles: true,
|
|
clientX: ballRect.left + 24,
|
|
clientY: ballRect.top + 24
|
|
}));
|
|
})()`,
|
|
})
|
|
|
|
await sleep(600)
|
|
|
|
// Capture screenshot of the full viewport
|
|
console.log('13. Capturing full-page screenshot via CDP...')
|
|
const screenshotResult = await sendCommand('Page.captureScreenshot', {
|
|
format: 'png',
|
|
quality: 100,
|
|
fromSurface: true,
|
|
})
|
|
|
|
const screenshotBuf = Buffer.from(screenshotResult.data, 'base64')
|
|
const localScreenshotPath = join(process.cwd(), 'tests', 'skills_calculator_audit.png')
|
|
const publicScreenshotPath = join(process.cwd(), 'public', 'skills_calculator_audit.png')
|
|
const x20Dir = '/google/data/rw/users/ta/taodao'
|
|
const x20Path = join(x20Dir, 'skills_calculator_audit.png')
|
|
const artifactPath = '/usr/local/google/home/taodao/.gemini/jetski/brain/d8366262-b464-4b0a-bff5-16918852fb1a/skills_calculator_audit.png'
|
|
|
|
writeFileSync(localScreenshotPath, screenshotBuf)
|
|
writeFileSync(publicScreenshotPath, screenshotBuf)
|
|
writeFileSync(artifactPath, screenshotBuf)
|
|
if (existsSync(x20Dir)) {
|
|
try {
|
|
copyFileSync(localScreenshotPath, x20Path)
|
|
console.log(`✓ Copied audit screenshot to x20: ${x20Path}`)
|
|
} catch (e) {
|
|
console.warn('Could not copy to x20:', e)
|
|
}
|
|
}
|
|
|
|
console.log(`✓ Saved screenshot to: ${localScreenshotPath}`)
|
|
console.log(`✓ Saved screenshot to: ${artifactPath}`)
|
|
|
|
// Cleanup
|
|
ws.close()
|
|
chromeProc.kill()
|
|
server.close()
|
|
|
|
console.log('=== All Browser Audit Tests Passed Successfully! ===')
|
|
}
|
|
|
|
runAudit().catch(err => {
|
|
console.error('Audit failed:', err)
|
|
process.exit(1)
|
|
})
|