Angular CDK Aria Accordion 组件测试 Harness 完全指南:AccordionHarness 与 AccordionGroupHarness 实战
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
导读
@angular/aria/accordion/testing是 Angular Components 仓库中 ARIA 手风琴(Accordion)组件面向测试的官方测试工具包,其公开 API 由 goldens/aria/accordion/testing/index.api.md 这份 API Extractor 报告完整锁定。本文基于该 API 报告,结合 src/aria/accordion/testing/ 下的真实源码与测试用例,系统讲解AccordionHarness、AccordionGroupHarness、AccordionSection枚举及过滤器的完整用法,你将掌握如何在 Angular 测试中定位、断言并驱动手风琴的展开、折叠、聚焦等全部交互,并理解其底层如何通过ContentContainerComponentHarness与aria-controls实现面板内内容的深度查询。
一、测试 Harness 是什么,为什么需要它
在 Angular 的测试体系中,Harness(测试工具)是 CDK 提供的一套与实现解耦的组件测试 API。它通过ComponentHarness基类封装对组件 DOM 的查找与交互,测试代码只依赖稳定的公开 API,不依赖具体 CSS 类名与内部结构。
@angular/aria/accordion/testing包定义了两种 Harness:
| Harness | 宿主选择器 | 职责 |
|---|---|---|
AccordionHarness | [ngAccordionTrigger] | 单个手风琴条目(trigger 视角)的查询与交互 |
AccordionGroupHarness | [ngAccordionGroup] | 整个手风琴组的查询,可枚举组内所有条目 |
该包的源码结构见 src/aria/accordion/testing/,其中 accordion-harness.ts 是核心实现,accordion-harness-filters.ts 定义过滤器接口,accordion-harness.spec.ts 则是对应测试套件,而 public-api.ts 与 index.ts 负责对外导出。
从 API 报告可见,该包依赖@angular/cdk/testing中的ComponentHarness、ContentContainerComponentHarness、HarnessPredicate与BaseHarnessFilters,这意味着它完全遵循 CDK 测试生态的标准范式,可直接与TestbedHarnessEnvironment协同工作。
二、AccordionSection 枚举:理解内容的两个分区
API 报告定义了一个关键枚举AccordionSection,源码位于 accordion-harness.ts:
/** Selectors for the sections that may contain user content. */ export enum AccordionSection { TRIGGER = '[ngAccordionTrigger]', PANEL = '[ngAccordionPanel]', }它的作用是标识手风琴条目中可能承载用户内容的两个区域:
TRIGGER:触发器区域,即[ngAccordionTrigger]元素本身,用户点击它以展开/折叠。PANEL:面板区域,即[ngAccordionPanel]元素,承载被展开后显示的内容。
这个枚举正是AccordionHarness继承自ContentContainerComponentHarness<AccordionSection>的泛型参数:它告诉 CDK 在解析"内容容器"时,应从哪些区域查找嵌套的子 Harness。也正因如此,AccordionHarness可以直接调用getHarness()、getAllHarnesses()、locatorFor()等容器级查询方法,去查询面板内部的任意自定义 Harness。
三、AccordionHarness:单个条目(Trigger)的完整测试接口
3.1 定位方式与静态工厂
static hostSelector = '[ngAccordionTrigger]';AccordionHarness的宿主选择器是[ngAccordionTrigger],即每个使用ngAccordionTrigger指令的触发器元素对应一个 Harness 实例。
静态方法with()用于构建HarnessPredicate以按条件筛选:
static with(options: AccordionHarnessFilters = {}): HarnessPredicate<AccordionHarness>它内部通过HarnessPredicate注册了三个筛选维度(源码见 accordion-harness.ts):
title:用HarnessPredicate.stringMatches与getTitle()的结果做匹配,支持字符串或正则;expanded:比较isExpanded()的结果与期望值;disabled:比较isDisabled()的结果与期望值。
这些谓词会被 CDK 的加载器在getHarness/getAllHarnesses时自动应用于所有匹配的触发器元素,语义等价于"在满足选择器的元素中,再做属性级过滤"。
3.2 状态查询方法
| 方法 | 返回类型 | 底层实现(依据 accordion-harness.ts) |
|---|---|---|
isExpanded() | Promise<boolean> | 读取宿主元素的aria-expanded属性是否为字符串'true' |
isDisabled() | Promise<boolean> | 读取宿主元素的aria-disabled属性是否为字符串'true' |
getTitle() | Promise<string> | 读取宿主元素的text()文本内容 |
isFocused() | Promise<boolean> | 判断宿主元素是否处于聚焦状态 |
注意这些断言均基于ARIA 状态属性(aria-expanded/aria-disabled)而非内部组件状态,因此断言结果与真实的无障碍语义完全一致。这些属性由AccordionTrigger指令的宿主绑定负责维护(见 accordion-trigger.ts),实现了"组件如何表现,Harness 就如何断言"的解耦设计。
3.3 交互方法
| 方法 | 行为(依据 accordion-harness.ts) |
|---|---|
toggle() | 点击触发器元素,反转展开状态 |
expand() | 若当前未展开则执行toggle(),即"幂等地展开" |
collapse() | 若当前已展开则执行toggle(),即"幂等地折叠" |
focus() | 对宿主元素调用focus() |
blur() | 对宿主元素调用blur() |
其中expand()/collapse()先读取状态再做条件切换,属于"目标状态驱动"的安全封装:多次调用不会产生副作用,非常适合在beforeEach中做前置状态准备。
3.4 深度查询:getRootHarnessLoader 与面板内内容
这是本 Harness 最有价值的能力。AccordionHarness重写了受保护的getRootHarnessLoader()方法(见 accordion-harness.ts):
protected override async getRootHarnessLoader() { const panelId = await (await this.host()).getAttribute('aria-controls'); const documentRoot = await this.documentRootLocatorFactory().rootHarnessLoader(); return documentRoot.getChildLoader(`[ngAccordionPanel][id="${panelId}"]`); }其原理是:
- 读取触发器上的
aria-controls属性,获得其控制的面板 ID(该属性由 accordion-trigger.ts 中的[attr.aria-controls]绑定输出); - 从文档根 Loader 中定位
[ngAccordionPanel][id="<panelId>"]面板元素; - 将查询根设置为该面板,从而让
getHarness()等查询自动作用域到面板内容,而不是整个文档。
这一设计让测试代码可以非常自然地编写"展开条目 → 断言面板内组件"的用例,而无需手动拼接选择器。官方测试 accordion-harness.spec.ts 中专门验证了这一点:
it('should query components inside the accordion panel using ContentContainerComponentHarness', async () => { const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); const button = await accordion.getHarness(TestButtonHarness); expect(await button.getText()).toBe('Inside Content 1'); });其中TestButtonHarness是一个自定义的轻量 Harness,宿主选择器为button.test-button,仅用于验证面板内查询能力。
四、AccordionGroupHarness:组的枚举与组合
AccordionGroupHarness负责以组为单位进行查询,源码见 accordion-harness.ts:
export class AccordionGroupHarness extends ComponentHarness { static hostSelector = '[ngAccordionGroup]'; static with(options: AccordionGroupHarnessFilters = {}): HarnessPredicate<AccordionGroupHarness> { return new HarnessPredicate(AccordionGroupHarness, options); } async getAccordions(filters: AccordionHarnessFilters = {}): Promise<AccordionHarness[]> { return this.locatorForAll(AccordionHarness.with(filters))(); } }- 宿主选择器为
[ngAccordionGroup],即AccordionGroup指令所在的容器元素; with()直接透传AccordionGroupHarnessFilters构造谓词;- 核心方法
getAccordions(filters?):在组作用域内枚举所有AccordionHarness,且可透传AccordionHarnessFilters进行过滤。由于locatorForAll是惰性的,它在多次调用间会复用定位器,性能上也有保障。
测试用例验证了组合用法(accordion-harness.spec.ts):
const group = await loader.getHarness(AccordionGroupHarness); const accordions = await group.getAccordions(); expect(accordions.length).toBe(2); expect(await accordions[0].getTitle()).toBe('Section 1');由此可以清晰看到 API 报告中的三层查询模型:
TestbedHarnessEnvironment.loader(fixture) │ getHarness / getAllHarnesses ▼ AccordionGroupHarness ── getAccordions(filters?) ──► AccordionHarness[] │ │ getHarness(...) ▼ ▼ [ngAccordionGroup] [ngAccordionTrigger] ──► 面板内内容五、过滤器(Filters):精准定位的关键
5.1 AccordionHarnessFilters
定义于 accordion-harness-filters.ts,继承BaseHarnessFilters:
export interface AccordionHarnessFilters extends BaseHarnessFilters { title?: string | RegExp; expanded?: boolean; disabled?: boolean; }| 字段 | 类型 | 说明 |
|---|---|---|
title | string \| RegExp | 仅匹配标题文本等于该值(或满足该正则)的条目,支持大小写与部分匹配语义,由HarnessPredicate.stringMatches实现 |
expanded | boolean | 仅匹配展开状态等于该值的条目 |
disabled | boolean | 仅匹配禁用状态等于该值的条目 |
继承自BaseHarnessFilters的字段还包括text、selector与ancestor(CDK 标准能力),可用于进一步按祖先元素或文本过滤。
官方测试展示了三种过滤的实战效果(accordion-harness.spec.ts):
// 按标题过滤 const byTitle = await loader.getAllHarnesses(AccordionHarness.with({title: 'Section 1'})); // 按展开状态过滤 const expanded = await loader.getAllHarnesses(AccordionHarness.with({expanded: true})); // 按禁用状态过滤 const disabled = await loader.getAllHarnesses(AccordionHarness.with({disabled: true}));5.2 AccordionGroupHarnessFilters
export interface AccordionGroupHarnessFilters extends BaseHarnessFilters {}它不新增任何字段,仅保留 CDK 标准的text、selector、ancestor过滤能力,用于在存在多个手风琴组时按通用条件定位目标组。
六、完整实战:在 Angular 测试中驱动手风琴
以下示例综合自 accordion-harness.spec.ts 的真实用法,展示一套完整的测试编写流程。
6.1 测试环境与模板
import {Component} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {AccordionHarness, AccordionGroupHarness} from './accordion-harness'; import {AccordionGroup, AccordionPanel, AccordionTrigger} from '../index'; @Component({ imports: [AccordionGroup, AccordionPanel, AccordionTrigger], template: ` <div ngAccordionGroup> <div #panel1="ngAccordionPanel" ngAccordionPanel> <button class="test-button">Inside Content 1</button> </div> <button ngAccordionTrigger [panel]="panel1">Section 1</button> <div #panel2="ngAccordionPanel" ngAccordionPanel>Content 2</div> <button ngAccordionTrigger [panel]="panel2" disabled>Section 2</button> </div> `, }) class AccordionHarnessTestComponent {} beforeEach(() => { TestBed.configureTestingModule({imports: [AccordionHarnessTestComponent]}); const fixture = TestBed.createComponent(AccordionHarnessTestComponent); fixture.detectChanges(); loader = TestbedHarnessEnvironment.loader(fixture); });要点说明:
- 手风琴的三个指令(
AccordionGroup、AccordionPanel、AccordionTrigger)通过imports数组引入,这是现代 Angular 独立组件(standalone)的写法; - 触发器通过
[panel]="panel1"绑定其控制的面板模板引用,面板用#panel1="ngAccordionPanel"导出; - 面板内容放在
ngAccordionPanel元素内部即可(生产场景通常配合ngAccordionContent的ng-template做懒渲染,见 accordion-panel.ts 中的文档示例); - 加载 Harness 统一使用
TestbedHarnessEnvironment.loader(fixture)。
6.2 典型用例模式
定位并读取状态:
const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 1'})); expect(await accordion.getTitle()).toBe('Section 1'); expect(await accordion.isExpanded()).toBeFalse(); expect(await accordion.isDisabled()).toBeFalse();展开 / 折叠 / 切换:
await accordion.expand(); expect(await accordion.isExpanded()).toBeTrue(); await accordion.collapse(); expect(await accordion.isExpanded()).toBeFalse(); await accordion.toggle(); expect(await accordion.isExpanded()).toBeTrue();聚焦与失焦:
await accordion.focus(); expect(await accordion.isFocused()).toBeTrue(); await accordion.blur(); expect(await accordion.isFocused()).toBeFalse();组内过滤枚举:
const group = await loader.getHarness(AccordionGroupHarness); const all = await group.getAccordions(); // 2 个 const disabledOnly = await group.getAccordions({disabled: true}); // 1 个6.3 运行测试的构建与命令
该包在仓库中使用 Bazel 构建,测试目标定义于 src/aria/accordion/testing/BUILD.bazel:ts_project目标testing编译不含*.spec.ts的源码并依赖//:node_modules/@angular/core与//src/cdk/testing;ng_project目标unit_tests_lib收集规格文件,依赖组件本体 src/aria/accordion 及//src/cdk/testing/testbed;最终由ng_web_test_suite目标unit_tests执行。
仓库采用 Bazel 管理,运行测试的典型方式是(命令需在仓库根目录执行):
yarn bazel test //src/aria/accordion/testing:unit_tests七、深入原理:Harness 与组件实现的对应关系
7.1 ARIA 属性的双向印证
Harness 读取的aria-expanded、aria-disabled、aria-controls全部由组件指令真实输出:
- accordion-trigger.ts 的宿主绑定:
'[attr.aria-expanded]': 'expanded()'、'[attr.aria-controls]': '_pattern.controls()'、'[attr.aria-disabled]': '_pattern.disabled()',并带有role="button"与tabindex管理; - accordion-panel.ts 的宿主绑定:
'role': 'region'、'[attr.id]': 'id()'、'[attr.aria-labelledby]'以及通过inert属性在未展开时对辅助技术隐藏内容。
因此,Harness 的断言天然与无障碍语义绑定:isExpanded()读aria-expanded,而组件恰好在展开状态将其置为'true';isDisabled()读aria-disabled,与触发器的disabled输入一致。测试实际上就是在验证用户(含辅助技术用户)所感知的状态。
7.2 getTitle 的语义边界
getTitle()实现为(await this.host()).text(),即取触发器元素的文本内容。在 accordion-harness.spec.ts 的模板中,触发器<button ngAccordionTrigger [panel]="panel1">Section 1</button>的文本即"标题"。若你的标题结构更复杂(如含图标),返回的是包括图标文本在内的全部文本,需要配合正则过滤器使用。
7.3 懒渲染与内容查询的配合
面板内容使用ngAccordionContent的ng-template时会被延迟渲染(由DeferredContentAware宿主指令与afterRenderEffect联动控制,见 accordion-panel.ts),只有展开后才真正进入 DOM。这意味着"先展开、再查询面板内内容"是测试必需的顺序;而getRootHarnessLoader的惰性 Loader 设计确保查询总是在当前 DOM 上实时解析,天然适配这一时序。
八、API 报告与 Golden 文件:为什么它重要
goldens/aria/accordion/testing/index.api.md是由 API Extractor 自动生成的公开 API 快照,属于仓库的 golden(黄金)文件机制。它的价值在于:
- API 契约锁定:任何对公开 API 的破坏性变更(改名、删方法、改签名)都会在 CI 中与这份快照比对失败,从而强制开发者审视变更;
- 文档即真源:本文所述的全部公开类型、方法、过滤字段,均可在该文件中逐条核对,是测试代码兼容性的权威依据;
- 辅助审计:仓库中还配套 goldens/ts-circular-deps.json 等其它 golden 文件共同构成质量防线。
从该文件可以看出,本包公开面刻意保持精简:仅两个 Harness 类、两个过滤器接口与一个枚举,全部标注为@public,没有额外导出AccordionSection之外的实现细节,符合 CDK 一贯的"最小公开面"设计哲学。
九、小结
围绕 goldens/aria/accordion/testing/index.api.md 定义的公开 API,本文完整覆盖了@angular/aria/accordion/testing的实战用法:
- 定位:
AccordionHarness.with(filters)按标题/展开/禁用状态过滤,AccordionGroupHarness.getAccordions(filters)在组内批量枚举; - 断言:
isExpanded()、isDisabled()、getTitle()、isFocused()全部基于 ARIA 状态属性,与无障碍语义一致; - 交互:
toggle()、expand()、collapse()幂等安全,focus()/blur()覆盖键盘焦点场景; - 深度查询:通过
getRootHarnessLoader()与aria-controls的联动,自动将查询作用域限定到对应面板,可对面板内自定义组件直接断言; - 契约保障:golden API 报告 + Bazel 测试目标(
//src/aria/accordion/testing:unit_tests)共同守护该 API 的稳定性。
编写手风琴组件测试时,你可以放心依赖这套 Harness:它既是对 ARIA 语义的实现级验证,也是与 CDK 测试生态无缝衔接的官方入口。进一步阅读可参考组件实现 src/aria/accordion/accordion-group.ts、src/aria/accordion/accordion-trigger.ts、src/aria/accordion/accordion-panel.ts 以及完整测试 src/aria/accordion/testing/accordion-harness.spec.ts。
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考