关于 hyva-playwright-test
hyva-playwright-test 是用于编写 Hyvä 店面 Playwright 测试的技能,适用于使用 Alpine.js 的项目。它整理了考虑 Alpine.js DOM 行为的定位器、交互和断言方式。
主要功能
- 01可靠且限定范围的定位器策略
- 02Alpine.js 交互模式
- 03自动重试的网页优先断言
- 04Hyvä 与 Luma 选择器对照
适用场景
- 01测试 Hyvä 店面消息并避开隐藏元素匹配
- 02编写筛选器、菜单、表单和购物车更新测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ hyva-themes/ hyva-ai-tools --skill hyva-playwright-test安装后在 Claude Code 中输入/hyva-playwright-test确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/account-patterns.mdcart-patterns.mdcategory-patterns.mdpage-object-patterns.mdproduct-patterns.mdselector-patterns.md详细介绍
为 Hyvä + Alpine.js 编写 Playwright 测试
概述
Hyvä 使用 Alpine.js + Tailwind CSS 替代了 Luma 的 KnockoutJS/RequireJS/jQuery。Playwright 的严格模式(拒绝匹配多个元素的定位器)与 Alpine.js 的 DOM 模式冲突,因为隐藏元素会遍布整个页面。本文档记录了为 Hyvä 店面编写 Playwright 测试时发现的陷阱和解决方案。
首要规则:隐藏的 Alpine 元素
Hyvä 模板会在 DOM 中散布诸如 <div x-show="displayErrorMessage" class="message error"> 这样的元素。这些元素不可见但存在,因此像 .message.error 这样的裸选择器会同时匹配隐藏和可见的实例,导致 Playwright 严格模式违规。
始终将页面级消息限定在 #messages 容器内:
// WRONG — matches hidden Alpine x-show elements throughout DOM
await expect(page.locator('.message.success')).toContainText('Added to cart');
await expect(page.locator('.message-error')).toContainText('Error');
// RIGHT — scoped to the visible messages container
await expect(page.locator('#messages .message.success')).toContainText('Added to cart');
await expect(page.locator('#messages .message-error, #messages .message.error')).toContainText('Error');
不要使用: 裸 .message、.message.error、.message.success 或 div.message 作为选择器。
例外 — 页面内联消息: 并非所有 .message 元素都是闪存消息。搜索结果的“无结果”提示(.message.notice)会作为静态内联内容呈现在 #maincontent 内,而非 #messages 容器内。对于这些内联消息,使用裸类选择器是正确的。
选择器策略
getByRole()— 始终优先使用 — 最贴近用户感知页面的方式。当相同文本出现在标题、链接、面包屑和sr-only跨度中时,可避免文本歧义。getByLabel()— 用于表单控件(复选框、带关联标签的输入框)。getByText()— 用于非交互元素,并限定在容器内(例如,page.locator('#maincontent').getByText(...))。getByPlaceholder()、getByAltText()— 分别用于输入框和图片。getByTestId()— Hyvä 提供data-testid属性或添加自定义测试 ID 时使用。- CSS 选择器 — 最后手段,仅在没有面向用户的定位器可用时使用。优先使用
aria-*属性选择器(例如,[aria-label="pagination"]、[aria-current="page"]),而非基于类的选择器。必须使用 CSS 时,应限定在唯一容器内(例如,#messages .message.success)。
避免使用: :visible 伪选择器 — 根据 Playwright 文档,“通常最好寻找一种更可靠的方式来唯一标识元素。”应改为限定在容器内,或使用角色/属性选择器。仅当 DOM 没有其他方式区分元素时,才将 :visible 作为绝对最后手段。
Alpine.js 交互模式
| 模式 | 问题 | 解决方案 |
|---|---|---|
x-show 隐藏元素 | 严格模式:匹配多个元素 | 限定在唯一容器内(#messages),使用角色/属性选择器 |
x-defer="intersect" | 元素在可见前尚未初始化 | 交互前调用 scrollIntoViewIfNeeded() |
x-if(模板) | 条件为真之前,元素不会出现在 DOM 中 | 先点击触发器,再查询子元素 |
输入框上的 x-model | 提交表单后 Alpine 会清除值 | 不要在提交后断言输入框的值;通过成功消息进行验证 |
x-text / x-html 异步更新 | 购物车徽标会异步更新 | 使用带超时的 Web 优先断言:not.toHaveText('0', { timeout: 15_000 }) |
x-show 子菜单 | 悬停前处于隐藏状态 | 点击子项前,对父项调用 hover() |
| Alpine 表单显示 | 选中复选框前字段处于隐藏状态 | 选中复选框后调用 waitFor({ state: 'visible' }) |
对输入框调用 press('Enter') | 可能会意外提交绑定到 Alpine 的表单 | 优先对提交按钮显式调用 .click() |
断言
始终使用会自动等待并重试的 Web 优先断言:
// DO — auto-retries // DON'T — no retry
await expect(loc).toBeVisible(); // expect(await loc.isVisible()).toBe(true);
await expect(loc).toContainText('X'); // expect(await loc.textContent()).toContain('X');
对于 Alpine.js 异步更新(购物车数量、价格),请为断言设置更长的超时时间 — 绝不要使用 waitForTimeout():
// Cart count updates asynchronously via Alpine x-text
await expect(page.locator('#menu-cart-icon span[x-text="summaryCount"]'))
.not.toHaveText('0', { timeout: 15_000 });
Hyvä 与 Luma 选择器差异
| 元素 | Hyvä 选择器 | Luma 选择器 |
|---|---|---|
| 分页导航 | getByRole('navigation', { name: 'pagination' }) | ul.pages-items |
| 页面链接 | getByRole('link', { name: 'Page 2' }) | .pages-items li a |
| 当前页面 | [aria-current="page"] | <strong> 元素 |
| 筛选按钮 | getByRole('button', { name: 'Color filter' }) | .filter-options-title |
| 购物车图标徽标 | #menu-cart-icon > span[x-text="summaryCount"] | .counter-number |
| 账户菜单 | #customer-menu + nav | .customer-menu |
| 成功消息 | #messages .message.success | .message-success |
| 错误消息 | #messages .message-error, #messages .message.error | .message-error |
| 主菜单 | getByRole('navigation', { name: 'Main menu' }) | nav.navigation |
| 页脚导航 | getByRole('navigation', { name: 'Company Menu' }).getByRole('link', { name }) | nav ul li:nth-child(N) a |
| 商品图片 | #gallery img[itemprop="image"] | #gallery img:visible |
| 加入购物车(商品卡片) | getByRole('button', { name: /Add to Cart/ }).first() | button.btn-primary:visible |
参考资料
代码示例见 references/。请加载与当前任务相关的文件:
始终有用:
- page-object-patterns.md — 页面对象结构、导航、表单提交、重定向
- selector-patterns.md — 选择器修复前后对比(消息、文本歧义、表单)
特定页面(测试相应页面时加载):
- cart-patterns.md — 购物车加载指示器等待、数量变更、迷你购物车
- product-patterns.md — 套装商品数量、图库图片
- account-patterns.md — 密码更改(Alpine 复选框显示)
- category-patterns.md — 筛选(x-defer 滚动)、分页(ARIA)