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/); });