尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Angular Material Timepicker 测试 Harness 完整指南:从 API Golden 报告到源码级实践

Angular Material Timepicker 测试 Harness 完整指南:从 API Golden 报告到源码级实践 Angular Material Timepicker 测试 Harness 完整指南从 API Golden 报告到源码级实践【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components本指南以 Angular Material 仓库中angular/material_timepicker_testing包的 API 报告goldens/material/timepicker/testing/index.api.md为核心系统讲解MatTimepickerHarness、MatTimepickerInputHarness、MatTimepickerToggleHarness三个测试 Harness 的公开 API、过滤条件与底层实现。读完本文你将掌握如何在基于 Testbed 的组件测试中通过 Harness 稳定地打开时间选择器、读取/设置输入值、断言面板状态与选项内容并理解这些 API 在 src/material/timepicker/testing 目录中的真实源码行为。一、认识这份文档API Golden 报告是什么goldens/material/timepicker/testing/index.api.md是由微软 API Extractor 工具自动生成的 API 报告文件文件头部明确标注“Do not edit this file”。它的作用是锁定包对外公开的 API 形态angular/material_timepicker_testing包只能导出报告中列出的这些类、接口与方法任何对公开 API 的增删改都会破坏 golden 校验从而在 CI 中被检测出来仓库根目录的 goldens/BUILD.bazel 负责相关 golden 测试目标作为 API 消费者开发者的权威速查表报告里每一个public条目都代表该包对外承诺的稳定接口。对应地包的真实源码入口是 src/material/timepicker/testing/public-api.ts它导出了四个文件export * from ./timepicker-harness; export * from ./timepicker-harness-filters; export * from ./timepicker-input-harness; export * from ./timepicker-toggle-harness;即三个 Harness 类加一个过滤器定义文件与 golden 报告完全一一对应。此外还有包级入口 src/material/timepicker/testing/index.ts 转发public-api并配套构建目标 src/material/timepicker/testing/BUILD.bazel。为什么需要测试 Harness组件测试如果直接操作 DOM如querySelector一旦组件内部模板结构或 CSS 类名发生变化测试就会大面积失效。Harness 将“组件对外暴露的交互语义”与“内部 DOM 实现”解耦——Harness 的hostSelector宿主选择器和方法内部的选择逻辑是唯一需要跟随组件实现演进的地方而测试代码只依赖 Harness 的公开方法。Angular CDK 的 angular/cdk/testing 提供了ComponentHarness、HarnessPredicate、TestbedHarnessEnvironment等基础设施三者共同构成这套测试框架的基石。二、MatTimepickerHarness时间选择器面板的测试入口2.1 类签名与宿主选择器export class MatTimepickerHarness extends ComponentHarness { static hostSelector: string; // 实际值为 mat-timepicker static withT extends MatTimepickerHarness( this: ComponentHarnessConstructorT, options?: TimepickerHarnessFilters, ): HarnessPredicateT; isOpen(): Promiseboolean; getOptions(filters?: OmitOptionHarnessFilters, ancestor): PromiseMatOptionHarness[]; selectOption(filters: OptionHarnessFilters): Promisevoid; protected _getPanelSelector(): Promisestring; }从源码 src/material/timepicker/testing/timepicker-harness.ts 可以看到其hostSelector为mat-timepicker即匹配mat-timepicker元素。与MatDatepickerHarness等同类 Harness 一致with()静态方法通过new HarnessPredicate(this, options)构造一个按TimepickerHarnessFilters过滤的谓词。2.2 关键方法的行为细节isOpen()判断面板是否打开。实现上通过_getPanelSelector()得到面板选择器再用documentRootLocatorFactory()文档根定位器查找对应面板是否存在。由于时间选择器的下拉面板渲染在 Overlay 中、位于组件 DOM 树之外因此必须从 document root 而不是从 host 内部查找async isOpen(): Promiseboolean { const selector await this._getPanelSelector(); const panel await this._documentRootLocator.locatorForOptional(selector)(); return panel ! null; }_getPanelSelector()面板通过mat-timepicker-panel-id属性关联到具体的时间选择器实例选择器形如#panel-idprotected async _getPanelSelector(): Promisestring { return #${await (await this.host()).getAttribute(mat-timepicker-panel-id)}; }这解释了输入框 Harness 中getTimepicker()为何要读取mat-timepicker-id属性来反向定位详见第三节。getOptions(filters?)读取面板内的全部选项返回MatOptionHarness[]。它复用了 Material 核心测试包 src/material/core/testing 中导出的MatOptionHarness菜单、下拉、自动完成等组件共用同一套 option Harness。注意两点面板关闭时调用会直接抛错Unable to retrieve options for timepicker. Timepicker panel is closed.过滤参数类型为OmitOptionHarnessFilters, ancestorancestor由 Harness 内部用面板选择器强制填充避免调用者把选项范围限定到错误容器async getOptions(filters?: OmitOptionHarnessFilters, ancestor): PromiseMatOptionHarness[] { if (!(await this.isOpen())) { throw new Error(Unable to retrieve options for timepicker. Timepicker panel is closed.); } return this._documentRootLocator.locatorForAll( MatOptionHarness.with({ ...(filters || {}), ancestor: await this._getPanelSelector(), } as OptionHarnessFilters), )(); }selectOption(filters)取第一个匹配过滤条件的选项并模拟点击若无匹配项则抛出Could not find a mat-option matching ...错误async selectOption(filters: OptionHarnessFilters): Promisevoid { const options await this.getOptions(filters); if (!options.length) { throw Error(Could not find a mat-option matching ${JSON.stringify(filters)}); } await options[0].click(); }三、MatTimepickerInputHarness输入框的测试入口3.1 类签名与宿主选择器export class MatTimepickerInputHarness extends ComponentHarness { static hostSelector: string; // 实际值为 .mat-timepicker-input static withT extends MatTimepickerInputHarness( this: ComponentHarnessConstructorT, options?: TimepickerInputHarnessFilters, ): HarnessPredicateT; isTimepickerOpen(): Promiseboolean; openTimepicker(): PromiseMatTimepickerHarness; closeTimepicker(): Promisevoid; getTimepicker(filter?: TimepickerHarnessFilters): PromiseMatTimepickerHarness; isDisabled(): Promiseboolean; isRequired(): Promiseboolean; getValue(): Promisestring; setValue(newValue: string): Promisevoid; getPlaceholder(): Promisestring; focus(): Promisevoid; blur(): Promisevoid; isFocused(): Promiseboolean; }源码 src/material/timepicker/testing/timepicker-input-harness.ts 中hostSelector为.mat-timepicker-input由输入指令MatTimepickerInput在宿主元素上添加该指令声明在 src/material/timepicker/timepicker-input.ts宿主选择器为input[matTimepicker]。3.2 面板联动方法isTimepickerOpen()读取宿主元素的aria-expanded属性是否为true这与组件实际渲染的 ARIA 状态完全一致无需关心面板 DOMopenTimepicker()若输入框未禁用则向宿主发送TestKey.DOWN_ARROW按键与真实用户用键盘打开时间选择器的行为一致随后返回关联的MatTimepickerHarnessasync openTimepicker(): PromiseMatTimepickerHarness { if (!(await this.isDisabled())) { const host await this.host(); await host.sendKeys(TestKey.DOWN_ARROW); } return this.getTimepicker(); }注意TestKey.DOWN_ARROW来自angular/cdk/testing是 CDK 测试框架统一封装的按键枚举closeTimepicker()点击 document root 元素以关闭面板随后调用forceStabilize()等待关闭动画结束async closeTimepicker(): Promisevoid { await this._documentRootLocator.rootElement.click(); await this.forceStabilize(); }getTimepicker(filter?)通过宿主上的mat-timepicker-id属性定位到面板的mat-timepicker-panel-id再在 document root 中查找对应的MatTimepickerHarness。若宿主没有该属性则抛出Element is not associated with a timepickerasync getTimepicker(filter: TimepickerHarnessFilters {}): PromiseMatTimepickerHarness { const host await this.host(); const timepickerId await host.getAttribute(mat-timepicker-id); if (!timepickerId) { throw Error(Element is not associated with a timepicker); } return this._documentRootLocator.locatorFor( MatTimepickerHarness.with({ ...filter, selector: [mat-timepicker-panel-id${timepickerId}], }), )(); }可以看到mat-timepicker-id与mat-timepicker-panel-id构成了一对内外关联的 ID 契约这是理解 timepicker 输入框与面板如何绑定的关键源码细节。3.3 值、状态与焦点方法getValue()/setValue(newValue)读取/写入原生input的 value。setValue不是直接赋值而是模拟真实键盘输入——先clear()清空再通过sendKeys(newValue)逐键输入从而触发组件的输入事件与表单更新逻辑若传入空字符串则跳过发送按键避免产生多余 focus 事件实现“清空值”的语义async setValue(newValue: string): Promisevoid { const inputEl await this.host(); await inputEl.clear(); if (newValue) { await inputEl.sendKeys(newValue); } }getPlaceholder()读取宿主placeholder属性isDisabled()/isRequired()分别读取宿主disabled/required属性focus()/blur()/isFocused()聚焦、失焦与聚焦状态断言用于测试表单 touched/验证触发时机。3.4 with() 中的自定义过滤MatTimepickerInputHarness.with()是唯一注册了额外过滤条件的 Harness另两个 Harness 的with()仅接受基类过滤。它通过HarnessPredicate.stringMatches支持按value与placeholder过滤二者均接受字符串或正则表达式static withT extends MatTimepickerInputHarness( this: ComponentHarnessConstructorT, options: TimepickerInputHarnessFilters {}, ): HarnessPredicateT { return new HarnessPredicate(this, options) .addOption(value, options.value, (harness, value) { return HarnessPredicate.stringMatches(harness.getValue(), value); }) .addOption(placeholder, options.placeholder, (harness, placeholder) { return HarnessPredicate.stringMatches(harness.getPlaceholder(), placeholder); }); }四、MatTimepickerToggleHarness切换按钮的测试入口export class MatTimepickerToggleHarness extends ComponentHarness { static hostSelector: string; // 实际值为 .mat-timepicker-toggle static with(options?: TimepickerToggleHarnessFilters): HarnessPredicateMatTimepickerToggleHarness; openTimepicker(): Promisevoid; isTimepickerOpen(): Promiseboolean; isDisabled(): Promiseboolean; }源码 src/material/timepicker/testing/timepicker-toggle-harness.ts 的hostSelector为.mat-timepicker-togglemat-timepicker-toggle组件根元素上的类名。它的内部结构很简单通过locatorFor(button)定位到可点击的按钮元素所有交互都落在该按钮上openTimepicker()先检查isTimepickerOpen()未打开时才点击按钮避免重复触发isTimepickerOpen()读取按钮的aria-expanded属性isDisabled()读取按钮的disabled属性并通过angular/cdk/coercion的coerceBooleanProperty归一化为布尔值async isDisabled(): Promiseboolean { const button await this._button(); return coerceBooleanProperty(await button.getAttribute(disabled)); }与输入框 Harness 通过键盘打开不同Toggle Harness 模拟的是鼠标点击行为两条打开路径在真实产品中分别对应键盘用户与鼠标用户测试时可按需选择。五、三种过滤器接口精准定位测试目标过滤器定义见 src/material/timepicker/testing/timepicker-harness-filters.ts均继承自 CDK 的BaseHarnessFiltersexport interface TimepickerHarnessFilters extends BaseHarnessFilters {} export interface TimepickerInputHarnessFilters extends BaseHarnessFilters { value?: string | RegExp; placeholder?: string | RegExp; } export interface TimepickerToggleHarnessFilters extends BaseHarnessFilters {}BaseHarnessFilters提供selectorCSS 选择器、ancestor祖先元素等通用过滤字段可用于区分页面上的多个实例TimepickerInputHarnessFilters额外支持按value输入值和placeholder占位文本过滤匹配规则与HarnessPredicate.stringMatches一致字符串按子串/精确语义匹配正则按模式匹配TimepickerHarnessFilters与TimepickerToggleHarnessFilters目前为空扩展仅保留基类能力未来新增过滤维度时不会破坏既有 API 形态。六、完整测试实践把 Harness 用起来仓库自带的测试 src/material/timepicker/testing/timepicker-harness.spec.ts 是学习 Harness 用法的最佳范本。测试使用TestbedHarnessEnvironment.documentRootLoader创建加载器并通过provideNativeDateAdapter与禁用动画的MATERIAL_ANIMATIONS提供者完成环境配置前者由 src/material/core 导出TestBed.configureTestingModule({ providers: [ provideNativeDateAdapter(), {provide: MATERIAL_ANIMATIONS, useValue: {animationsDisabled: true}}, ], }); const adapter TestBed.inject(DateAdapter); adapter.setLocale(en-US); fixture TestBed.createComponent(TimepickerHarnessTest); loader TestbedHarnessEnvironment.documentRootLoader(fixture);测试组件模板展示了 input timepicker 的标准组合并以interval4h生成 4 小时间隔的选项input idone [matTimepicker]onePicker mat-timepicker #onePicker [interval]interval()/ input idtwo [matTimepicker]twoPicker mat-timepicker #twoPicker [interval]interval()/几个代表性用例1. 加载与关联loader.getAllHarnesses(MatTimepickerHarness)得到 2 个实例通过MatTimepickerInputHarness.with({selector: #one})拿到指定输入框再input.getTimepicker()获得其关联的时间选择器。2. 打开/关闭状态断言初始timepicker.isOpen()为false调用input.openTimepicker()后变为true。3. 读取选项内容打开面板后timepicker.getOptions()用parallel并发读取每个 option 的文本得到[12:00 AM, 4:00 AM, 8:00 AM, 12:00 PM, 4:00 PM, 8:00 PM]——这正是interval4h从 0 点到 20 点的六档选项。4. 关闭状态读选项抛错面板未打开时getOptions()会以Unable to retrieve options for timepicker. Timepicker panel is closed.被拒绝用expectAsync(...).toBeRejectedWithError断言。5. 选择选项await timepicker.selectOption({text: 4:00 PM})后input.getValue()变为4:00 PM且timepicker.isOpen()回到false验证了选择后自动关闭的行为。此外 src/material/timepicker/testing/timepicker-input-harness.spec.ts 与 src/material/timepicker/testing/timepicker-toggle-harness.spec.ts 分别覆盖输入框与切换按钮 Harness 的读写值、焦点、禁用态与打开行为可作为更细粒度的参考。七、补充timepicker 主包 API 与 Harness 的关系Harness 测试的面板与选项均来自主包 goldens/material/timepicker/index.api.md 中定义的组件MatTimepickerD面板组件关键输入有interval选项间隔、options自定义选项数组、panelClass、ariaLabel等输出selected、opened、closedMatTimepickerInputD输入指令实现ControlValueAccessor与Validator输入matTimepicker必填、matTimepickerMin、matTimepickerMax、matTimepickerOpenOnClick、disabled同时提供信号化双向绑定value/valueChangeMatTimepickerToggleD切换按钮组件for属性别名timepicker指向目标 timepicker支持aria-label、tabIndex、disableRipple并允许投影[matTimepickerToggleIcon]自定义图标MatTimepickerConfig与MAT_TIMEPICKER_CONFIG全局默认配置注入令牌可配置interval与disableRipple参见 src/material/timepicker/timepicker.ts。Harness 中的getOptions()返回的MatOptionHarness对应的正是面板内由options生成的mat-option列表interval决定默认选项的疏密——这与测试中interval4h产生 6 个选项的预期完全吻合。更多关于间隔字符串语法如90m、1.5 hours与输入验证matTimepickerParse、matTimepickerMin/matTimepickerMax错误的说明可参考官方文档 src/material/timepicker/timepicker.md。八、最佳实践小结优先使用 Harness 而非原生 DOM 查询测试只依赖with()、getValue()、openTimepicker()等语义化方法组件模板重构不会破坏测试区分两种打开路径键盘场景用MatTimepickerInputHarness.openTimepicker()发送 Down Arrow鼠标场景用MatTimepickerToggleHarness.openTimepicker()点击按钮记住面板关闭约束getOptions()与selectOption()都要求面板处于打开状态测试中应先openTimepicker()关闭后如需等待动画完成再断言可用closeTimepicker()内部的forceStabilize语义或显式await fixture.whenStable()善用过滤器区分多实例同一页面存在多个 timepicker 时通过with({selector: #one})、{value: /^4:/}、{placeholder: Start time}等条件精确定位目标实例以 golden 报告为 API 变更红线若你的二次开发修改了 testing 包需同步更新 goldens/material/timepicker/testing/index.api.md 并通过 golden 校验确保公开 API 的稳定性可追踪。【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表