File size: 3,552 Bytes
09aeaeb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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/);
});