Angular CDK Aria Accordion 组件测试 Harness 完全指南:AccordionHarness 与 AccordionGroupHarness 实战
2026/9/12 6:53:45 网站建设 项目流程

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/ 下的真实源码与测试用例,系统讲解AccordionHarnessAccordionGroupHarnessAccordionSection枚举及过滤器的完整用法,你将掌握如何在 Angular 测试中定位、断言并驱动手风琴的展开、折叠、聚焦等全部交互,并理解其底层如何通过ContentContainerComponentHarnessaria-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中的ComponentHarnessContentContainerComponentHarnessHarnessPredicateBaseHarnessFilters,这意味着它完全遵循 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.stringMatchesgetTitle()的结果做匹配,支持字符串或正则;
  • 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}"]`); }

其原理是:

  1. 读取触发器上的aria-controls属性,获得其控制的面板 ID(该属性由 accordion-trigger.ts 中的[attr.aria-controls]绑定输出);
  2. 从文档根 Loader 中定位[ngAccordionPanel][id="<panelId>"]面板元素;
  3. 将查询根设置为该面板,从而让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; }
字段类型说明
titlestring \| RegExp仅匹配标题文本等于该值(或满足该正则)的条目,支持大小写与部分匹配语义,由HarnessPredicate.stringMatches实现
expandedboolean仅匹配展开状态等于该值的条目
disabledboolean仅匹配禁用状态等于该值的条目

继承自BaseHarnessFilters的字段还包括textselectorancestor(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 标准的textselectorancestor过滤能力,用于在存在多个手风琴组时按通用条件定位目标组。

六、完整实战:在 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); });

要点说明:

  • 手风琴的三个指令(AccordionGroupAccordionPanelAccordionTrigger)通过imports数组引入,这是现代 Angular 独立组件(standalone)的写法;
  • 触发器通过[panel]="panel1"绑定其控制的面板模板引用,面板用#panel1="ngAccordionPanel"导出;
  • 面板内容放在ngAccordionPanel元素内部即可(生产场景通常配合ngAccordionContentng-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/testingng_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-expandedaria-disabledaria-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 懒渲染与内容查询的配合

面板内容使用ngAccordionContentng-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询