FAST Element 声明式复杂场景测试指南:duplicate-template-names 与 nested-elements Fixture 深度解析
前端UI组件【免费下载链接】fastThe adaptive interface system for modern web experiences.项目地址https://gitcode.com/gh_mirrors/fa/fast点击查看免费下载导读本指南聚焦microsoft/fast-element声明式模板Declarative HTML体系中的scenarios 复杂场景测试夹具fixtures。scenarios目录专门用于构造多个特性同时交互 真实使用模式边界情况的端到端测试用例当前包含两个核心场景重复f-template名称时首个模板的保留策略以及嵌套自定义元素间跨 shadow 边界的状态传播、父子属性绑定水合、f-repeat内事件处理与f-when条件渲染。读完本文你将掌握这两类复杂场景的 fixture 文件组织、Playwright 断言方式以及其背后template-bridge与observerMap的源码级实现原理并了解如何扩展这类测试体系。一、scenarios 在声明式测试体系中的定位在microsoft/fast-element仓库中声明式运行时的测试通过预渲染 HTML 浏览器水合 Playwright 断言的 fixture 体系完成fixture 按类别划分见 fixtures/README.md类别说明bindings/各种绑定类型attribute、content、event、dot-syntax、hostscenarios/复杂场景涉及多个特性交互及边界情况directives/属性与元素指令f-repeat、f-when、f-ref等extensions/扩展功能attribute maps、observer mapsecosystem/其他生态 APIerrors、lifecycle、performance每个 fixture 是自包含的测试用例通过 Vite 开发服务器在真实浏览器中运行见 WRITING_FIXTURES.md。标准文件结构如下category/fixture-name/ ├── fixture-name.spec.ts # Playwright 测试 ├── entry.html # 入口模板页面上的根自定义元素构建输入 ├── index.html # 预渲染后的 HTML由构建脚本生成勿手改 ├── main.ts # 组件定义与运行时设置 ├── state.json # 服务端渲染使用的初始状态 └── templates.html # 声明式 f-template 定义其中index.html由npm run build:fixtures -w microsoft/fast-element生成源文件是entry.html、state.json、templates.html新 fixture 只需在对应类别目录下创建上述文件即可被自动发现无需额外注册。scenarios 目录的职责按 scenarios/README.md 的定义正是Fixtures for complex scenarios that may involve multiple features interacting together and edge cases that arise from real-world usage patterns——即多个特性叠加、真实使用模式下的边界行为。目前包含两个 fixtureFixture描述duplicate-template-names多个连接的f-templatepublisher 使用相同的name属性时简单绑定元素保留第一个模板分配nested-elements嵌套自定义元素跨 shadow 边界状态传播、父子属性绑定水合、f-repeat内通过$c.parent上下文访问的事件处理、以及重复内容中的f-when条件下面分别展开。二、duplicate-template-names重复模板名的首个分配保留策略2.1 场景要验证的行为当页面上存在两个f-template元素声明了相同的name例如都指向duplicate-template-element时声明式运行时必须保留第一个连接的 publisher 的模板分配并且整个过程中不得产生任何运行时错误。这是真实 Web 场景中常见的边界情况——例如多个 HTML 片段、多个 SSR 渲染源意外输出同名模板时系统需要表现出确定性行为而不是互相覆盖或抛错。2.2 Fixture 源码拆解入口页面 entry.html!DOCTYPE html html langen-US head meta charsetutf-8 title/title /head body duplicate-template-element label{{label}}/duplicate-template-element script typemodule src./main.ts/script /body /html页面上放置一个duplicate-template-element其label属性绑定到state.json中的label。初始状态 state.json{ label: initial }模板定义 templates.html —— 关键点在于重复声明f-template nameduplicate-template-element templatespan{{label}}/span/template /f-template f-template nameduplicate-template-element templatespan{{label}}/span/template /f-template两个f-template的name完全相同且都在文档加载后连接connected。这就是duplicate connected publishers的构造方式。组件定义 main.tsimport { attr } from microsoft/fast-element/attr.js; import { declarativeTemplate } from microsoft/fast-element/declarative.js; import { FASTElement } from microsoft/fast-element/fast-element.js; import { enableHydration } from microsoft/fast-element/hydration.js; class DuplicateTemplateElement extends FASTElement { attr public label: string ; } DuplicateTemplateElement.define({ name: duplicate-template-element, template: declarativeTemplate(), }); const hydration enableHydration(); void hydration.whenHydrated().then(() { (window as any).hydrationCompleted true; });注意两点组件使用template: declarativeTemplate()它会自动注册 FAST 内部的f-templatepublisher见 WRITING_FIXTURES.md 关于main.ts的约定。enableHydration()在元素连接前调用并在whenHydrated()完成后设置全局标志hydrationCompleted供 Playwright 等待。2.3 测试断言行为确定且无错误duplicate-template-names.spec.ts 的核心断言逻辑test(keeps the first template assignment without errors, async ({ page }) { // 1. 在导航前注册等待确保监听器先于页面加载生效 const hydrationCompleted page.waitForFunction( () (window as any).hydrationCompleted true, ); await page.goto(/fixtures/scenarios/duplicate-template-names/); await hydrationCompleted; // 2. 水合完成后元素初始内容为 initial const customElement page.locator(duplicate-template-element); await expect(customElement).toHaveText(initial); // 3. 通过 setAttribute 修改 label验证绑定仍然响应 await page.evaluate(() { document .querySelector(duplicate-template-element) ?.setAttribute(label, updated); }); await expect(customElement).toHaveText(updated); // 4. 全程不得产生任何 error 事件或未处理的 promise rejection const result await page.evaluate(() ({ errors: (window as any).__duplicateTemplateErrors, })); expect(result.errors).toEqual([]); });测试在beforeEach中通过page.addInitScript提前挂载了error与unhandledrejection监听器把水合与渲染过程中的所有异常收集进__duplicateTemplateErrors数组最终断言其为空——从行为正确和无错误两个维度验证了重复模板名场景的健壮性。2.4 源码级原理template-bridge 如何保留首个 publisher重复名称下的首个分配保留并非偶然其确定性来自 template-bridge.ts 中processBucket的实现private processBucket(registry: CustomElementRegistry, name: string): void { const bucket this.getBucket(registry, name); if (!bucket) { return; } // Set iteration preserves insertion order, so duplicate publishers leave // the first connected publisher responsible for pending requests. const publisher bucket.publishers.values().next().value; ... }从这段实现可以看出同名模板的 publisher 被组织进同一个 bucketpublisher 存放在SetTemplatePublisher中Set 迭代保持插入顺序因此bucket.publishers.values().next().value取出的必然是第一个连接的 publisher所有待处理请求request.publisher都被指派给这个首个 publisher重复的 publisher 不会抢占模板分配。此外与duplicate-template-names场景对应的解析级测试也存在于 template-bridge.pw.spec.ts如keeps the first publisher when duplicate publishers share a name与does not reassign a resolved template for duplicate f-template names与浏览器级 fixture 测试互为印证。三、nested-elements嵌套元素与跨 shadow 边界状态传播nested-elements是 scenarios 中最具代表性的多特性叠加场景它在一个 fixture 内同时验证了四组能力三层嵌套自定义元素parent-element→child-element→grand-child-element间的状态传播与水合父→子属性绑定水合时不重复生成子元素的结构化视图对比 SSR 与水合后的 DOM 计数f-repeat内部事件处理中$c.parent上下文访问this绑定到宿主元素重复内容内嵌f-when条件渲染。3.1 入口页面与初始状态entry.html 放置了 3 个parent-element实例同一category、不同列表数据、事件测试元素与绑定宿主元素body parent-element category{{category}}/parent-element parent-element titleEmpty List :items{{emptyItems}} category{{category}}/parent-element parent-element titleSingle Item :items{{singleItem}} category{{category}}/parent-element test-element-repeat-event/test-element-repeat-event test-when-in-repeat/test-when-in-repeat parent-binding-host/parent-binding-host script typemodule src./main.ts/script /bodystate.json 提供了category: General、三个列表数据集、whenRepeatItemsAlpha/Beta等初始状态。注意第二个parent-element使用了属性绑定:items{{emptyItems}}——按 WRITING_FIXTURES.md 的约定当同一元素的多个实例需要不同的同名属性值时属性绑定:前缀是允许的写法。3.2 三层嵌套与状态传播parent → child → grand-child模板链 templates.htmlparent-element模板通过f-repeat渲染子元素并把自身属性下传f-template nameparent-element template div classlist-container h2{{title}}/h2 div classitems f-repeat value{{item in items}} positioningtrue child-element text{{item.text}} idx{{$index}} category{{category}} /child-element /f-repeat /div /div /template /f-templatechild-element模板继续把category下传给孙元素f-template namechild-element template div classitem span classindex{{idx}}/span span classtext{{text}}/span grand-child-element category{{category}}/grand-child-element /div /template /f-template f-template namegrand-child-element template span classcategory{{category}}/span /template /f-template这里体现了声明式模板的绑定上下文规则f-repeat内部的绑定凡是无上下文前缀的路径都解析到自定义元素宿主自身。因此category{{category}}在child-element模板中取的是宿主child-element的category属性{{item.text}}与{{$index}}则分别取重复项数据与索引。组件定义 main.ts 中的关键点ItemListparent-element在connectedCallback中先于super.connectedCallback()设置title与items注释明确指出这是为了让数据在ElementController.bindObservables重放绑定时立即可用各元素定义均传入[observerMap()]扩展使模板中发现的根属性获得深度响应式观察Itemchild-element通过deepMerge(this, data)应用模拟获取到的数据——deepMerge来自microsoft/fast-element/declarative-utilities.js它替换数组引用而非原地更新从而避免同步重入并让 repeat 绑定观察到新数组引用见 syntax.md 的observerMap一节。测试断言nested-elements.spec.tstest(should pass parent attribute to child elements, async ({ page }) { // ...等待水合完成 // 每个 child 都收到父级的 category 属性 for (let i 0; i childCount; i) { await expect(childElements.nth(i)).toHaveAttribute(category, General); } // grand-child 渲染了 parent → child → grand-child 一路传递的 category for (let i 0; i childCount; i) { const categoryText grandChildren.nth(i).locator(.category); await expect(categoryText).toHaveText(General); } // 修改父级 category 为 Updated await firstParent.evaluate((node: ItemList) { node.category Updated; }); // 子元素属性与孙元素渲染同步更新 for (let i 0; i childCount; i) { await expect(childElements.nth(i)).toHaveAttribute(category, Updated); } for (let i 0; i childCount; i) { const categoryText grandChildren.nth(i).locator(.category); await expect(categoryText).toHaveText(Updated); } });该用例同时验证了水合后的初始状态正确、以及运行时的响应式更新能跨三层 shadow 边界逐级传播。3.3 父子属性绑定水合不重复结构化视图parent-bound-child与parent-binding-host专门验证水合不得导致 DOM 重复。parent-binding-host模板中f-repeat渲染的parent-bound-child使用属性绑定接收复杂对象f-template nameparent-binding-host template f-repeat value{{item in parentBoundItems}} parent-bound-child appearancefull-page :actions{{item.actions}} :progress{{item.progress}} /parent-bound-child /f-repeat /template /f-template f-template nameparent-bound-child template f-when value{{appearance full-page}} f-when value{{progress}} div classprogress{{progress.percent}}%/div /f-when f-when value{{actions actions.trailing}} f-repeat value{{action in actions.trailing}} button classaction typebutton{{action.label}}/button /f-repeat /f-when /f-when /template /f-template这里还展示了f-when支持的比较与逻辑运算符、且右操作数可以是字符串字面量full-page、绑定值progress或复合表达式actions actions.trailing。测试通过对比水合后与SSR 阶段的 DOM 计数来证明结构视图没有被重复创建expect(result).toEqual({ hydrated: { actionButtons: 2, childHydrated: true, parentHydrated: true, progressViews: 1, }, ssr: { actionButtons: 2, progressViews: 1, }, });SSR 阶段的计数parentBoundChildSsrCounts是在main.ts中于水合前直接从预渲染 DOM 读取并存入window的水合后计数与之完全一致说明父级属性绑定水合bindObservables重放不会为子元素重复创建f-when/f-repeat产生的结构化视图。测试同时通过node.$fastController.isHydrated确认父子元素控制器均已进入水合完成状态。3.4 f-repeat 内的事件处理与 $c.parent 上下文test-element-repeat-event验证事件处理在 repeat 内的this绑定。模板f-template nametest-element-repeat-event template ul f-repeat value{{item in repeatEventItems}} li button typebutton click{$c.parent.handleItemClick($e)}{{item.name}}/button /li /f-repeat /ul /template /f-template$c.parent是执行上下文execution context的父级视图模型引用——在f-repeat内部它指向宿主元素见 syntax.md 的 Execution Context Access 一节声明式表达式的$c前缀对应命令式模板中${(x, c) ...}的c。测试过程初始为空列表按钮数为 0动态设置repeatEventItems为[{ name: Alpha }, { name: Beta }]按钮变为 2 个点击第一个按钮断言宿主元素上出现了clickedItemName Alpha。main.ts中TestElementRepeatEvent.handleItemClick的实现表明this就是宿主handleItemClick(e: Event) { this.clickedItemName (e.currentTarget as HTMLButtonElement).textContent!; }测试注释明确说明若this被错误地绑定到 repeat 项而非宿主则clickedItemName不会出现在宿主元素上——这是对通过$c.parent路径从上下文中解析方法宿主这一行为的直接验证。3.5 f-when 嵌套在 f-repeat 内test-when-in-repeat把条件渲染放进重复内容f-template nametest-when-in-repeat template ul f-repeat value{{item in whenRepeatItems}} li f-when value{{showNames}} button classname typebutton click{$c.parent.handleItemClick($e)}{{item.name}}/button /f-when /li /f-repeat /ul /template /f-template注意f-when的值{{showNames}}是无前缀路径按上下文规则解析到宿主元素而按钮内文本{{item.name}}解析到 repeat 项。测试流程完整覆盖了条件渲染的生命周期showNames默认true两个按钮渲染并可点击点击后宿主收到clickedItemName切换showNames false按钮全部消失数量为 0再切回true按钮重新出现且事件仍正常再次点击断言成功。这验证了f-when在重复内容内随宿主属性变化正确增删视图且销毁重建后事件绑定不丢失。四、如何运行与扩展 scenarios 测试4.1 运行命令fixture 测试的构建与运行在packages/fast-element工作区进行# 生成所有 fixture 的 index.html由 entry.html state.json templates.html 预渲染而来 npm run build:fixtures -w microsoft/fast-element # 运行声明式 fixture 的 Playwright 测试 npm run test:chromium:declarative -w microsoft/fast-element此外这些 fixture 还会被microsoft/webui交叉渲染器集成测试复用npm run test:webui-integration -w microsoft/fast-element或分步执行npm run build:fixtures:webui -w microsoft/fast-element与npm exec -w microsoft/fast-element -- playwright test --configplaywright.declarative.webui.config.ts详见 syntax.md 的 WebUI Integration Testing 一节。因此 fixtures 中的main.ts必须使用包名导入如microsoft/fast-element/declarative.js而非相对路径以保证在 webui 集成构建的目录结构下依然可解析。4.2 新增场景 fixture 的约定若要为本目录新增一个复杂场景用例需遵循 WRITING_FIXTURES.md 的完整约定在scenarios/下创建 kebab-case 命名的子目录提供entry.html、templates.html、state.json、main.ts、name.spec.ts与fast-build.config.json标准配置为entry/state/output/templates四项如需可在fast-build.config.json中增加attribute-name-strategy选项main.ts中元素类继承FASTElement、使用template: declarativeTemplate()定义需要时附加observerMap()/attributeMap()扩展并在元素连接前调用enableHydration()在 spec 中导航前先建立page.waitForFunction(() (window as any).hydrationCompleted true)等待确保断言不早于水合完成执行运行npm run build:fixtures -w microsoft/fast-element生成index.html切勿手改生成文件再执行测试验证。entry.html的属性绑定遵循精简原则同名复杂绑定如list{{list}}无需书写非原始值会被自动剥离并由状态传播提供重命名绑定应通过调整state.json属性名避免仅在同一元素的多个实例需要不同同名属性值时使用:items{{...}}属性绑定见 fixtures/README.md 的 Entry HTML attribute guidelines。五、总结复杂场景 fixture 的工程价值scenarios目录代表声明式模板测试体系中最具挑战性的部分——单一特性测试无法覆盖的问题。duplicate-template-names验证了运行时在异常输入重复模板名下的确定性收敛行为其首个 publisher 负责语义直接由template-bridge.ts中基于 Set 插入顺序的实现保证nested-elements则把嵌套水合、状态跨层传播、repeat 内事件上下文与条件渲染四个特性叠加进同一个真实页面并通过水合前后 DOM 计数一致的方式守住水合不产生重复结构这一核心质量红线。对开发者而言这两个 fixture 既是可复制的端到端测试范本也是理解 FAST Element 声明式运行时边界行为的活文档。赞分享前端UI组件【免费下载链接】fastThe adaptive interface system for modern web experiences.项目地址https://gitcode.com/gh_mirrors/fa/fast点击查看免费下载相关推荐SWIFT swift deploy 模型服务部署完全指南OpenAI 兼容 API、多后端与多任务场景SWIFT swift deploy 模型服务部署完全指南OpenAI 兼容 API、多后端与多任务场景 SWIFT 提供了一键式的 swift deploy前端UI组件FAST Element 声明式模板扩展attributeMap 与 observerMap 深度实战指南FAST Element 声明式模板扩展attributeMap 与 observerMap 深度实战指南 导读 在 FAST Element 的声明式de前端UI组件FAST Element 声明式 HTML 语法完全指南f-template、绑定、指令与 SSR 水合FAST Element 声明式 HTML 语法完全指南f template、绑定、指令与 SSR 水合 FAST Element 的声明式 HTML 运行时前端UI组件上一篇黑苹果长期维护机型EFI配置终极指南从新手到专家的完整教程下一篇flame_gamepads 手柄输入接入指南在 Flame 游戏中桥接 gamepads 包创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考