oracle7-chameleon / scripts /browser_contract.mjs
psiloceyeben's picture
Upload folder using huggingface_hub
09aeaeb verified
Raw History Blame Contribute Delete
5.06 kB
/** 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()<deadline){
await new Promise(resolve=>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.'};
}