ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Angular Material Select 测试 Harness 全解:MatSelectHarness 与 SelectHarnessFilters 实战指南

Angular Material Select 测试 Harness 全解:MatSelectHarness 与 SelectHarnessFilters 实战指南 Angular Material Select 测试 Harness 全解MatSelectHarness 与 SelectHarnessFilters 实战指南【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/componentsMatSelectHarness是 Angular Material 为mat-select下拉选择组件提供的官方组件测试 Harness它让开发者无需直接操作 DOM即可在单元测试与端到端测试中稳定地查询、交互和断言 Select 的行为。本文以仓库中angular/material_select_testing的 API 报告文档 goldens/material/select/testing/index.api.md 为核心骨架结合 select-harness.ts 的实现源码与 select-harness.spec.ts 的测试用例系统讲解其全部公开 API、筛选器设计、底层实现原理与实战用法。读完本文你将掌握如何用一套稳定、类型安全的方式对单选、多选、分组下拉框进行完整的状态断言与用户交互模拟。一、什么是 Component Harness 与 Select HarnessAngular CDK 的 Component Harness 是一套面向测试的组件访问层抽象测试代码通过 Harness 提供的语义化 API 与组件交互而不是直接抓取 DOM 选择器。这样做的好处是组件内部 DOM 结构重构时只要 Harness 对外 API 不变测试就无需改动。MatSelectHarness正是mat-select的官方 Harness 实现位于 src/material/select/testing/select-harness.ts。它继承自MatFormFieldControlHarnessBaseform-field-control-harness.ts因此天然继承了表单控件通用的getLabel()能力可以按aria-labelledby、aria-label或for属性解析出控件的标签文本。其类签名与公开成员由 API Extractor 自动生成在 goldens/material/select/testing/index.api.md 中export class MatSelectHarness extends MatFormFieldControlHarnessBase { static hostSelector: string; static withT extends MatSelectHarness(options?: SelectHarnessFilters): HarnessPredicateT; blur(): Promisevoid; clickOptions(filter?: OptionHarnessFilters): Promisevoid; close(): Promisevoid; focus(): Promisevoid; getOptionGroups(filter?: OmitOptgroupHarnessFilters, ancestor): PromiseMatOptgroupHarness[]; getOptions(filter?: OmitOptionHarnessFilters, ancestor): PromiseMatOptionHarness[]; getValueText(): Promisestring; isDisabled(): Promiseboolean; isEmpty(): Promiseboolean; isFocused(): Promiseboolean; isMultiple(): Promiseboolean; isOpen(): Promiseboolean; isRequired(): Promiseboolean; isValid(): Promiseboolean; open(): Promisevoid; }注API 报告文件由工具自动生成标注 “Do not edit this file”它是验证公开 API 稳定性的黄金基线golden file任何破坏性变更都会在构建阶段被捕获。二、引入依赖与测试环境准备2.1 包与入口MatSelectHarness与SelectHarnessFilters从angular/material/select/testing导出入口定义见 src/material/select/testing/public-api.ts 与 src/material/select/testing/index.ts// src/material/select/testing/public-api.ts export * from ./select-harness; export * from ./select-harness-filters;在测试中引入import {MatSelectHarness} from angular/material/select/testing;Harness 本身不依赖 DOM 实现它通过 CDK 的TestbedHarnessEnvironmentTestBed 环境或ProtractorHarnessEnvironmentE2E 环境适配到具体运行环境。2.2 加载 Harness 的三种姿势仓库测试用例 select-harness.spec.ts 展示了标准加载方式import {HarnessLoader} from angular/cdk/testing; import {TestbedHarnessEnvironment} from angular/cdk/testing/testbed; fixture TestBed.createComponent(SelectHarnessTest); fixture.detectChanges(); loader TestbedHarnessEnvironment.loader(fixture);loader.getAllHarnesses(MatSelectHarness)获取当前页面所有匹配的 Select Harnessloader.getHarness(MatSelectHarness.with({selector: #single-selection}))按条件精确获取单个实例TestbedHarnessEnvironment.documentRootLoader()当面板渲染在全局 overlay 中时仍需通过根加载器配合ancestor过滤定位。Select 的面板是渲染在cdk-overlay全局层中的因此getOptions()等面板内查询并不是在当前组件内查找而是借助documentRootLocatorFactory()在文档根上定位详见下文源码解析。三、SelectHarnessFilters按条件筛选 Select 实例SelectHarnessFilters定义在 src/material/select/testing/select-harness-filters.tsexport interface SelectHarnessFilters extends MatFormFieldControlHarnessFilters { /** Only find instances which match the given disabled state. */ disabled?: boolean; }它继承MatFormFieldControlHarnessFilters表单控件通用筛选条件含selector、label、ancestor等并新增一个disabled条件用于按禁用状态过滤。在MatSelectHarness.with()静态方法select-harness.ts中这两个条件被注册进HarnessPredicatestatic withT extends MatSelectHarness( this: ComponentHarnessConstructorT, options: SelectHarnessFilters {}, ): HarnessPredicateT { return new HarnessPredicate(this, options) .addOption(disabled, options.disabled, async (harness, disabled) { return (await harness.isDisabled()) disabled; }) .addOption(label, options.label, (harness, label) { return HarnessPredicate.stringMatches(harness.getLabel(), label); }); }disabled布尔值精确匹配 Select 的禁用状态label字符串或正则通过HarnessPredicate.stringMatches对getLabel()的结果做匹配。测试用例中的实际用法const enabledSelects await loader.getAllHarnesses(MatSelectHarness.with({disabled: false})); const disabledSelects await loader.getAllHarnesses(MatSelectHarness.with({disabled: true})); const selects await loader.getAllHarnesses(MatSelectHarness.with({label: US States}));在 select-harness.spec.ts 中当测试组件将某个 Select 的禁用状态通过signal切换后命中数量会从 4/0 变为 3/1验证了该筛选条件的实时性。四、状态查询 APIisXxx 系列全解析MatSelectHarness提供了一组语义化的布尔状态查询全部基于宿主元素上的 CSS 类实现见 select-harness.ts核心逻辑如下private _prefix mat-mdc; async isDisabled(): Promiseboolean { return (await this.host()).hasClass(${this._prefix}-select-disabled); } async isValid(): Promiseboolean { return !(await (await this.host()).hasClass(ng-invalid)); } async isRequired(): Promiseboolean { return (await this.host()).hasClass(${this._prefix}-select-required); } async isEmpty(): Promiseboolean { return (await this.host()).hasClass(${this._prefix}-select-empty); } async isMultiple(): Promiseboolean { return (await this.host()).hasClass(${this._prefix}-select-multiple); }方法返回底层判定依据典型断言场景isDisabled()boolean宿主是否有mat-mdc-select-disabled类断言受控禁用状态isValid()boolean宿主是否没有ng-invalid类配合 Angular Forms 校验断言必填校验失败isRequired()boolean宿主是否有mat-mdc-select-required类断言必填标记isEmpty()boolean宿主是否有mat-mdc-select-empty类断言未选择任何值isMultiple()boolean宿主是否有mat-mdc-select-multiple类区分单选/多选模式isFocused()boolean宿主元素的聚焦状态断言焦点管理isOpen()boolean文档根中是否存在该 Select 对应的#id-panel面板断言面板开合isOpen()的实现值得注意它不是看宿主元素而是判断 overlay 中的面板是否存在select-harness.tsasync isOpen(): Promiseboolean { return !!(await this._documentRootLocator.locatorForOptional(await this._getPanelSelector())()); }对应测试select-harness.spec.ts验证了单选与多选两种模式下 open/close 前后isOpen()的翻转。五、用户交互 API打开、关闭、聚焦与选择5.1 open() 与 close()open()在面板未打开时找到宿主内的触发器元素.mat-mdc-select-trigger并模拟点击select-harness.tsasync open(): Promisevoid { if (!(await this.isOpen())) { const trigger await this.locatorFor(.${this._prefix}-select-trigger)(); return trigger.click(); } }close()的实现则通过点击 overlay 背景cdk-overlay-backdrop来关闭面板select-harness.ts源码注释说明这是单选/多选两种模式下都最一致的关闭方式备选方案是向宿主派发 ESCAPE 键事件async close(): Promisevoid { if (await this.isOpen()) { return (await this._backdrop()).click(); } }5.2 focus() 与 blur()async focus(): Promisevoid { return (await this.host()).focus(); } async blur(): Promisevoid { return (await this.host()).blur(); }对应测试先断言isFocused()为 falsefocus()后为 trueblur()后恢复 falseselect-harness.spec.ts。5.3 clickOptions()一键完成“打开面板 点击选项”clickOptions(filter?: OptionHarnessFilters)是高频便捷方法内部顺序执行打开面板 → 并行读取“是否多选”与“匹配选项列表” → 多选时点击全部匹配项单选时点击第一个匹配项select-harness.tsasync clickOptions(filter?: OptionHarnessFilters): Promisevoid { await this.open(); const [isMultiple, options] await parallel(() [ this.isMultiple(), this.getOptions(filter), ]); if (options.length 0) { throw Error(Select does not have options matching the specified filter); } if (isMultiple) { await parallel(() options.map(option option.click())); } else { await options[0].click(); } }注意两点没有匹配项时会抛出Error(Select does not have options matching the specified filter)便于快速定位筛选条件写错的问题多处使用 CDK 的parallel()并发执行异步操作避免串行等待拖慢测试。六、内容读取 API选项、分组与值文本6.1 getOptions() 与 getOptionGroups()Select 的面板渲染在 overlay 中所以这两个方法通过_documentRootLocator在文档根中定位选项/分组并把ancestor强制指定为当前 Select 的面板选择器从而只返回本 Select 面板内的选项select-harness.tsasync getOptions(filter?: OmitOptionHarnessFilters, ancestor): PromiseMatOptionHarness[] { return this._documentRootLocator.locatorForAll( this._optionClass.with({ ...(filter || {}), ancestor: await this._getPanelSelector(), } as OptionHarnessFilters), )(); }面板选择器由宿主的id推导而来select-harness.tsprivate async _getPanelSelector(): Promisestring { const id await (await this.host()).getAttribute(id); return #${id}-panel; }因此使用with({selector: #xxx})给 Select 设置稳定的id是测试可定位的前提。这一设计也让测试能够在“多个 Select 同时打开”的场景下精确取回各自面板中的选项——select-harness.spec.ts 专门验证了这一点同时打开#single-selection与#grouped两个面板后仍能分别正确取回 11 个与 14 个选项。返回的MatOptionHarness来自angular/material/core/testingoption-harness.ts它提供了方法说明click()点击该选项getText()获取选项主文本.mdc-list-item__primary-textisDisabled()是否禁用mdc-list-item--disabled类isSelected()是否选中mdc-list-item--selected类isActive()是否为激活项mat-mdc-option-active类isMultiple()是否处于多选模式MatOptionHarness.with()支持按text字符串或正则与isSelected过滤选项。分组则返回MatOptgroupHarnessgetOptionGroups()的签名显式忽略了调用方可能传入的ancestor条件OmitOptgroupHarnessFilters, ancestor保证分组查询始终限定在本面板内。6.2 getValueText()读取当前选中值文本async getValueText(): Promisestring { const value await this.locatorFor(.${this._prefix}-select-value)(); return value.text(); }它读取宿主内.mat-mdc-select-value元素的文本。测试验证单选点击第 4 个选项后getValueText()返回Kansas多选依次点击两个选项后返回Kansas, New York多选用逗号连接select-harness.spec.ts。七、完整实战示例单选 必填校验 状态断言综合仓库测试模板select-harness.spec.ts一个覆盖完整流程的测试如下import {Component, signal} from angular/core; import {ComponentFixture, TestBed} from angular/core/testing; import {FormControl, ReactiveFormsModule, Validators} from angular/forms; import {HarnessLoader} from angular/cdk/testing; import {TestbedHarnessEnvironment} from angular/cdk/testing/testbed; import {MatFormFieldModule} from angular/material/form-field; import {MatSelectModule} from angular/material/select; import {MatSelectHarness} from angular/material/select/testing; Component({ template: mat-form-field mat-labelUS States/mat-label mat-select [disabled]isDisabled() [required]isRequired() idstate-select for (state of states; track state) { mat-option [value]state.code{{ state.name }}/mat-option } /mat-select /mat-form-field , imports: [MatSelectModule, MatFormFieldModule, ReactiveFormsModule], }) class SelectHarnessTest { isDisabled signal(false); isRequired signal(false); states [ {code: AL, name: Alabama}, {code: CA, name: California}, {code: KS, name: Kansas}, {code: NY, name: New York}, ]; } describe(state-select, () { let fixture: ComponentFixtureSelectHarnessTest; let loader: HarnessLoader; beforeEach(() { fixture TestBed.createComponent(SelectHarnessTest); fixture.detectChanges(); loader TestbedHarnessEnvironment.loader(fixture); }); it(支持完整的状态断言与交互, async () { const select await loader.getHarness( MatSelectHarness.with({selector: #state-select}), ); // 初始状态 expect(await select.isDisabled()).toBe(false); expect(await select.isRequired()).toBe(false); expect(await select.isEmpty()).toBe(true); expect(await select.isOpen()).toBe(false); expect(await select.getValueText()).toBe(); // 打开面板并断言选项 await select.open(); expect(await select.isOpen()).toBe(true); const options await select.getOptions(); expect(options.length).toBe(4); expect(await options[2].getText()).toBe(Kansas); // 选中一项后关闭 await options[2].click(); expect(await select.getValueText()).toBe(Kansas); expect(await select.isEmpty()).toBe(false); await select.close(); expect(await select.isOpen()).toBe(false); }); it(disabled 筛选条件生效, async () { expect((await loader.getAllHarnesses(MatSelectHarness.with({disabled: false}))).length).toBe(1); fixture.componentInstance.isDisabled.set(true); fixture.detectChanges(); expect((await loader.getAllHarnesses(MatSelectHarness.with({disabled: true}))).length).toBe(1); }); });八、底层原理前缀、宿主选择器与 overlay 定位MatSelectHarness的实现有四个关键设计select-harness.tsstatic hostSelector .mat-mdc-select; private _prefix mat-mdc; private _optionClass MatOptionHarness; private _optionGroupClass MatOptgroupHarness; private _documentRootLocator this.documentRootLocatorFactory(); private _backdrop this._documentRootLocator.locatorFor(.cdk-overlay-backdrop);宿主选择器hostSelector .mat-mdc-select定义了 Harness 匹配的根元素这也是 CDK 测试框架定位 Harness 的入口类名前缀统一所有状态类mat-mdc-select-disabled/required/empty/multiple都通过_prefix拼接避免了魔法字符串散落可替换的选项类_optionClass/_optionGroupClass保存选项与分组 Harness 类便于在子类中替换overlay 全局定位_documentRootLocator用于在文档根查找面板与背景层这正是面板渲染在 CDK Overlay 容器中的结果——这也是为什么isOpen()、getOptions()等都必须经过文档根而不是当前 fixture 内部。这一分层也解释了为什么getOptions的过滤参数被限定为OmitOptionHarnessFilters, ancestorancestor由 Harness 内部根据面板选择器强制注入调用方无需也不应干预。九、如何运行与验证构建与测试入口Select Harness 的单元测试通过 Bazel 组织见 src/material/select/testing/BUILD.bazelts_project(name testing)构建 Harness 库本身排除*.spec.ts依赖//src/cdk/testing、//src/material/core/testing、//src/material/form-field/testing/controlng_project(name unit_tests_lib)测试代码额外依赖angular/core、angular/forms、//src/cdk/overlay、//src/cdk/testing/testbed、//src/material/select等ng_web_test_suite(name unit_tests)在浏览器中运行测试套件。运行测试的命令仓库采用 Bazel 构建体系详见 docs/README.mdyarn bazel test //src/material/select/testing:unit_tests测试环境还有一个值得注意的细节select-harness.spec.ts测试中通过MATERIAL_ANIMATIONS令牌传入{animationsDisabled: true}关闭动画并在afterEach中手动调用overlayContainer.ngOnDestroy()清理 Overlay 容器避免测试之间相互泄漏——在编写自己的 Select 测试时同样建议关闭动画以消除时序不确定性。十、常见问题与最佳实践小结务必为 Select 设置稳定id_getPanelSelector()依赖宿主的id推导面板选择器#id-panel没有稳定 id 将无法可靠定位面板与选项面板内容必须在文档根查询不要试图在组件 fixture 内直接找.mat-mdc-option请始终通过getOptions()/getOptionGroups()多选场景用clickOptions它会点击所有匹配项而单选只点第一个若需精确控制请先open()再逐个操作MatOptionHarness状态类断言是“实现事实”而非“语义保证”isDisabled()等基于mat-mdc-select-*类实现若未来 DOM 类名变更Harness 内部会同步更新测试代码无需跟随改动——这正是使用 Harness 的价值isValid()依赖 Angular Forms 的ng-invalid类仅在使用formControl/ngModel且校验失败时返回 false测试中#with-form-control绑定了Validators.required空值控制select-harness.spec.ts。总而言之MatSelectHarness把 Select 的 DOM 细节overlay 面板、trigger 元素、状态类完整封装在 select-harness.ts 内部对外只暴露语义化、类型安全、环境无关的 API。结合 goldens/material/select/testing/index.api.md 这份 API 基线你可以在测试中放心使用全部公开方法并在组件升级时获得 API 变更的早期预警。理解它的实现原理后你也能轻松举一反三为仓库中其他表单控件如 src/material/select、src/material/input 下的 Harness编写同样高质量的测试代码。【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表