Angular Material MatSort 测试 Harness 完全指南:MatSortHarness 与 MatSortHeaderHarness 的 API 与实战
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
本指南基于当前仓库中 goldens/material/sort/testing/index.api.md 这份 API Extractor 生成的公开 API 报告,系统讲解 Angular MaterialmatSort/mat-sort-header组件测试工具(Harness)的完整公共接口、底层实现与实战用法。读完本文,你将掌握如何通过MatSortHarness与MatSortHeaderHarness在组件测试中稳定地定位排序列、读取/触发排序状态、断言禁用与激活行为,并理解这些能力背后依赖的 CDK Component Harness 架构与aria-sort语义。
一、为什么需要 Sort 测试 Harness
matSort指令与mat-sort-header组件用于为表格数据添加排序状态与可视化反馈(详见 sort.md)。在编写单元测试时,如果直接通过 DOM 查询去定位排序表头,代码会高度依赖组件内部模板结构与 CSS 类名,一旦内部实现调整(例如按钮包裹结构、图标投影方式变化),测试就会大面积失效。
Component Harness 是@angular/cdk/testing提供的测试抽象层:它把"如何找到元素、如何读取状态、如何触发交互"封装为稳定的公共 API,测试代码只与 Harness 的公开方法打交道。MatSortHarness与MatSortHeaderHarness正是 Angular Material 为排序功能提供的这一层测试接口,测试通过HarnessPredicate做精确过滤,从而获得与实现解耦、可跨测试环境(TestBed、Protractor、WebDriver 等)复用的能力。
二、公共 API 全景:从 Golden 报告出发
goldens/material/sort/testing/index.api.md记录了@angular/material_sort_testing包的全部公开导出,包括两个 Harness 类、两个过滤器接口,以及它们依赖的@angular/cdk/testing基础类型。
2.1 MatSortHarness
import { BaseHarnessFilters } from '@angular/cdk/testing'; import { ComponentHarness } from '@angular/cdk/testing'; import { HarnessPredicate } from '@angular/cdk/testing'; // @public export class MatSortHarness extends ComponentHarness { getActiveHeader(): Promise<MatSortHeaderHarness | null>; getSortHeaders(filter?: SortHeaderHarnessFilters): Promise<MatSortHeaderHarness[]>; static hostSelector: string; static with(options?: SortHarnessFilters): HarnessPredicate<MatSortHarness>; }MatSortHarness代表整个matSort容器(即带有matSort指令的父元素),职责是:
- 定位排序容器:通过静态属性
hostSelector(实现值为.mat-sort,见 sort-harness.ts)找到宿主元素; - 获取全部表头:
getSortHeaders(filter?)返回容器内所有(或符合过滤条件的)MatSortHeaderHarness实例; - 获取当前激活排序列:
getActiveHeader()遍历所有表头,返回第一个isActive()为真的表头,没有任何列处于排序激活状态时返回null; - 构造过滤器:静态方法
with(options?)返回HarnessPredicate<MatSortHarness>,用于在测试中按条件筛选排序容器实例。
2.2 MatSortHeaderHarness
// @public export class MatSortHeaderHarness extends ComponentHarness { click(): Promise<void>; getLabel(): Promise<string>; getSortDirection(): Promise<SortDirection>; static hostSelector: string; isActive(): Promise<boolean>; isDisabled(): Promise<boolean>; static with(options?: SortHeaderHarnessFilters): HarnessPredicate<MatSortHeaderHarness>; }MatSortHeaderHarness代表单个mat-sort-header,提供对单个排序列的完整状态读取与交互能力:
| 方法 | 返回值 | 语义 |
|---|---|---|
click() | Promise<void> | 点击表头以切换排序方向(仅在启用状态下有效,见下方实现说明) |
getLabel() | Promise<string> | 读取表头的文本标签(即列标题文本) |
getSortDirection() | Promise<SortDirection> | 读取当前排序方向,取值为'asc'、'desc'或''(未排序) |
isActive() | Promise<boolean> | 当前表头是否处于排序激活状态 |
isDisabled() | Promise<boolean> | 当前表头是否被禁用 |
static with(options?) | HarnessPredicate<...> | 按标签文本或排序方向构造过滤条件 |
static hostSelector | string | 宿主选择器,实现值为.mat-sort-header |
SortDirection类型来自 sort.ts(经 sort-header-harness.ts 引入),与业务侧matSortChange事件中携带的方向保持一致。
2.3 过滤器接口
// @public (undocumented) export interface SortHarnessFilters extends BaseHarnessFilters { } // @public (undocumented) export interface SortHeaderHarnessFilters extends BaseHarnessFilters { label?: string | RegExp; sortDirection?: SortDirection; }SortHarnessFilters继承 CDK 的BaseHarnessFilters,本身未新增字段,用于未来扩展,也用于限定MatSortHarness.with()的筛选维度;SortHeaderHarnessFilters在基础过滤之上新增两个维度:label:按表头文本过滤,支持精确字符串与正则表达式;sortDirection:按排序方向过滤,'asc'/'desc'/''。
两个接口的定义位于 sort-harness-filters.ts,完整导出链为 public-api.ts → index.ts。
三、源码级实现剖析
3.1 hostSelector:Harness 与 DOM 的契约
Harness 通过静态hostSelector完成从"逻辑查找"到"DOM 查找"的映射:
// src/material/sort/testing/sort-harness.ts export class MatSortHarness extends ComponentHarness { static hostSelector = '.mat-sort'; ... } // src/material/sort/testing/sort-header-harness.ts export class MatSortHeaderHarness extends ComponentHarness { static hostSelector = '.mat-sort-header'; ... }这意味着宿主的matSort指令与mat-sort-header组件在渲染时会分别应用.mat-sort与.mat-sort-header类,Harness 才能据此定位。从 sort-header.ts 可以看到宿主绑定中还维护了[attr.aria-sort]与[class.mat-sort-header-disabled],这两者恰好是下文两个核心状态读取方法的直接数据来源。
3.2 getSortDirection():以 aria-sort 为准的状态读取
MatSortHeaderHarness.getSortDirection()的底层实现是读取宿主元素的aria-sort属性并做语义映射(sort-header-harness.ts):
async getSortDirection(): Promise<SortDirection> { const host = await this.host(); const ariaSort = await host.getAttribute('aria-sort'); if (ariaSort === 'ascending') { return 'asc'; } else if (ariaSort === 'descending') { return 'desc'; } return ''; }也就是说,Harness 的排序状态完全依赖无障碍语义属性aria-sort(取值'ascending'/'descending',未排序时组件不输出该属性或输出'none')。这既是实现事实,也暗示了一个约束:测试中断言排序方向时,实际验证的是组件是否正确维护了aria-sort语义。这一点在业务组件自身的单元测试 sort.spec.ts 中同样得到验证——点击表头后aria-sort会在'none'→'ascending'→'descending'→'none'之间轮转。
3.3 isActive() 与 isDisabled() 的实现
async isActive(): Promise<boolean> { return !!(await this.getSortDirection()); } async isDisabled(): Promise<boolean> { return (await this.host()).hasClass('mat-sort-header-disabled'); }isActive()是getSortDirection()的非空判断:只要存在排序方向('asc'或'desc')即视为激活,未排序返回false;isDisabled()检查宿主是否带有.mat-sort-header-disabled类,与 sort-header.scss 中定义的禁用样式类一致,绑定逻辑见 sort-header.ts。
3.4 getLabel():读取列标题文本
private _container = this.locatorFor('.mat-sort-header-container'); async getLabel(): Promise<string> { return (await this._container()).text(); }标签文本取自表头内部.mat-sort-header-container容器的文本内容。在mat-table场景下,由于默认使用列 id 作为表头 id(见 sort.md 中 "Using sort with the mat-table" 一节),getLabel()返回的文本通常即为列标题。
3.5 click():触发排序交互
async click(): Promise<void> { return (await this.host()).click(); }click()直接点击宿主元素。注意测试用例注释明确说明"仅在表头启用时有效"——当表头通过[disabled]或容器级matSortDisabled被禁用时,mat-sort-header内部会忽略排序切换(对应 sort.md 的 "Disabling sorting" 一节),因此测试中应先通过isDisabled()判断可点击性。
3.6 getActiveHeader():遍历查找激活列
async getActiveHeader(): Promise<MatSortHeaderHarness | null> { const headers = await this.getSortHeaders(); for (let i = 0; i < headers.length; i++) { if (await headers[i].isActive()) { return headers[i]; } } return null; }该方法线性遍历所有表头并返回第一个激活项;由于排序语义上同一时刻只有一个激活列,该实现是正确且高效的。未排序时返回null。
四、实战:在 TestBed 中驱动排序测试
当前仓库自带完整可运行的单测示例 sort-harness.spec.ts,其测试组件包含一个五列可排序的matSort表格(Dessert / Calories / Fat / Carbs / Protein),第三列 Fat 可通过 signal 动态禁用。以下结合该文件梳理标准用法。
4.1 初始化 HarnessLoader
import {HarnessLoader, parallel} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {MatSortModule, Sort} from '../../sort'; import {MatSortHarness} from './sort-harness'; fixture = TestBed.createComponent(SortHarnessTest); fixture.detectChanges(); loader = TestbedHarnessEnvironment.loader(fixture);通过TestbedHarnessEnvironment.loader(fixture)创建HarnessLoader,之后所有 Harness 查找都经由该 loader 完成。
4.2 加载容器与表头
const sort = await loader.getHarness(MatSortHarness); // 找到唯一的 matSort 容器 const headers = await sort.getSortHeaders(); // 获取全部 5 个表头当页面存在多个排序表格时,可用loader.getAllHarnesses(MatSortHarness)获取全部容器,或通过MatSortHarness.with()组合BaseHarnessFilters(如ancestor、selector等)精确锁定目标。
4.3 用过滤器做定向断言
按标签文本过滤(精确字符串):
const headers = await sort.getSortHeaders({label: 'Carbs'}); expect(headers.length).toBe(1); expect(await headers[0].getLabel()).toBe('Carbs');按正则过滤:
const headers = await sort.getSortHeaders({label: /^C/}); // 匹配 Calories 与 Carbs按排序方向过滤:初始全部未排序时{sortDirection: ''}命中 5 个;点击第一个表头后再查{sortDirection: 'asc'}仅命中 1 个:
let headers = await sort.getSortHeaders({sortDirection: ''}); expect(headers.length).toBe(5); await headers[0].click(); headers = await sort.getSortHeaders({sortDirection: 'asc'}); expect(headers.length).toBe(1);label与sortDirection两个过滤条件在MatSortHeaderHarness.with()中通过HarnessPredicate.stringMatches与getLabel()/getSortDirection()联动实现(sort-header-harness.ts),因此过滤器断言与直接方法断言基于同一套数据源,结果天然一致。
4.4 状态读取与交互断言
禁用状态(动态切换后重新检测变更):
const thirdHeader = (await sort.getSortHeaders())[2]; expect(await thirdHeader.isDisabled()).toBe(false); fixture.componentInstance.disableThirdHeader.set(true); fixture.detectChanges(); expect(await thirdHeader.isDisabled()).toBe(true);激活状态与方向轮转:
expect(await secondHeader.isActive()).toBe(false); await secondHeader.click(); expect(await secondHeader.isActive()).toBe(true); expect(await secondHeader.getSortDirection()).toBe('asc'); await secondHeader.click(); expect(await secondHeader.getSortDirection()).toBe('desc');激活列定位:
expect(await sort.getActiveHeader()).toBeNull(); // 初始无排序 await fifthHeader.click(); const activeHeader = await sort.getActiveHeader(); expect(activeHeader).toBeTruthy(); expect(await activeHeader!.getLabel()).toBe('Protein');批量读取可用parallel(() => headers.map(h => h.getLabel()))并行执行异步调用,避免串行等待(这是 CDK Testing 推荐的性能模式,测试文件 sort-harness.spec.ts 中即有应用)。
4.5 完整测试组件模板
<table matSort (matSortChange)="sortData($event)"> <tr> <th mat-sort-header="name">Dessert</th> <th mat-sort-header="calories">Calories</th> <th mat-sort-header="fat" [disabled]="disableThirdHeader()">Fat</th> <th mat-sort-header="carbs">Carbs</th> <th mat-sort-header="protein">Protein</th> </tr> @for (dessert of sortedData; track dessert) { <tr> <td>{{dessert.name}}</td> <td>{{dessert.calories}}</td> <td>{{dessert.fat}}</td> <td>{{dessert.carbs}}</td> <td>{{dessert.protein}}</td> </tr> } </table>mat-sort-header的id(如"name"、"calories")用于标识排序列,matSortChange事件携带{active, direction}供业务侧排序数据(测试中通过sortData(sort: Sort)实现升/降序排列)。在mat-table中若不给表头显式指定 id,则默认使用列 id(见 sort.md)。
五、构建与依赖关系
MatSortHarness/MatSortHeaderHarness的构建配置见 testing/BUILD.bazel:testing目标编译全部非 spec 的 TS 文件,其依赖仅为//src/cdk/testing(提供ComponentHarness、HarnessPredicate、BaseHarnessFilters)与//src/material/sort(提供SortDirection及排序组件本体);unit_tests_lib额外依赖@angular/core、@angular/platform-browser与//src/cdk/testing/testbed(提供TestbedHarnessEnvironment)。从依赖结构可以看出,Sort 测试 Harness 是纯 CDK 抽象层的薄封装——不依赖任何具体测试运行器,这也是它可以跨 TestBed / Protractor / WebDriver 环境复用的根本原因。
六、使用建议与注意事项
- 优先通过 Harness 断言,而非直接查询 DOM:
getSortDirection()等方法的输出绑定aria-sort语义,天然与 sort.md 中强调的无障碍契约一致,测试即文档; - 区分"禁用"与"未激活":
isDisabled()反映用户能否操作,isActive()反映是否正在排序,两者独立;需要验证点击无效时,应先用isDisabled()断言前置条件; - 掌握方向轮转规律:默认排序起点为
asc,点击后依次asc→desc→ 取消排序;可通过matSortStart="desc"全局反转、单表头start="desc"局部反转,或用matSortDisableClear/disableClear阻止取消排序(sort.md "Changing the sort order" 一节)——Harness 断言时应与这些业务配置保持一致; - 过滤器优先使用
label定位:列标题在真实表格中具有唯一性,用{label: 'Carbs'}或正则/^C/比按数组下标取元素更稳健,也能避免因列顺序调整导致的脆弱断言; SortDirection类型三态:'asc'/'desc'/''(空字符串表示未排序),编写过滤器与断言时不要遗漏空态分支。
七、小结
MatSortHarness与MatSortHeaderHarness构成了 Angular Material 排序功能的官方测试入口:前者负责容器级定位与激活列查询,后者负责单列的标签读取、方向读取、禁用/激活判断与点击交互,两者通过SortHarnessFilters/SortHeaderHarnessFilters实现精确过滤。其实现以aria-sort属性与.mat-sort-header-disabled类为数据源,与组件运行时状态严格同源,保证了测试断言与真实渲染行为的一致性。结合仓库中的 API 报告、Harness 源码、过滤器定义 与 完整测试套件,你可以直接参照本节示例为任何使用matSort的表格编写稳健、可维护的排序行为测试。
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考