/** Trusted declarative checks. No supplied JavaScript, URLs, network or host actions. */ const shapes={click:['op','selector'],fill:['op','selector','value'],press:['op','selector','key'],reload:['op'],visible:['op','selector'],text:['op','selector','equals'],value:['op','selector','equals'],count:['op','selector','equals'],location:['op','equals']}; shapes.wait_count=['op','selector','equals']; shapes.dblclick=['op','selector']; shapes.type=['op','selector','value']; export function validateContract(value){ if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).sort().join()!=='name,steps,version'||!['browser-contract-v1','browser-contract-v2'].includes(value.version)||typeof value.name!=='string'||value.name.length<1||value.name.length>120||!Array.isArray(value.steps)||value.steps.length<1||value.steps.length>(value.version==='browser-contract-v2'?80:12))throw Error('INVALID_BROWSER_CONTRACT'); let assertions=0; for(const s of value.steps){ if(!s||typeof s!=='object'||Array.isArray(s)||!Object.hasOwn(shapes,s.op)||Object.keys(s).sort().join()!==[...shapes[s.op]].sort().join())throw Error('INVALID_BROWSER_STEP'); if('selector'in s&&(typeof s.selector!=='string'||!s.selector.length||s.selector.length>200||s.selector.includes('>>')||/^(xpath|text|internal|role)=/.test(s.selector)))throw Error('INVALID_CSS_SELECTOR'); if(s.op==='press'&&!['Enter','Tab','Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Escape','Backspace'].includes(s.key))throw Error('INVALID_BROWSER_KEY'); if(s.op==='fill'&&(typeof s.value!=='string'||s.value.length>1000))throw Error('INVALID_FILL_VALUE'); if(s.op==='type'&&(value.version!=='browser-contract-v2'||typeof s.value!=='string'||!/^[\x20-\x7e]{1,256}$/.test(s.value)))throw Error('INVALID_TYPE_VALUE'); if(['text','value'].includes(s.op)&&(typeof s.equals!=='string'||s.equals.length>1000))throw Error('INVALID_EXPECTED_TEXT'); if(['count','wait_count'].includes(s.op)&&(!Number.isInteger(s.equals)||s.equals<0||s.equals>1000))throw Error('INVALID_EXPECTED_COUNT'); if(s.op==='wait_count'&&value.version!=='browser-contract-v2')throw Error('WAIT_COUNT_REQUIRES_V2'); if(s.op==='dblclick'&&value.version!=='browser-contract-v2')throw Error('DBLCLICK_REQUIRES_V2'); if(s.op==='location'&&(value.version!=='browser-contract-v2'||typeof s.equals!=='string'||s.equals.length>500||!s.equals.startsWith('/')||s.equals.startsWith('//')||/[\\\s]/.test(s.equals)))throw Error('INVALID_EXPECTED_LOCATION'); if(['visible','text','value','count','wait_count','location'].includes(s.op))assertions++; } if(!assertions)throw Error('ASSERTION_REQUIRED'); return structuredClone(value); } export async function evaluateContract(page, raw){ const contract=validateContract(raw),results=[]; const initialOrigin=contract.version==='browser-contract-v2'?new URL(page.url()).origin:null; const mismatch=(kind,index,expected,actual)=>{const error=Error(kind+':'+index);error.assertion={step:index,expected,actual:typeof actual==='string'?actual.slice(0,1000):actual};throw error;}; for(const [index,s] of contract.steps.entries()){ const locator=s.selector?page.locator('css='+s.selector):null; if(s.op==='reload')await page.reload({waitUntil:'load',timeout:4000}); else if(s.op==='click')await locator.click({timeout:2000}); else if(s.op==='dblclick')await locator.dblclick({timeout:2000}); else if(s.op==='fill')await locator.fill(s.value,{timeout:2000}); else if(s.op==='type')await locator.pressSequentially(s.value,{timeout:2000}); else if(s.op==='press')await locator.press(s.key,{timeout:2000}); else if(s.op==='visible')await locator.waitFor({state:'visible',timeout:2000}); else if(s.op==='text'){const actual=await locator.innerText({timeout:2000});if(actual!==s.equals)mismatch('TEXT_ASSERTION_FAILED',index,s.equals,actual);} else if(s.op==='value'){const actual=await locator.inputValue({timeout:2000});if(actual!==s.equals)mismatch('VALUE_ASSERTION_FAILED',index,s.equals,actual);} else if(s.op==='count'){const actual=await locator.count();if(actual!==s.equals)mismatch('COUNT_ASSERTION_FAILED',index,s.equals,actual);} else if(s.op==='wait_count'){ const deadline=performance.now()+2000; let actual=await locator.count(); while(actual!==s.equals&&performance.now()setTimeout(resolve,25)); actual=await locator.count(); } if(actual!==s.equals)mismatch('WAIT_COUNT_ASSERTION_FAILED',index,s.equals,actual); } else if(s.op==='location'){ const current=new URL(page.url()),actual=current.pathname+current.search+current.hash; if(current.origin!==initialOrigin)mismatch('ORIGIN_ASSERTION_FAILED',index,initialOrigin,current.origin); if(actual!==s.equals)mismatch('LOCATION_ASSERTION_FAILED',index,s.equals,actual); } results.push({step:index,op:s.op,status:'PASS'}); } return {name:contract.name,contract_version:contract.version,steps:results,status:'PASS',scope:'Only the declared interactions and assertions; not whole-application equivalence.'}; }