news 2026/9/13 1:14:11

Angular Material MatSort 测试 Harness 完全指南:MatSortHarness 与 MatSortHeaderHarness 的 API 与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Angular Material MatSort 测试 Harness 完全指南:MatSortHarness 与 MatSortHeaderHarness 的 API 与实战

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)的完整公共接口、底层实现与实战用法。读完本文,你将掌握如何通过MatSortHarnessMatSortHeaderHarness在组件测试中稳定地定位排序列、读取/触发排序状态、断言禁用与激活行为,并理解这些能力背后依赖的 CDK Component Harness 架构与aria-sort语义。

一、为什么需要 Sort 测试 Harness

matSort指令与mat-sort-header组件用于为表格数据添加排序状态与可视化反馈(详见 sort.md)。在编写单元测试时,如果直接通过 DOM 查询去定位排序表头,代码会高度依赖组件内部模板结构与 CSS 类名,一旦内部实现调整(例如按钮包裹结构、图标投影方式变化),测试就会大面积失效。

Component Harness 是@angular/cdk/testing提供的测试抽象层:它把"如何找到元素、如何读取状态、如何触发交互"封装为稳定的公共 API,测试代码只与 Harness 的公开方法打交道。MatSortHarnessMatSortHeaderHarness正是 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 hostSelectorstring宿主选择器,实现值为.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(如ancestorselector等)精确锁定目标。

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);

labelsortDirection两个过滤条件在MatSortHeaderHarness.with()中通过HarnessPredicate.stringMatchesgetLabel()/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-headerid(如"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(提供ComponentHarnessHarnessPredicateBaseHarnessFilters)与//src/material/sort(提供SortDirection及排序组件本体);unit_tests_lib额外依赖@angular/core@angular/platform-browser//src/cdk/testing/testbed(提供TestbedHarnessEnvironment)。从依赖结构可以看出,Sort 测试 Harness 是纯 CDK 抽象层的薄封装——不依赖任何具体测试运行器,这也是它可以跨 TestBed / Protractor / WebDriver 环境复用的根本原因。

六、使用建议与注意事项

  1. 优先通过 Harness 断言,而非直接查询 DOMgetSortDirection()等方法的输出绑定aria-sort语义,天然与 sort.md 中强调的无障碍契约一致,测试即文档;
  2. 区分"禁用"与"未激活"isDisabled()反映用户能否操作,isActive()反映是否正在排序,两者独立;需要验证点击无效时,应先用isDisabled()断言前置条件;
  3. 掌握方向轮转规律:默认排序起点为asc,点击后依次ascdesc→ 取消排序;可通过matSortStart="desc"全局反转、单表头start="desc"局部反转,或用matSortDisableClear/disableClear阻止取消排序(sort.md "Changing the sort order" 一节)——Harness 断言时应与这些业务配置保持一致;
  4. 过滤器优先使用label定位:列标题在真实表格中具有唯一性,用{label: 'Carbs'}或正则/^C/比按数组下标取元素更稳健,也能避免因列顺序调整导致的脆弱断言;
  5. SortDirection类型三态'asc'/'desc'/''(空字符串表示未排序),编写过滤器与断言时不要遗漏空态分支。

七、小结

MatSortHarnessMatSortHeaderHarness构成了 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),仅供参考

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

ESP32-S3 N16R8开发实战:环境搭建与项目组织全指南

ESP32-S3这颗芯片最近热度确实高&#xff0c;尤其是N16R8这个配置版本&#xff0c;在AI图像、离线语音、音频处理这些场景里几乎成了首选。我手里这块N16R8开发板已经用了快半年&#xff0c;从最初搭环境到跑通完整项目&#xff0c;中间踩了不少坑&#xff0c;也积累了一些实际…

作者头像 李华
网站建设 2026/9/13 1:12:02

基于群智能优化算法的光伏组件参数辨识:GWO、DBO与DOA对比实践

先说结论&#xff1a;用群智能优化算法做光伏组件参数辨识&#xff0c;这件事的本质就是在一个高维、非线性、多峰值的参数空间里找全局最优解。你手里拿到的I-V曲线数据是“果”&#xff0c;而单二极管/双二极管模型里的那些参数&#xff08;光生电流、串联电阻、并联电阻、二…

作者头像 李华
网站建设 2026/9/13 1:10:29

红黑树C++实现全解析:旋转、插入删除修复与调试验证

先聊个很多人都在经历的尴尬瞬间&#xff1a;红黑树的五个性质背得滚瓜烂熟&#xff0c;面试前能默写&#xff0c;可一到真要自己用C实现一棵能跑的、插入删除都不崩的红黑树&#xff0c;就变成大型翻车现场。这个问题我太有体会了&#xff0c;前前后后写了三版&#xff0c;每一…

作者头像 李华
网站建设 2026/9/13 1:09:05

光模块测试为何必须用超低噪声程控电源?

1. 为什么光模块测试绕不开AT66333A这台电源&#xff1f;做光模块测试的工程师&#xff0c;尤其是刚接手高速SFP/QSFP28/OSFP模块产线验证的朋友&#xff0c;大概率都踩过这个坑&#xff1a;用普通直流电源给模块上电&#xff0c;一通电&#xff0c;眼图就抖、误码率飙升、DDM数…

作者头像 李华