@antprofuse/saddle-islands-tools 0.1.13 → 0.1.14

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.14",
4
4
  "description": "Independent Saddle–Islands engineering tools",
5
5
  "files": [
6
6
  "tools"
@@ -102,3 +102,11 @@ 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类猜选根节点。背景取该实际根的计算值,水平溢出同时检查该根和业务文档;运行证据保留源页面绑定与实际几何。测试阶段静态验证声明和绑定,实际渲染结果由产品阶段验证。
@@ -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');
@@ -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
+ }