Elastic UI:用 EuiColorPickerObject 为 EuiColorPicker 编写 Playwright 组件对象测试
【免费下载链接】euiElastic UI Framework 🙌项目地址: https://gitcode.com/GitHub_Trending/eu/eui
EuiColorPicker 是一个由文本输入锚点加浮层面板构成的复合组件,直接拼 CSS 选择器写 Playwright 测试既脆弱又难维护。Elastic UI(EUI)官方测试辅助包@elastic/eui-test-helpers为此提供了EuiColorPickerObject这个 Playwright Component Object:它封装了锚点、浮层面板与色板(swatch)的稳定定位逻辑,并解释了 EUI 为颜色选择器生成的复合data-test-subj值为何必须用 token 匹配而非精确匹配。读完后,你可以在 Playwright/Scout 消费方测试中可靠地打开颜色选择器、选择色板、读写输入框颜色值,并理解这套封装背后的选择器设计与取舍。
一、背景:@elastic/eui-test-helpers与 Component Object 模式
@elastic/eui-test-helpers是 EUI monorepo 中与@elastic/eui独立版本化的测试辅助库,目前提供的是面向 Playwright/Scout 消费方的Component Object——围绕单个 PlaywrightLocator的语义化封装,把"如何可靠地找到并操作某个 EUI 组件"的知识沉淀在库内,消费方不再需要猜测哪些 CSS 类名只"看起来"对(见 包级 README)。
安装方式为:
yarn add --dev @elastic/eui-test-helpers注意该库与@elastic/eui独立版本化。因为辅助库依赖的是 EUI 组件的 DOM 结构和data-test-subj约定,所以要选择与你被测@elastic/eui版本兼容的辅助库版本;@playwright/test被声明为peerDependency,运行时由消费方自带。
所有 Component Object 的构造函数统一签名(scope, testSubj):
scope— PlaywrightPage或Locator,即搜索范围;testSubj— 你在应用中给组件根元素设置的data-test-subj值。
EuiColorPickerObject即从包入口统一导出(见 入口文件):
import { EuiColorPickerObject } from '@elastic/eui-test-helpers'; const colorPicker = new EuiColorPickerObject(page, 'myColorPicker'); await colorPicker.locator.click(); await colorPicker.swatches.first().click();二、关键前提:把data-test-subj设在EuiColorPicker本身上,以及"复合值"问题
这是使用EuiColorPickerObject最容易被踩坑的一点,也是组件对象文档反复强调的前提:
Set
data-test-subjonEuiColorPickeritself.EuiColorPickerrenders it on its anchor aseuiColorPickerAnchor <yourSubj>, a compound value the shared root lookup matches by token. Without that,getByTestIdon your own subj finds nothing.
也就是说,你必须把data-test-subj设置在EuiColorPicker组件 props 上,而不能期望锚点元素的data-test-subj就是你自己写的那个值。从源码看,EuiColorPicker 实现中有:
const testSubjAnchor = classNames('euiColorPickerAnchor', dataTestSubj);随后这个复合值会被写到锚点上:
- 默认文本输入锚点:
data-test-subj={testSubjAnchor}(color_picker.tsx#L642); - 自定义
buttonprop 场景:克隆你的 button 并注入同样的data-test-subj(color_picker.tsx#L590)。
因此锚点上的最终属性形如data-test-subj="euiColorPickerAnchor myColorPicker"。这正是 Playwright 原生page.getByTestId('myColorPicker')(精确匹配)找不到的原因——属性值并不等于你传的那个字符串。
Component Object 基类BaseObject用一个"按空白分词匹配单个 token"的 CSS 选择器解决了这个问题(见 base_object.ts#L18-L19):
const testSubjSelector = (testSubj: string): string => `[data-test-subj~="${testSubj.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`;这里用的是 CSS 的~=(单词匹配)操作符,只要data-test-subj的空白分隔列表中包含你的 token 即可命中,这与 Kibana 的@kbn/test-subj-selector约定一致。构造函数还会把你传入的componentSelector交给内部的"组件类型守卫"assertComponent()(base_object.ts#L104-L124):它通过root.and(scope.locator(componentSelector))校验命中的元素确实带有所需的组件类名,防止你拿EuiColorPickerObject去操作另一个恰好使用同名data-test-subj的非颜色选择器元素。
三、三条稳定选择器:锚点、面板、色板
EuiColorPickerObject的全部定位能力建立在三个稳定的 CSS 选择器常量上,集中定义在 selectors.ts:
export const EuiColorPickerSelectors = { /** The default text-input anchor. A custom `button` prop has no class of its own. */ ANCHOR_SELECTOR: '.euiColorPicker__input', /** The popover panel, rendered in a portal and only while open. */ PANEL_SELECTOR: '[data-popover-panel].euiColorPicker__popoverPanel', /** A swatch button inside the panel. */ SWATCH_SELECTOR: '.euiColorPickerSwatch', };对照 EUI 组件源码可以逐条印证:
| 选择器 | 源码出处 | 说明 |
|---|---|---|
.euiColorPicker__input | 默认锚点<EuiFieldText className="euiColorPicker__input">(color_picker.tsx#L627) | 仅默认文本输入锚点拥有该类名;自定义buttonprop 没有 |
.euiColorPicker__popoverPanel | EuiPopover的panelClassName="euiColorPicker__popoverPanel"(color_picker.tsx#L681-L704) | 浮层面板的类名,同时携带EuiPopover生成的data-popover-panel标记 |
.euiColorPickerSwatch | 色板按钮,快照中可见class="euiColorPickerSwatch emotion-..."(color_picker 测试快照) | 面板内每个色板对应一个按钮 |
组件对象的实现非常薄——构造时把锚点选择器交给基类做类型守卫,两个 getter 分别给出面板与色板定位器(见 object.ts):
export class EuiColorPickerObject extends BaseObject { constructor(scope: ObjectScope, testSubj: string) { super(scope, testSubj, EuiColorPickerSelectors.ANCHOR_SELECTOR); } /** * The popover panel. Rendered in a portal, so it is found on the page, not * under the anchor. Resolves to zero elements while closed. */ public get panel(): Locator { return this.root.page().locator(EuiColorPickerSelectors.PANEL_SELECTOR); } /** The swatch buttons inside {@link panel}. */ public get swatches(): Locator { return this.panel.locator(EuiColorPickerSelectors.SWATCH_SELECTOR); } }注意panelgetter 刻意调用this.root.page()把搜索范围提升到整个页面,而不是在锚点子树内查找——因为EuiColorPicker的浮层面板由EuiPopover通过 portal 渲染到页面其他位置,且只在打开时存在。
四、API 详解:panel、swatches与locator
组件对象文档给出的 API 面如下,这里结合实现逐条展开:
| Member | Description |
|---|---|
panel | 浮层面板的Locator。面板渲染在 portal 中,因此它在页面上而非锚点之下才能找到。关闭时解析为零个元素,打开后携带data-popover-open="true" |
swatches | 面板内部色板按钮的Locator(panel的子定位) |
locator | 继承自BaseObject的锚点定位器。默认锚点是一个普通文本输入框:点击它打开选择器,并用 Playwright 自带的inputValue()/fill()读取或写入颜色值 |
几个实战要点:
locator就是那个 HEX 文本输入框。EuiColorPicker的默认锚点显示当前颜色的大写 HEX 值(无颜色时显示占位符),你既可以用await colorPicker.locator.fill('#FF0000')直接改值,也可以用await colorPicker.locator.inputValue()断言当前颜色,无需自己拼输入框选择器。panel是"打开才有"的定位器。关闭状态下count()为 0;打开后EuiPopover会同步设置data-popover-open="true",所以打开与否可以用toHaveAttribute('data-popover-open', 'true')断言。swatches依赖panel。它等价于panel.locator('.euiColorPickerSwatch'),因此色板只在面板打开后可见;Playground 故事使用默认调色板,至少渲染一个色板。
五、用测试用例验证:对着 EUI Storybook 的自检规范
EuiColorPickerObject自带了一个对真实组件的验证用例,直接跑在 EUI 的 Storybook iframe 上。它用包内工具 storybook.ts 生成带参数的故事 URL,并通过 URL 参数data-test-subj:testColorPicker给 Playground 故事注入被测子句(见 object.spec.ts):
const TEST_SUBJ = 'testColorPicker'; const PLAYGROUND_URL = storyUrl( 'forms-euicolorpicker-euicolorpicker--playground', `data-test-subj:${TEST_SUBJ}` ); test.describe('EuiColorPickerObject', () => { let colorPicker: EuiColorPickerObject; test.beforeEach(async ({ page }) => { await page.goto(PLAYGROUND_URL); colorPicker = new EuiColorPickerObject(page, TEST_SUBJ); await colorPicker.locator.waitFor({ state: 'visible' }); }); test('resolves to exactly one element despite the compound contenteditable="false">【免费下载链接】euiElastic UI Framework 🙌
项目地址: https://gitcode.com/GitHub_Trending/eu/eui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考