@antprofuse/saddle-islands-tools 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antprofuse/saddle-islands-tools",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Independent Saddle–Islands engineering tools",
5
5
  "files": [
6
6
  "tools"
@@ -102,3 +102,19 @@ MT使用 `iteration stage-gate` 时必须提供 `--compile-result NEW_EXTERNAL_D
102
102
 
103
103
 
104
104
  分支未出现与组件本身隐藏必须区分。仅检查分支是否出现时使用 `{kind:"component-state", target:"cmp_123abc", componentState:"源分支名", present:false}`;同一组件可处于另一个正式状态而保持可见。公共工具观察唯一组件、实际状态和可见性,未知或缺失状态不能冒充“该分支不存在”。`present:true` 验证当前可见分支。它仍是原case内的断言,不新增平行case;分支内部的具体组件及内容继续独立断言。
105
+
106
+ ### 页面视觉(非组件身份)
107
+
108
+ `kind: "page-visual"` 保留页面视觉责任,不声明 `target`、`instance` 或组件状态,不进入cmp覆盖计数。期望支持 `fullWidth: true`、`fullHeight: true`、`noHorizontalOverflow: true`、`backgroundColor: "rgb(...)"`,至少声明一项;暂不支持按住阶段。尺寸是业务页视口的完整宽高(含起点),不是宿主壳或某个组件的尺寸。
109
+
110
+ Owner在项目配置填写 `pageSourceId`,精确引用冻结 `anchorInventory` 中 `kind: page` 的源ID;与已有 `pageRoute` 一并绑定到编译产物。公共编译拒绝未知页面、缺绑定及借用组件身份,执行前拒绝与冻结绑定不同的运行路由。
111
+
112
+ 产品在业务页面唯一根节点声明 `data-page-root="true"`,它是布尔页面载体标记,不是组件ID,不加入 `.registry`。工具在已选择的业务frame内采集该根节点,Minifish原生 `$$dataset.pageRoot` 优先;缺失、重复或非法标记失败。不按期望颜色、尺寸、CSS类猜选根节点。背景取该实际根的计算值,水平溢出同时检查该根和业务文档;运行证据保留源页面绑定与实际几何。测试阶段静态验证声明和绑定,实际渲染结果由产品阶段验证。
113
+
114
+ ### 空文字内容叶
115
+
116
+ 公开 `semantic-observation.mjs` 导出 `observeNativeEmptyText(node, {target, instance?})`,返回实际 `leafStyles`、`box`、`leafBox`、`clips`、`viewport`、身份与实例。`node` 由公开语义定位得到,`target` 是活动cmp。仅限选定组件独占一个原生 `.a-text`,其中一个SPAN内容叶(无子元素时原生实体自身为叶);拓扑不符明确失败,不按字号、颜色或业务名称选叶。原生dataset为身份权威,跨组件叶禁止借用。
117
+
118
+ 接口要求原始内容精确为空,拒绝空白/非空、缺失/重复、隐藏或透明祖先;允许空字形及零几何,但仍检查有限几何、祖先/视口裁剪。样式取真实内容叶而非外包装;用既有 `validateNativeTextStyles(e.leafStyles, expected, scale)` 比较源期望。采到样式本身不代表样式正确。
119
+
120
+ 业务行、兄弟组件、实例顺序、布局和源允许为空的条件仍由项目测试依据Spec声明,通过公开语义集合与所有权接口检查;不从cmp字符串推断业务路径,不把允许空字形扩展成普通文字可隐藏。MT只做静态及合成设施检查,MP负责实际产品运行。
@@ -9,15 +9,15 @@ import {compileCase} from '../compiler/case-plan.mjs';
9
9
  async function fixture(){
10
10
  const root=await mkdtemp(path.join(os.tmpdir(),'alternate-tool-project-')),project=path.join(root,'alternate'),configFile=path.join(project,'config.json');
11
11
  await mkdir(path.join(project,'harness/regions'),{recursive:true});await mkdir(path.join(project,'definitions'));await mkdir(path.join(project,'harness/plugin'));
12
- const config={schemaVersion:1,definitions:'definitions',catalog:'catalog.json',compilerAdapter:'harness/compiler.mjs',adapterRoots:['harness'],observationRoot:'harness',observationModules:['regions/alternate.mjs'],observationImports:['node:assert/strict','@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs'],pageRoute:'pages/alternate/view',pluginSource:'harness/plugin',repositoryRoot:'.',runtimeAdapters:{'@minifish/project-runtime':'harness/runtime.mjs','@minifish/project-host':'harness/host.mjs'}};
12
+ const config={schemaVersion:1,definitions:'definitions',catalog:'catalog.json',compilerAdapter:'harness/compiler.mjs',adapterRoots:['harness'],observationRoot:'harness',observationModules:['regions/alternate.mjs'],observationImports:['@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-observation.mjs','node:assert/strict','@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs'],pageRoute:'pages/alternate/view',pluginSource:'harness/plugin',repositoryRoot:'.',runtimeAdapters:{'@minifish/project-runtime':'harness/runtime.mjs','@minifish/project-host':'harness/host.mjs'}};
13
13
  await writeFile(configFile,JSON.stringify(config));
14
14
  await writeFile(path.join(project,'harness/compiler.mjs'),'export function compile(){throw Error("synthetic compile adapter")}');
15
15
  await writeFile(path.join(project,'harness/runtime.mjs'),'export const prepareHeldSnapshot=()=>{},independentBoundary=()=>{},assertNativeState=()=>{},projectAssert=()=>{},locateProject=()=>{},queryBoundaryIds=()=>{},validateReleasedPaymentRows=()=>{};');
16
16
  await writeFile(path.join(project,'harness/host.mjs'),'export const hostPolicy={};');
17
- await writeFile(path.join(project,'harness/regions/alternate.mjs'),"import {semanticLocator} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs';export function locate(root,name){return semanticLocator(root,name)};export function assertTarget(){}");
17
+ await writeFile(path.join(project,'harness/regions/alternate.mjs'),"import {semanticLocator} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs';import {observeNativeEmptyText} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-observation.mjs';export function locate(root,name){return semanticLocator(root,name)};export function assertTarget(){}");
18
18
  return {root,project,configFile,config,loaded:await loadProject(project,configFile)};
19
19
  }
20
- test('alternate project static closure records tool/adapter separately and copies no unrelated region',async()=>{const f=await fixture();try{const output=path.join(f.root,'artifact'),records=await packageRunner(f.loaded,output,'regions/alternate.mjs');assert(records.some(r=>r.kind==='tool'&&r.path==='harness/semantic-anchor.mjs'));for(const name of ['component-membership.mjs','held-spatial.mjs','spatial-contract.mjs','region-order.mjs','visual-expectation.mjs','visual-carrier.mjs','jsapi.mjs','browser.mjs','official-bridge-dispatch.mjs']){assert(records.some(r=>r.kind==='tool'&&r.path==='harness/'+name));assert((await readFile(path.join(output,'tools/harness',name),'utf8')).length>0);}assert(records.some(r=>r.kind==='adapter'&&r.path==='harness/regions/alternate.mjs'));assert(records.every(r=>/^[a-f0-9]{64}$/.test(r.sha256)));assert(!records.some(r=>r.path.includes('T1')));assert((await readFile(path.join(output,'adapters/harness/regions/alternate.mjs'),'utf8')).includes('../../../tools/harness/semantic-anchor.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
20
+ test('alternate project static closure records tool/adapter separately and copies no unrelated region',async()=>{const f=await fixture();try{const output=path.join(f.root,'artifact'),records=await packageRunner(f.loaded,output,'regions/alternate.mjs');assert(records.some(r=>r.kind==='tool'&&r.path==='harness/semantic-anchor.mjs'));for(const name of ['component-membership.mjs','held-spatial.mjs','spatial-contract.mjs','region-order.mjs','visual-expectation.mjs','visual-carrier.mjs','jsapi.mjs','browser.mjs','official-bridge-dispatch.mjs','empty-native-text.mjs']){assert(records.some(r=>r.kind==='tool'&&r.path==='harness/'+name));assert((await readFile(path.join(output,'tools/harness',name),'utf8')).length>0);}assert(records.some(r=>r.kind==='adapter'&&r.path==='harness/regions/alternate.mjs'));assert(records.every(r=>/^[a-f0-9]{64}$/.test(r.sha256)));assert(!records.some(r=>r.path.includes('T1')));assert((await readFile(path.join(output,'adapters/harness/regions/alternate.mjs'),'utf8')).includes('../../../tools/harness/semantic-anchor.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
21
21
  for(const [name,code] of [['unknown package',"import 'fs';"],['dependency traversal',"import '../../outside.mjs';"],['direct DOM',"export const read=root=>root.evaluate(()=>1);"],['dynamic import',"export const read=()=>import('./other.mjs');"]])test(name+' is rejected from the closed observation dependency graph',async()=>{const f=await fixture();try{await writeFile(path.join(f.project,'harness/regions/alternate.mjs'),code);await writeFile(path.join(f.project,'outside.mjs'),'export const leaked=true;');await assert.rejects(packageRunner(f.loaded,path.join(f.root,'artifact'),'regions/alternate.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
22
22
  test('symlink aliases, undeclared module, missing config, and package output are rejected',async()=>{const f=await fixture();try{const module=path.join(f.project,'harness/regions/alternate.mjs');await rm(module);await writeFile(path.join(f.project,'harness/other.mjs'),'export const safe=true;');await symlink('../other.mjs',module);await assert.rejects(packageRunner(f.loaded,path.join(f.root,'alias'),'regions/alternate.mjs'),/Symlink/);await assert.rejects(packageRunner(f.loaded,path.join(f.root,'unknown'),'regions/unknown.mjs'),/declared module/);await assert.rejects(loadProject(f.project,path.join(f.project,'missing.json')));await assert.rejects(packageRunner(f.loaded,path.join(f.project,'artifact'),'regions/alternate.mjs'),/outside/);}finally{await rm(f.root,{recursive:true,force:true});}});
23
23
  test('generic composition preserves exact preparation and original ID mappings without project names',()=>{const noop=()=>{},policy={validateSemanticTargets:noop,supplementalDefinition:()=>null,officialTitle:()=>false,validatePreparationOverrides:noop,validateFixtureTypes:noop,validateSpecialBoundaries:noop,validateIndependentFixture:noop,validatePacketPlan:noop,validatePluginEvents:noop,validatePluginAssertionIdentity:noop,validateObservationModuleName:noop};const catalog={cases:[{id:'alternate / page / value / content',title:'value'}]},definition={caseId:catalog.cases[0].id,ancestors:['open'],basis:[{path:'alternate/source.yaml'}],preparation:{},assertions:[{id:'value',kind:'text',target:'alternate / page / value',expected:'42',count:1}]},fragment={id:'open',inputs:{query:{key:'42'}},boundaries:[],actions:[{kind:'open-page'}]};const plan=compileCase(catalog,definition,[fragment],policy);assert.deepEqual(plan.inputs,fragment.inputs);assert.deepEqual(plan.assertions,definition.assertions);assert.equal(plan.case.id,definition.caseId);assert.throws(()=>compileCase(catalog,{...definition,ancestors:['unknown']},[fragment],policy),/missing preparation/);});
@@ -1,3 +1,4 @@
1
+ import {validatePageVisual} from '../harness/page-visual.mjs';
1
2
  import {validateSpatial} from '../harness/spatial-contract.mjs';
2
3
  import assert from 'node:assert/strict';
3
4
 
@@ -16,6 +17,7 @@ export function assertionComponents(plan){
16
17
  const seen=new Set(),components=new Set();
17
18
  for(const a of plan.assertions){
18
19
  assert(a&&typeof a.id==='string'&&a.id&&!seen.has(a.id),'Unique assertion identity required');seen.add(a.id);
20
+ if(a.kind==='page-visual'){validatePageVisual(a);assert(!(plan.beforeTarget??[]).some(x=>x.kind==='press'),'Page visual is not supported during held input');continue;}
19
21
  if(a.spatial!==undefined){
20
22
  assert(a.kind==='region'&&(plan.beforeTarget??[]).some(x=>x.kind==='press'),'Spatial snapshot currently requires held region assertion');
21
23
  validateSpatial(a.spatial);
@@ -30,8 +30,14 @@ export async function loadComponentContract(project){
30
30
  const inventory=await read(sourcePath);assert.equal(inventory.sha256,source.sha256,'Inventory digest mismatch');
31
31
  const registry=path.resolve(root,'../.registry');
32
32
  const active=await read(path.join(registry,'anchors.jsonc')),retired=await read(path.join(registry,'anchors-retired.jsonc'));
33
+ const parsedInventory=JSON.parse(inventory.bytes);let page;
34
+ if(config.pageSourceId!==undefined){
35
+ assert(typeof config.pageSourceId==='string'&&parsedInventory.targets.some(row=>row.kind==='page'&&row.id===config.pageSourceId),'Configured page must be an exact frozen source page');
36
+ assert(typeof config.pageRoute==='string'&&config.pageRoute,'Configured product route required');
37
+ page={sourceId:config.pageSourceId,route:config.pageRoute};
38
+ }
33
39
  const {activeComponentIds,componentStates,branches}=JSON.parse(execFileSync('python3',[fileURLToPath(new URL('../component_index_bridge.py',import.meta.url))],{input:JSON.stringify({inventory:JSON.parse(inventory.bytes),active:active.bytes.toString(),retired:retired.bytes.toString(),withStates:true}),encoding:'utf8',env:{...process.env,PYTHONDONTWRITEBYTECODE:'1'}}));
34
40
  assert.equal(git('rev-parse','HEAD'),head,'Candidate changed during contract construction');
35
41
  const binding=x=>({path:x.path,sha256:x.sha256});
36
- return {schemaVersion:1,head,activeComponentIds,componentStates,branches,config:binding(configInput),inventory:binding(inventory),registry:{active:binding(active),retired:binding(retired)}};
42
+ return {schemaVersion:1,head,activeComponentIds,componentStates,branches,...(page?{page}:{}),config:binding(configInput),inventory:binding(inventory),registry:{active:binding(active),retired:binding(retired)}};
37
43
  }
@@ -1,3 +1,4 @@
1
+ import {validatePageVisual} from './page-visual.mjs';
1
2
  import assert from 'node:assert/strict';
2
3
 
3
4
  // Serialized independently; the product declaration is observed, never inferred
@@ -18,6 +19,7 @@ export function validateComponentStates(plan,contract){
18
19
  const reference=ref=>{if(ref?.target!==undefined)assert(active.has(ref.target),'Reference must belong to the frozen active component set');};
19
20
  for(const actions of [plan.actions,plan.beforeTarget,plan.afterTarget,plan.completion])for(const action of actions??[])reference(action);
20
21
  for(const a of plan.assertions??[]){
22
+ if(a.kind==='page-visual'){validatePageVisual(a);assert(contract.page?.sourceId&&contract.page.route,'Compiler-bound page required');assert(!(plan.beforeTarget??[]).some(x=>x.kind==='press'),'Page visual is not supported during held input');continue;}
21
23
  reference(a);
22
24
  for(const ref of [...(a.spatial?.absent??[]),...(a.spatial?.order??[]).flatMap(pair=>[pair.above,pair.below])])reference(ref);
23
25
  if(a.kind==='component-state')assert(a.componentState!==undefined,'Branch state required');
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ // Platform topology only; never select by expected text, paint or business name.
3
+ export function sampleEmptyNativeText(root){
4
+ const doc=root.ownerDocument,win=doc.defaultView;
5
+ const data=(n,key)=>Object.prototype.hasOwnProperty.call(n,'$$dataset')?n.$$dataset?.[key]:n.getAttribute('data-'+key.replace(/[A-Z]/g,c=>'-'+c.toLowerCase()));
6
+ const identity=data(root,'anchorId');
7
+ if(typeof identity!=='string'||!/^cmp_[a-f0-9]{6}$/.test(identity))throw Error('Selected component identity required');
8
+ const declared=n=>Object.prototype.hasOwnProperty.call(n,'$$dataset')?Object.prototype.hasOwnProperty.call(n.$$dataset??{},'anchorId'):n.hasAttribute('data-anchor-id');
9
+ const parts=[root,...root.querySelectorAll('.a-text')].filter(n=>n.matches('.a-text'));
10
+ if(parts.length!==1)throw Error('One native text entity required');
11
+ const native=parts[0],leaves=[...native.querySelectorAll('*')].filter(n=>!n.children.length);
12
+ if(!leaves.length)leaves.push(native);
13
+ if(leaves.length!==1||(leaves[0]!==native&&leaves[0].tagName!=='SPAN'))throw Error('One native text content leaf required');
14
+ const leaf=leaves[0];
15
+ for(let n=leaf;n&&n!==root;n=n.parentElement)if(declared(n))throw Error('Foreign semantic text ownership');
16
+ if(root.textContent!=='')throw Error('Exact empty content required');
17
+ const rect=n=>{const b=n.getBoundingClientRect();return {x:b.x,y:b.y,width:b.width,height:b.height};};
18
+ let visible=true,opacity=1;const clips=[];
19
+ for(let n=leaf;n;n=n.parentElement){const s=win.getComputedStyle(n);visible&&=s.display!=='none'&&s.visibility==='visible';opacity*=Number(s.opacity);clips.push({...rect(n),clipX:/hidden|clip|auto|scroll/.test(s.overflowX),clipY:/hidden|clip|auto|scroll/.test(s.overflowY)});}
20
+ const s=win.getComputedStyle(leaf);
21
+ return {anchorId:identity,instance:data(root,'anchorInstance')??null,rawText:root.textContent,visible,opacity,box:rect(native),leafBox:rect(leaf),viewport:{x:0,y:0,width:win.innerWidth,height:win.innerHeight},clips,leafStyles:[Object.fromEntries(['fontFamily','fontSize','fontWeight','lineHeight','color'].map(k=>[k,s[k]]))]};
22
+ }
23
+ export function validateEmptyNativeText(e,{target,instance}={}){
24
+ assert.equal(e.anchorId,target,'Exact empty component identity');
25
+ if(instance!==undefined)assert.equal(String(e.instance),String(instance),'Exact empty component instance');
26
+ assert.equal(e.rawText,'','Exact empty content required');assert(e.visible&&e.opacity>0,'Empty native leaf must remain rendered');
27
+ for(const key of ['x','y','width','height'])assert(Number.isFinite(e.box[key]),'Finite empty text geometry');
28
+ assert(e.box.width>=0&&e.box.height>=0,'Empty text allows zero geometry, not negative geometry');
29
+ for(const b of [...e.clips,{...e.viewport,clipX:true,clipY:true}]){
30
+ if(b.clipX)assert(e.box.x>=b.x-.5&&e.box.x+e.box.width<=b.x+b.width+.5,'Empty text horizontal clipping');
31
+ if(b.clipY)assert(e.box.y>=b.y-.5&&e.box.y+e.box.height<=b.y+b.height+.5,'Empty text vertical clipping');
32
+ }
33
+ assert.equal(e.leafStyles.length,1,'Actual empty content leaf styles required');return e;
34
+ }
35
+ export async function observeEmptyNativeText(node,options){
36
+ assert.equal(await node.count(),1,'Unique empty component required');
37
+ return validateEmptyNativeText(await node.evaluate(sampleEmptyNativeText),options);
38
+ }
@@ -1,3 +1,4 @@
1
+ import {observePageVisual,assertPageRoute} from './page-visual.mjs';
1
2
  import {sampleComponentState,assertComponentState,assertComponentBranch,validateComponentStates} from './component-state.mjs';
2
3
  import {sampleDeclaredComponents,validateComponentMembership} from './component-membership.mjs';
3
4
  import {prepareHeldWithSpatial} from './held-spatial.mjs';
@@ -32,6 +33,7 @@ function target(root, name) { return semanticLocator(root, name); }
32
33
  export async function runMiniappCase({page, plan, testInfo}) {
33
34
  validateRegionVisualAssertions(plan.assertions);
34
35
  validateComponentStates(plan,plan.componentContract);
36
+ if(plan.assertions.some(a=>a.kind==='page-visual'))assertPageRoute(plan.componentContract.page,process.env.MINIFISH_PAGE_ROUTE);
35
37
  assert(plan.componentContract?.schemaVersion===1,'Compiler-bound component membership contract required');
36
38
  validateComponentMembership({declarations:[]},plan.componentContract.activeComponentIds);
37
39
  assert.equal(plan.resources.length, 0, 'resource handling is not implemented in this runner yet');
@@ -120,11 +122,12 @@ export async function runMiniappCase({page, plan, testInfo}) {
120
122
  if(!held.timing.eligible){result.inputValidity='invalid';result.target='invalidated';}
121
123
  }else for (const assertion of plan.assertions) {
122
124
  await synchronizeAnchors(app.root);
123
- const locator=assertion.kind==='host-navigation'?undefined:locate(app.root, assertion.target,{instance:assertion.instance});
125
+ const locator=['host-navigation','page-visual'].includes(assertion.kind)?undefined:locate(app.root, assertion.target,{instance:assertion.instance});
124
126
  try {
125
127
  result.observations.componentMembership=validateComponentMembership(await app.root.locator(':root').evaluate(sampleDeclaredComponents),plan.componentContract.activeComponentIds);
126
128
  if(assertion.componentState!==undefined&&assertion.kind!=='component-state'){await expect(locator).toHaveCount(1);await expect(locator).toBeVisible();result.observations[assertion.id+':componentState']=assertComponentState(await locator.evaluate(sampleComponentState),assertion.componentState);}
127
- if(assertion.kind==='component-state'){
129
+ if(assertion.kind==='page-visual'){result.observations[assertion.id]=await observePageVisual(app.root,assertion,plan.componentContract.page);
130
+ }else if(assertion.kind==='component-state'){
128
131
  const count=await locator.count(),members=[];assert(count<=1,'Unique branch owner required');
129
132
  if(count)members.push({componentState:await locator.evaluate(sampleComponentState),visible:await locator.isVisible()});
130
133
  result.observations[assertion.id]=assertComponentBranch({count,members},assertion.componentState,assertion.present,plan.componentContract.componentStates[assertion.target]);
@@ -0,0 +1,55 @@
1
+ import assert from 'node:assert/strict';
2
+ const fields=new Set(['fullWidth','fullHeight','noHorizontalOverflow','backgroundColor']);
3
+ export function validatePageVisual(a){
4
+ assert(a.kind==='page-visual','Page visual kind required');
5
+ assert(a.target===undefined&&a.instance===undefined&&a.componentState===undefined&&a.spatial===undefined&&a.visual===undefined,'Page observation cannot impersonate a component');
6
+ let checks=0;
7
+ for(const key of fields){
8
+ if(a[key]===undefined)continue;
9
+ if(key==='backgroundColor')assert(typeof a[key]==='string'&&a[key].trim(),'Page background must be explicit');
10
+ else assert(a[key]===true,'Page geometry flags must assert true');
11
+ checks++;
12
+ }
13
+ assert(checks>0,'Substantive page visual expectation required');
14
+ }
15
+ // Runs only in the selected business frame. The page marker is not a component ID.
16
+ export function sampleBusinessPage(root){
17
+ const doc=root.ownerDocument;
18
+ const candidates=[];
19
+ for(const n of doc.querySelectorAll('*')){
20
+ const native=Object.prototype.hasOwnProperty.call(n,'$$dataset');
21
+ const declared=native?!!n.$$dataset&&Object.prototype.hasOwnProperty.call(n.$$dataset,'pageRoot'):n.hasAttribute('data-page-root');
22
+ if(!declared)continue;
23
+ const value=native?n.$$dataset.pageRoot:n.getAttribute('data-page-root');
24
+ if(value!==true&&value!=='true')throw Error('Invalid business page marker');
25
+ candidates.push(n);
26
+ }
27
+ if(candidates.length!==1)throw Error('Exactly one declared business page root required');
28
+ const node=candidates[0],box=node.getBoundingClientRect(),style=doc.defaultView.getComputedStyle(node);
29
+ let visible=box.width>0&&box.height>0;
30
+ for(let n=node;n;n=n.parentElement){const s=doc.defaultView.getComputedStyle(n);visible&&=s.display!=='none'&&s.visibility==='visible'&&Number(s.opacity)>0;}
31
+ return {box:{x:box.x,y:box.y,width:box.width,height:box.height},viewport:{width:doc.defaultView.innerWidth,height:doc.defaultView.innerHeight},
32
+ visible,backgroundColor:style.backgroundColor,scrollWidth:node.scrollWidth,clientWidth:node.clientWidth,
33
+ documentScrollWidth:doc.documentElement.scrollWidth,frame:doc.defaultView.location.href};
34
+ }
35
+ export function assertPageVisual(metrics,a){
36
+ validatePageVisual(a);
37
+ assert(metrics?.visible,'Business page must be visible');
38
+ for(const value of [metrics.box?.x,metrics.box?.y,metrics.box?.width,metrics.box?.height,metrics.viewport?.width,metrics.viewport?.height])assert(Number.isFinite(value),'Finite page geometry required');
39
+ assert(metrics.viewport.width>0&&metrics.viewport.height>0&&metrics.box.width>0&&metrics.box.height>0,'Nonzero business page geometry required');
40
+ const near=(x,y,name)=>assert(Math.abs(x-y)<=.25,name);
41
+ if(a.fullWidth){near(metrics.box.x,0,'Page starts at viewport left');near(metrics.box.width,metrics.viewport.width,'Page full width');}
42
+ if(a.fullHeight){near(metrics.box.y,0,'Page starts at viewport top');near(metrics.box.height,metrics.viewport.height,'Page full height');}
43
+ if(a.noHorizontalOverflow){assert(Number.isFinite(metrics.scrollWidth)&&Number.isFinite(metrics.clientWidth)&&Number.isFinite(metrics.documentScrollWidth),'Page overflow measurements required');assert(metrics.scrollWidth<=metrics.clientWidth&&metrics.documentScrollWidth<=metrics.viewport.width,'No horizontal business overflow');}
44
+ if(a.backgroundColor!==undefined)assert.equal(metrics.backgroundColor,a.backgroundColor,'Page background');
45
+ return {check:'page-visual',metrics};
46
+ }
47
+ export async function observePageVisual(root,a,contract){
48
+ assert(contract?.sourceId&&contract.route,'Compiler-bound page source and route required');
49
+ return {...assertPageVisual(await root.locator(':root').evaluate(sampleBusinessPage),a),page:contract};
50
+ }
51
+
52
+ export function assertPageRoute(contract,route){
53
+ assert(contract?.sourceId&&contract.route,'Compiler-bound page required');
54
+ assert(typeof route==='string'&&route.replace(/^\//,'')===contract.route.replace(/^\//,''),'Runtime page route differs from frozen page binding');
55
+ }
@@ -195,3 +195,4 @@ export async function nativeInnerBox(node){
195
195
  }
196
196
 
197
197
  export async function sampleTextVisibility(node){return node.evaluate(sampleNativeTextVisibility);}
198
+ export {observeEmptyNativeText as observeNativeEmptyText,sampleEmptyNativeText,validateEmptyNativeText} from './empty-native-text.mjs';