oracle7-chameleon / scripts /browser_contract.test.mjs
psiloceyeben's picture
Upload folder using huggingface_hub
09aeaeb verified
Raw History Blame Contribute Delete
3.55 kB
import test from 'node:test';
import assert from 'node:assert/strict';
import {validateContract,evaluateContract} from './browser_contract.mjs';
const wrap=steps=>({version:'browser-contract-v1',name:'control',steps});
test('typing is bounded v2, appends through keyboard API and never invokes fill',async()=>{
const raw={...wrap([{op:'type',selector:'input',value:'ython'},{op:'value',selector:'input',equals:'Python'}]),version:'browser-contract-v2'};
let value='P';const page={url:()=> 'https://local.invalid/',locator:()=>({
pressSequentially:async(text,opts)=>{assert.equal(opts.timeout,2000);value+=text;},
fill:()=>{throw Error('must not fill');},inputValue:async()=>value})};
assert.equal((await evaluateContract(page,raw)).status,'PASS');
for(const bad of ['', 'x'.repeat(257),'x\n','é',null])
assert.throws(()=>validateContract({...raw,steps:[{...raw.steps[0],value:bad},raw.steps[1]]}));
assert.throws(()=>validateContract({...raw,version:'browser-contract-v1'}));
});
test('double click requires v2 and uses bounded native browser action',async()=>{
const raw=wrap([{op:'dblclick',selector:'label'},{op:'count',selector:'label',equals:1}]);
assert.throws(()=>validateContract(raw),/DBLCLICK_REQUIRES_V2/);
let clicks=0;const page={url:()=> 'https://local.invalid/',locator:()=>({dblclick:async options=>{assert.equal(options.timeout,2000);clicks++;},count:async()=>1})};
assert.equal((await evaluateContract(page,{...raw,version:'browser-contract-v2'})).status,'PASS');
assert.equal(clicks,1);
});
test('eventual count is explicit v2; delayed rendering passes, permanent mismatch fails',async()=>{
const input={...wrap([{op:'wait_count',selector:'.tile',equals:2}]),version:'browser-contract-v2'};
assert.throws(()=>validateContract({...input,version:'browser-contract-v1'}),/WAIT_COUNT_REQUIRES_V2/);
let reads=0;
const page={url:()=> 'https://local.invalid/',locator:()=>({count:async()=>++reads<3?0:2})};
assert.equal((await evaluateContract(page,input)).status,'PASS');
assert.equal(reads,3);
const wrong={url:page.url,locator:()=>({count:async()=>0})};
await assert.rejects(evaluateContract(wrong,input),/WAIT_COUNT_ASSERTION_FAILED/);
// Original immediate contract semantics stay immediate.
reads=0;
await assert.rejects(evaluateContract(page,wrap([{op:'count',selector:'.tile',equals:2}])),/COUNT_ASSERTION_FAILED/);
assert.equal(reads,1);
});
test('rejects executable and unbounded contracts',()=>{
for(const steps of [[{op:'evaluate',code:'process.exit()'}],[{op:'click',selector:'button'}],Array(13).fill({op:'visible',selector:'body'}),[{op:'visible',selector:'xpath=//body'}],[{op:'count',selector:'body',equals:-1}],[{op:'visible',selector:'body',url:'https://example.com'}]])assert.throws(()=>validateContract(wrap(steps)));
});
test('contract is copied and requires explicit assertions',()=>{
const input=wrap([{op:'visible',selector:'body'}]);const copy=validateContract(input);copy.steps[0].selector='button';assert.equal(input.steps[0].selector,'body');
});
test('evaluator executes declared operations and catches wrong output',async()=>{
let value='';const page={locator:selector=>{assert.equal(selector,'css=input');return {fill:async v=>{value=v;},inputValue:async()=>value};}};
const result=await evaluateContract(page,wrap([{op:'fill',selector:'input',value:'hello'},{op:'value',selector:'input',equals:'hello'}]));assert.equal(result.steps.length,2);
await assert.rejects(evaluateContract(page,wrap([{op:'value',selector:'input',equals:'wrong'}])),/VALUE_ASSERTION_FAILED/);
});