Files
ycode/scripts/rea/media-assets-regression.mjs
T

110 lines
10 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import { tmpdir } from 'node:os'
import { pathToFileURL } from 'node:url'
const runtime=path.resolve('.tmp/rea-css-runtime/dist')
const load=relative=>import(pathToFileURL(path.join(runtime,relative)))
const {captureElectronMediaAssets}=await load('browser/CdpElectronMediaAssets.js')
const {electronVisualProbeInputSchema,electronMediaAssetsSchema}=await load('domain/javascript/ElectronVisualProbeSchema.js')
const {inspectElectronPageInputSchema,electronPageInspectionSchema}=await load('domain/javascript/electronObservation.js')
const {inspectCdpElectronPage}=await load('browser/CdpElectronInspection.js')
const {createElectronEvidence}=await load('application/javascript/ElectronEvidence.js')
const temporary=await fs.mkdtemp(path.join(tmpdir(),'rea-img-resource-'))
try {
const html=path.join(temporary,'index.html');await fs.writeFile(html,'<!doctype html><img class="selected">')
const page=pathToFileURL(html).href,resourceUrl=pathToFileURL(path.join(temporary,'actual.png')).href
const main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page}
const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII='
const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex')
const svgBytes=Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7"/></svg>')
const svgContent=svgBytes.toString('base64'),svgDigest=crypto.createHash('sha256').update(svgBytes).digest('hex')
const makeSnapshot=()=>({strings:[page,'main','HTML','IMG','class','selected','src','fallback.png',resourceUrl,'contenteditable','true','14px'],documents:[{
documentURL:0,frameId:1,scrollOffsetX:0,scrollOffsetY:0,
nodes:{nodeName:[2,3],parentIndex:[-1,0],backendNodeId:[10,20],attributes:[[],[4,5,6,7]],currentSourceURL:{index:[1],value:[8]}},
layout:{nodeIndex:[0,1],bounds:[[0,0,100,80],[10,10,40,50]],styles:[]}
}]})
let mode='ok',snapshot=makeSnapshot(),calls=[],listener,selectedResourceUrl=resourceUrl
const send=async(method,params={})=>{
calls.push({method,params})
assert.ok(!['Runtime.evaluate','Runtime.callFunctionOn','Runtime.getProperties','Network.getCookies','Network.loadNetworkResource','Page.navigate'].includes(method))
if(method==='Page.getFrameTree')return {frameTree:{frame:mode==='navigation-after-content'&&calls.some(c=>c.method==='Page.getResourceContent')?{...frame,id:'changed'}:frame}}
if(method==='DOM.getDocument')return {root:{nodeId:1}}
if(method==='DOM.querySelectorAll')return {nodeIds:mode==='absent'?[]:mode==='ambiguous'?[2,3]:[2]}
if(method==='DOM.describeNode')return {node:{nodeName:mode==='not-img'?'DIV':'IMG',backendNodeId:mode==='foreign-backend'?999:20}}
if(method==='Page.getLayoutMetrics')return {cssLayoutViewport:{clientWidth:100,clientHeight:80,pageX:0,pageY:0}}
if(method==='DOM.getNodeForLocation')return {backendNodeId:mode==='covered'?999:mode==='partly-covered'&&params.y>20?999:20}
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':mode==='unsupported'?'image/bmp':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
if(method==='Page.getResourceContent'){
if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak')
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):mode==='svg'?svgContent:png}
}
if(method==='DOMSnapshot.captureSnapshot'){const copy=structuredClone(snapshot);for(const doc of copy.documents)doc.layout.styles=doc.layout.nodeIndex.map(()=>params.computedStyles.map(()=>11));return copy}
if(method==='Page.captureScreenshot')return {data:png}
if(method==='Target.getTargets')return {targetInfos:[]}
return {}
}
assert.deepEqual(electronVisualProbeInputSchema.parse({}).media_selectors,[])
assert.equal(electronVisualProbeInputSchema.safeParse({media_selectors:Array.from({length:5},(_,i)=>'#img'+i)}).success,false)
assert.equal(electronVisualProbeInputSchema.safeParse({media_selectors:['#same','#same']}).success,false)
assert.equal(await captureElectronMediaAssets(snapshot,[main],[],send),undefined)
assert.equal(calls.length,0,'default off must perform no resource reads')
const capture=async()=>{calls=[];return electronMediaAssetsSchema.parse(await captureElectronMediaAssets(snapshot,[main],['.selected'],send))}
let result=await capture()
assert.equal(result.items[0].status,'exported')
assert.equal(result.items[0].source.selection,'current_source_url')
assert.equal(result.items[0].artifact.sha256,digest)
assert.equal(result.items[0].artifact.data_base64,png)
assert.equal(result.total_bytes,bytes.length)
assert.equal(calls.find(c=>c.method==='Page.getResourceContent').params.url,resourceUrl,'currentSrc wins over the fallback src attribute')
assert.equal(result.items[0].backend_node_id,20)
mode='svg';result=await capture()
assert.equal(result.items[0].status,'exported')
assert.equal(result.items[0].artifact.mime_type,'image/svg+xml')
assert.equal(result.items[0].artifact.sha256,svgDigest)
assert.equal(result.items[0].artifact.data_base64,svgContent)
mode='covered';result=await capture();assert.equal(result.items[0].status,'exported');assert.equal(result.items[0].visibility.status,'covered');assert.equal(result.items[0].selected_node.current_source_url,resourceUrl);
mode='partly-covered';assert.equal((await capture()).items[0].status,'exported','partial occlusion may still export actual loaded full image bytes')
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['unsupported','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
mode=test;result=await capture();assert.equal(result.items[0].status,'unknown',test);assert.equal(result.items[0].reason,reason,test);assert.equal(result.total_bytes,0,test)
assert.equal(result.items[0].artifact,undefined,test)
if(['not-loaded','unsupported','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false)
}
mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10]
assert.equal((await capture()).items[0].reason,'editable_subtree_excluded')
snapshot=makeSnapshot();snapshot.documents[0].layout.bounds[1]=[200,200,40,50]
assert.equal((await capture()).items[0].visibility.reason,'outside_visible_viewport')
snapshot=makeSnapshot();snapshot.documents[0].layout.nodeIndex=[0]
assert.equal((await capture()).items[0].visibility.reason,'no_visible_layout')
snapshot=makeSnapshot();delete snapshot.documents[0].nodes.currentSourceURL;snapshot.strings[7]=resourceUrl
assert.equal((await capture()).items[0].source.selection,'src')
snapshot=makeSnapshot();selectedResourceUrl='https://account:private-password@assets.invalid/img.png?token=private-query'
snapshot.strings[8]=selectedResourceUrl+'#fragment'
result=await capture()
assert.equal(result.items[0].status,'exported')
assert.equal(result.items[0].source.url,'https://assets.invalid/img.png')
assert.equal(result.items[0].selected_node.current_source_url,'https://assets.invalid/img.png')
assert.equal(result.items[0].source.url_sha256,crypto.createHash('sha256').update(snapshot.strings[8]).digest('hex'))
assert.equal(JSON.stringify(result).includes('private-'),false,'source metadata omits URL credentials and queries')
selectedResourceUrl=resourceUrl
snapshot=makeSnapshot()
// Exercise the public inspection normalization and Evidence path, not only the helper.
const input=inspectElectronPageInputSchema.parse({cdp_endpoint:'http://127.0.0.1:9223',target_id:'fixture',observation_ms:0,include_visuals:true,visual_probe:{media_selectors:['.selected']}})
const context={connection:{send,onEvent(fn){listener=fn;return()=>{listener=undefined}}},sessionId:'selected-session',input,target:{id:'fixture',type:'page',title:'Own fixture',attached:false},discovery:{version:{product:'Fixture/1',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'}}}
const inspection=electronPageInspectionSchema.parse(await inspectCdpElectronPage(context))
assert.equal(inspection.visuals.media_assets.items[0].artifact.sha256,digest)
assert.deepEqual(inspection.visuals.layout.documents[0].nodes[1].attributes,{class:'selected'})
const evidence=createElectronEvidence('inspect_electron_page',input,inspection,{id:'rea-cdp-electron',name:'Local selected resource fixture',version:'1+visual-local.3'})
assert.deepEqual(evidence.parameters.visual_probe.media_selectors,['.selected'])
assert.equal(evidence.normalized_result.visuals.media_assets.items[0].artifact.data_base64,png)
assert.match(evidence.evidence_id,/^ev_[a-f0-9]{64}$/)
const optedOut=electronPageInspectionSchema.parse(await inspectCdpElectronPage({...context,input:{...input,visual_probe:undefined}}))
assert.equal(optedOut.visuals.media_assets,undefined)
assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false)
assert.equal(listener,undefined)
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, raster/SVG export, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
}finally{await fs.rm(temporary,{recursive:true,force:true})}