EUI EuiRangeObject:用 Playwright Component Object 绕过 EuiRange 的 contenteditable="false">【免费下载链接】euiElastic UI Framework 🙌
项目地址: https://gitcode.com/GitHub_Trending/eu/eui
本篇指南基于@elastic/eui-test-helpers包中EuiRangeObject的官方文档 README,讲解如何用这个 Playwright Component Object 对 EUI 的EuiRange/EuiDualRange组件编写可靠测试:包括slider、numberInput两个语义化 Locator 的使用方式、showInput引发的data-test-subj双元素冲突成因、组件类型守卫机制,以及哪些场景被刻意排除在封装之外。读完后你将能够直接在自己的测试中引入该组件对象,避免 Playwright strict mode 下getByTestId命中多个元素而抛错的问题。
适用场景与背景:一个不直观的 EUI DOM 行为
在普通的 Playwright 测试中,你通常会给<EuiRange>或<EuiDualRange>设置data-test-subj,然后用page.getByTestId(x)定位元素。EUI 会把该属性透传到原生 range 滑杆(<input type="range">)上——range.test.tsx 快照中可以确认data-test-subj确实落在euiRangeSlider类名对应的原生输入框上。
问题在于:当EuiRange以showInput渲染时,同一个data-test-subj会被同时 spread 到原生滑杆和可见的数字输入框上。此时一个page.getByTestId(x)会解析出两个元素,在 Playwright 的 strict mode 下直接抛错。这是 EUI 真实存在的、并不显而易见的 DOM 歧义,EuiRangeObject的存在就是为了封装掉这个坑:
import { EuiRangeObject } from '@elastic/eui-test-helpers'; const range = new EuiRangeObject(page, 'myRange'); await range.slider.fill('50'); // 也可以 press 方向键、drag 等使用前提是 Playwright 配置中设置了testIdAttribute: 'data-test-subj'(该要求由基类 BaseObject 约定),并在被测组件上设置对应的data-test-subj。
API:两个语义化 Locator
EuiRangeObject的完整 API 由文档中的表格定义,结合 实现源码可以看到其真实行为:
| 成员 | 类型 | 说明 |
|---|---|---|
slider | Locator | 原生<input type="range">的定位器,一旦挂载即解析为恰好一个元素。CSS 选择器为.euiRangeSlider。当showInput="inputWithPopover"时,滑杆位于 popover 面板内部,在 popover 打开前不会挂载(需要先 focus/clicknumberInput才能打开)。 |
numberInput | Locator | 可见数字输入框的定位器,CSS 选择器为.euiRangeInput。仅当普通EuiRange设置了showInput时存在,否则解析为零个元素。 |
两个选择器集中定义在 EuiRangeSelectors 中:
export const EuiRangeSelectors = { /** The native `<input type="range">` element that carries the actual value. */ SLIDER_SELECTOR: '.euiRangeSlider', /** The visible number input, rendered on a plain `EuiRange` with `showInput`. */ NUMBER_INPUT_SELECTOR: '.euiRangeInput', };这两个类名在 EUI 组件源码中均有对应:滑杆在 range_slider.tsx 中以classNames('euiRangeSlider', className)渲染,数字输入框在 range_input.tsx 中以euiRangeInput euiRangeInput--${side}渲染——--min/--max后缀正对应EuiDualRange的双端输入。
冲突消解的实现原理
从 object.ts 的源码看,两个成员都是通过CSS 类与 testSubj 取交集来消歧的:
public get slider(): Locator { return this.root.and(this.scope.locator(EuiRangeSelectors.SLIDER_SELECTOR)); } public get numberInput(): Locator { return this.root.and(this.scope.locator(EuiRangeSelectors.NUMBER_INPUT_SELECTOR)); }this.root是由data-test-subj解析出的基础 Locator(可能是两个元素),.and()再叠加类名约束后,slider只命中滑杆、numberInput只命中数字输入框——即使两者共享同一个 test-subj 也各得其所。
组件类型守卫:为什么构造时传入了SLIDER_SELECTOR
EuiRangeObject的构造函数把EuiRangeSelectors.SLIDER_SELECTOR作为componentSelector传给基类:
constructor(scope: ObjectScope, testSubj: string) { super(scope, testSubj, EuiRangeSelectors.SLIDER_SELECTOR); }基类 BaseObject 会用它在每次调用公共方法前执行assertComponent():若data-test-subj命中的元素不同时匹配.euiRangeSlider,就抛出
Expected the element with>yarn add --dev @elastic/eui-test-helpers典型用法:
import { EuiRangeObject } from '@elastic/eui-test-helpers'; import { test, expect } from '@playwright/test'; test('set the range to 50', async ({ page }) => { await page.goto('/your-app'); await page.getByTestId('myRange').first().waitFor({ state: 'visible' }); const range = new EuiRangeObject(page, 'myRange'); // 若 showInput 已开启,等待时请用消歧后的 slider 而非裸 getByTestId await range.slider.fill('50'); expect(await range.slider.inputValue()).toBe('50'); });需要注意的两个实践要点:
- 等待时机:当
showInput开启时,裸page.getByTestId('myRange')会命中两个元素;此时应像仓库测试那样等待range.slider.waitFor({ state: 'visible' })而不是裸 testId。inputWithPopover下的挂载时序:popover 未打开前slider解析为 0 个元素,需要先await range.numberInput.click()打开 popover 再操作滑杆。小结
EuiRangeObject是 EUI 测试辅助包中一个针对性很强的封装:它用.euiRangeSlider/.euiRangeInput类名与data-test-subj取交集的方式,解决了showInput模式下同一 test-subj 落在滑杆与数字输入框两个元素上、导致 Playwright strict mode 报错的 EUI 特有歧义;同时通过基类的组件类型守卫防止误用。而EuiDualRange的 min/max 输入框、popover 开关操作、值读取这三类场景被有意排除在封装之外,由测试代码直接驱动——这种"封装歧义、不封装简单操作"的取舍,也正是该包 README 所强调的设计原则。【免费下载链接】euiElastic UI Framework 🙌
项目地址: https://gitcode.com/GitHub_Trending/eu/eui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考