news 2026/9/25 1:23:55

FAST Element 声明式复杂场景测试指南:duplicate-template-names 与 nested-elements Fixture 深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FAST Element 声明式复杂场景测试指南:duplicate-template-names 与 nested-elements Fixture 深度解析
  • 前端
  • UI组件

【免费下载链接】fast

The 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、host)
scenarios/复杂场景,涉及多个特性交互及边界情况
directives/属性与元素指令(f-repeat、f-when、f-ref等)
extensions/扩展功能(attribute maps、observer maps)
ecosystem/其他生态 API(errors、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"——即多个特性叠加、真实使用模式下的边界行为。目前包含两个 fixture:

Fixture描述
duplicate-template-names多个连接的<f-template>publisher 使用相同的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 lang="en-US"> <head> <meta charset="utf-8"> <title></title> </head> <body> <duplicate-template-element label="{{label}}"></duplicate-template-element> <script type="module" src="./main.ts"></script> </body> </html>

页面上放置一个duplicate-template-element,其label属性绑定到state.json中的label。

初始状态 state.json:

{ "label": "initial" }

模板定义 templates.html —— 关键点在于重复声明:

<f-template name="duplicate-template-element"> <template><span>{{label}}</span></template> </f-template> <f-template name="duplicate-template-element"> <template><span>{{label}}</span></template> </f-template>

两个<f-template>的name完全相同,且都在文档加载后连接(connected)。这就是"duplicate connected publishers"的构造方式。

组件定义 main.ts:

import { 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; });

注意两点:

  1. 组件使用template: declarativeTemplate(),它会自动注册 FAST 内部的<f-template>publisher(见 WRITING_FIXTURES.md 关于main.ts的约定)。
  2. 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 被组织进同一个 bucket,publisher 存放在Set<TemplatePublisher>中;
  • 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 内同时验证了四组能力:

  1. 三层嵌套自定义元素(parent-element→child-element→grand-child-element)间的状态传播与水合;
  2. 父→子属性绑定水合时不重复生成子元素的结构化视图(对比 SSR 与水合后的 DOM 计数);
  3. f-repeat内部事件处理中$c.parent上下文访问(this绑定到宿主元素);
  4. 重复内容内嵌f-when条件渲染。

3.1 入口页面与初始状态

entry.html 放置了 3 个parent-element实例(同一category、不同列表数据)、事件测试元素与绑定宿主元素:

<body> <parent-element category="{{category}}"></parent-element> <parent-element title="Empty List" :items="{{emptyItems}}" category="{{category}}"></parent-element> <parent-element title="Single 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 type="module" src="./main.ts"></script> </body>

state.json 提供了category: "General"、三个列表数据集、whenRepeatItems(Alpha/Beta)等初始状态。注意第二个parent-element使用了属性绑定:items="{{emptyItems}}"——按 WRITING_FIXTURES.md 的约定,当同一元素的多个实例需要不同的同名属性值时,属性绑定(:前缀)是允许的写法。

3.2 三层嵌套与状态传播(parent → child → grand-child)

模板链 templates.html:

parent-element模板通过f-repeat渲染子元素,并把自身属性下传:

<f-template name="parent-element"> <template> <div class="list-container"> <h2>{{title}}</h2> <div class="items"> <f-repeat value="{{item in items}}" positioning="true"> <child-element text="{{item.text}}" idx="{{$index}}" category="{{category}}" ></child-element> </f-repeat> </div> </div> </template> </f-template>

child-element模板继续把category下传给孙元素:

<f-template name="child-element"> <template> <div class="item"> <span class="index">{{idx}}</span> <span class="text">{{text}}</span> <grand-child-element category="{{category}}"></grand-child-element> </div> </template> </f-template> <f-template name="grand-child-element"> <template> <span class="category">{{category}}</span> </template> </f-template>

这里体现了声明式模板的绑定上下文规则:f-repeat内部的绑定,凡是无上下文前缀的路径,都解析到自定义元素(宿主)自身。因此category="{{category}}"在child-element模板中取的是宿主child-element的category属性,{{item.text}}与{{$index}}则分别取重复项数据与索引。

组件定义 main.ts 中的关键点:

  • ItemList(parent-element)在connectedCallback中先于super.connectedCallback()设置title与items,注释明确指出这是为了让数据在ElementController.bindObservables重放绑定时立即可用;
  • 各元素定义均传入[observerMap()]扩展,使模板中发现的根属性获得深度响应式观察;
  • Item(child-element)通过deepMerge(this, data)应用模拟获取到的数据——deepMerge来自@microsoft/fast-element/declarative-utilities.js,它替换数组引用而非原地更新,从而避免同步重入并让 repeat 绑定观察到新数组引用(见 syntax.md 的observerMap一节)。

测试断言(nested-elements.spec.ts):

test("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 name="parent-binding-host"> <template> <f-repeat value="{{item in parentBoundItems}}"> <parent-bound-child appearance="full-page" :actions="{{item.actions}}" :progress="{{item.progress}}" ></parent-bound-child> </f-repeat> </template> </f-template> <f-template name="parent-bound-child"> <template> <f-when value="{{appearance == 'full-page'}}"> <f-when value="{{progress}}"> <div class="progress">{{progress.percent}}%</div> </f-when> <f-when value="{{actions && actions.trailing}}"> <f-repeat value="{{action in actions.trailing}}"> <button class="action" type="button">{{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 name="test-element-repeat-event"> <template> <ul> <f-repeat value="{{item in repeatEventItems}}"> <li> <button type="button" @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)。测试过程:

  1. 初始为空列表(按钮数为 0);
  2. 动态设置repeatEventItems为[{ name: "Alpha" }, { name: "Beta" }],按钮变为 2 个;
  3. 点击第一个按钮,断言宿主元素上出现了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 name="test-when-in-repeat"> <template> <ul> <f-repeat value="{{item in whenRepeatItems}}"> <li> <f-when value="{{showNames}}"> <button class="name" type="button" @click="{$c.parent.handleItemClick($e)}">{{item.name}}</button> </f-when> </li> </f-repeat> </ul> </template> </f-template>

注意:f-when的值{{showNames}}是无前缀路径,按上下文规则解析到宿主元素,而按钮内文本{{item.name}}解析到 repeat 项。测试流程完整覆盖了条件渲染的生命周期:

  1. showNames默认true,两个按钮渲染并可点击(点击后宿主收到clickedItemName);
  2. 切换showNames = false,按钮全部消失(数量为 0);
  3. 再切回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 --config=playwright.declarative.webui.config.ts,详见 syntax.md 的 WebUI Integration Testing 一节)。因此 fixtures 中的main.ts必须使用包名导入(如@microsoft/fast-element/declarative.js)而非相对路径,以保证在 webui 集成构建的目录结构下依然可解析。

4.2 新增场景 fixture 的约定

若要为本目录新增一个复杂场景用例,需遵循 WRITING_FIXTURES.md 的完整约定:

  1. 在scenarios/下创建 kebab-case 命名的子目录;
  2. 提供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选项);
  3. main.ts中元素类继承FASTElement、使用template: declarativeTemplate()定义,需要时附加observerMap()/attributeMap()扩展,并在元素连接前调用enableHydration();
  4. 在 spec 中导航前先建立page.waitForFunction(() => (window as any).hydrationCompleted === true)等待,确保断言不早于水合完成执行;
  5. 运行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组件

【免费下载链接】fast

The adaptive interface system for modern web experiences.

项目地址:https://gitcode.com/gh_mirrors/fa/fast
点击查看免费下载
上一篇:黑苹果长期维护机型EFI配置终极指南:从新手到专家的完整教程
下一篇:flame_gamepads 手柄输入接入指南:在 Flame 游戏中桥接 gamepads 包

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 1:23:36

机器学习与语义分割在岩石薄片自动鉴定中的工程实践

简介&#xff1a;一份面向地质学与计算机交叉方向学习者的机器学习实战项目&#xff0c;围绕岩石薄片图像自动鉴定任务&#xff0c;整合了从数据集标注、特征提取、模型训练到测试评估的完整流程。资源既包含CNN等深度学习模型的构建与训练代码&#xff0c;也提供随机森林、SVM…

作者头像 李华
网站建设 2026/9/25 1:22:42

Sci-Down文献下载实操指南:从DOI定位到PDF管理的完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:21:23

ESP32上WASM硬件调用的原理与安全实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:21:17

TCNOpen开源TRDP协议栈Linux编译与列车通信测试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:21:15

十款HTML5播放器横评与集成踩坑指南

写网页时最容易让人从“还蛮简单”变成“怎么又黑了”的&#xff0c;就是视频播放器。<video src"demo.mp4" controls></video>这句代码放了多久&#xff0c;它就一直都好使&#xff0c;能播、能暂停、能拖进度。可一旦换到真实项目&#xff0c;需求立刻…

作者头像 李华