import assert from 'node:assert/strict'; // All pages and mutations here are isolated fixtures. The ordinary fixed root // deliberately has no scrolling overflow; only the document viewport can move. export function viewportFixture(url,request,response){ if(url.pathname!=='/fixture/viewport-seek') return false; const mode=url.searchParams.get('mode') || 'offscreen'; const initialTarget=['visible','obscured'].includes(mode) ? 80 : 1100; const anchor=(top=initialTarget)=>`目标笔记`; const noTarget=['missing','time','lazy'].includes(mode); const change=mode==='source' ? `history.replaceState({},'',location.pathname+'?changed=1')` : mode==='root' ? `root.replaceWith(root.cloneNode(true))` : mode==='scroller' ? `Object.defineProperty(document,'scrollingElement',{configurable:true,get:()=>document.body})` : mode==='disappear' ? `root.querySelector('.viewport-title').remove()` : mode==='blocked' ? `fetch('/fixture/write/viewport-seek',{method:'POST'}).catch(()=>{})` : mode==='lazy' ? `setTimeout(()=>root.insertAdjacentHTML('beforeend',${JSON.stringify(anchor(500))}),250)` : ''; response.setHeader('Content-Type','text/html; charset=utf-8'); response.end(`固定根内的视口有界定位

实际视口定位样本

普通已加载内容,根容器不独立滚动

${noTarget ? '' : anchor()}${mode==='duplicate' ? anchor(1140) : ''}
目标笔记
${mode==='obscured' ? '
遮挡目标
' : ''}`); return true; } function checkSummary(result,{passed,reason,code,movement=false,zero=false}){ assert.equal(result.passed,passed,JSON.stringify(result.errors)); assert.deepEqual(result.outputs,{status:[{text:'实际视口定位样本'}]},'the real read prefix is retained even when the later seek fails'); assert.equal(result.seek_results.length,1); const summary=result.seek_results[0]; assert.deepEqual(Object.keys(summary).sort(),['step_position','scrolls','movements','target_reached','stop_reason'].sort()); assert.equal(summary.step_position,2);assert.equal(summary.target_reached,passed);assert.equal(summary.stop_reason,reason); assert.equal(summary.scrolls,summary.movements.length); assert.ok(summary.movements.every(move=>Object.keys(move).sort().join(',')==='after,before,client_height,scroll_height' && Object.values(move).every(Number.isFinite) && move.before>=0 && move.after>=move.before && move.client_height>180 && move.scroll_height>move.client_height)); for(let index=1;index0);assert.ok(summary.movements.some(move=>move.after>move.before));} if(zero){assert.equal(summary.scrolls,0);assert.deepEqual(summary.movements,[]);} assert.equal(result.verify_budget_ms,undefined,'explicit viewport scope does not expand the ordinary verify budget'); assert.deepEqual(result.error_codes,code ? [code] : [],JSON.stringify(result.errors)); assert.equal(result.step_timings[0].status,'passed'); assert.equal(result.step_timings[1].status,passed ? 'passed' : 'failed'); assert.ok(result.evidence.length>=(passed ? 3 : 4),'real entry, prefix and final/failed captures are retained'); return summary; } export async function viewportRegression(client,entryUrl,passed){ const task='viewport-seek-regression',base=new URL('/fixture/viewport-seek',entryUrl).href; await client.call('open',{task_id:task,account_key:'test-account',entry_url:base}); await client.call('ready',{task_id:task,identity_label:'隔离视口定位测试'}); const seek={kind:'scroll',selector:'#viewport-root',scroll_scope:'viewport',direction:'down',pixels:320,max_scrolls:5,timeout_ms:3000, until_target:{selector:'#viewport-global a.viewport-title',field:'text',value:'目标笔记'}}; // A required output is read before seeking, since after a genuine document // movement this first-screen status is intentionally no longer in view. const run=(mode,extra={})=>client.call('verify',{task_id:task,entry_url:base+'?mode='+mode,parameters:{}, steps:[{kind:'read',selector:'#viewport-status',output_name:'status'},{...seek,...extra}],expected_outputs:{status:'实际视口定位样本'}}); try{ const actual=await run('offscreen'); const summary=checkSummary(actual,{passed:true,reason:'target_reached',movement:true}); assert.ok(summary.scrolls<=seek.max_scrolls); assert.equal(summary.movements[0].before,0);assert.equal(summary.movements[0].after,seek.pixels); assert.ok(summary.movements.every(move=>move.after-move.before<=seek.pixels),'fixed viewport movements cannot jump directly to a target'); const final=actual.evidence.at(-1); assert.equal(final.viewport.scroll_y,summary.movements.at(-1).after,'evidence records the same actual window displacement'); assert.equal(final.elements.find(element=>element.selector==='#viewport-root')?.scroll_y,undefined,'ordinary root never becomes an implicit inner scroll container'); checkSummary(await run('visible'),{passed:true,reason:'target_reached',zero:true}); checkSummary(await run('offscreen',{scroll_scope:undefined}),{passed:false,code:'UNOBSERVED_CURRENT_SELECTOR',reason:'target_changed',zero:true}); passed('显式viewport在固定普通根内匹配并实际固定步长滚document,证据与真实metrics一致;已可见零移动,旧未声明scope仍拒绝非滚动根'); for(const [mode,code,reason,extra] of [ ['missing','SCROLL_BUDGET_EXHAUSTED','scroll_budget',{max_scrolls:1}], ['missing','SCROLL_BOUNDARY_BEFORE_TARGET','boundary',{pixels:1600,max_scrolls:3}], ['disappear','SCROLL_BUDGET_EXHAUSTED','scroll_budget',{max_scrolls:1}], ['time','SCROLL_TARGET_TIMEOUT','time_budget',{timeout_ms:1}], ]){ const failure=await run(mode,extra); const item=checkSummary(failure,{passed:false,code,reason,movement:['missing','disappear'].includes(mode)}); if(reason==='boundary') assert.equal(item.movements.at(-1).after,item.movements.at(-1).before,'boundary summary records the real no-movement attempt'); } const lazy=await run('lazy',{max_scrolls:1,timeout_ms:2000}); const lazySummary=checkSummary(lazy,{passed:true,reason:'target_reached',movement:true}); assert.equal(lazySummary.scrolls,1); passed('视口目标缺失或消失不接受根外可见同名链接,滚动/边界/时间预算分别停止;短等允许根内自然出现且不重复滚动'); for(const [mode,code,reason,movement] of [ ['duplicate','NON_UNIQUE_TARGET','ambiguous',false], ['obscured','TARGET_OBSCURED','obscured',false], ['source','SCROLL_SOURCE_CHANGED','source_changed',true], ['root','STALE_TARGET','target_changed',true], ['scroller','STALE_TARGET','target_changed',true], ['blocked','WRITE_REQUEST_BLOCKED','blocked_request',true], ]){ const failure=await run(mode); checkSummary(failure,{passed:false,code,reason,movement,zero:!movement}); } passed('视口重名/遮挡不滚动,来源/固定根/实际scrollingElement身份变化和未知POST中断;失败保留真实阅读前缀、位移与截图'); }finally{ await client.call('close',{task_id:task}); } }