hyva-playwright-test by hyva-themes/hyva-ai-tools
npx skills add https://github.com/hyva-themes/hyva-ai-tools --skill hyva-playwright-testHyvä 使用 Alpine.js + Tailwind CSS 替代了 Luma 的 KnockoutJS/RequireJS/jQuery。Playwright 的严格模式(拒绝匹配多个元素的定位器)与 Alpine.js 的 DOM 模式(页面中存在隐藏元素)存在冲突。本技能文档记录了在为 Hyvä 店面编写 Playwright 测试时发现的陷阱和解决方案。
Hyvä 模板在整个 DOM 中散布着像 <div x-show="displayErrorMessage" class="message error"> 这样的元素。这些元素不可见但存在,因此像 .message.error 这样的裸选择器会同时匹配隐藏和可见的实例,导致 Playwright 严格模式违规。
始终将页面级别的消息限定在 #messages 容器内:
// 错误 — 匹配整个 DOM 中隐藏的 Alpine x-show 元素
await expect(page.locator('.message.success')).toContainText('已添加到购物车');
await expect(page.locator('.message-error')).toContainText('错误');
// 正确 — 限定在可见的消息容器内
await expect(page.locator('#messages .message.success')).toContainText('已添加到购物车');
await expect(page.locator('#messages .message-error, #messages .message.error')).toContainText('错误');
切勿使用: 裸的 、、 或 作为选择器。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
.message.message.error.message.successdiv.message例外 — 内联页面消息: 并非所有 .message 元素都是闪存消息。搜索结果中的“无结果”通知(.message.notice)是作为静态内联内容在 #maincontent 内呈现的,而不是在 #messages 容器内。对于这些内联消息,裸的类选择器是正确的。
getByRole() — 始终优先使用 — 最接近用户感知页面的方式。避免了相同文本出现在标题、链接、面包屑导航和 sr-only 跨度中时的文本歧义。getByLabel() — 用于表单控件(复选框、带有关联标签的输入框)。getByText() — 用于非交互式元素,限定在容器内(例如,page.locator('#maincontent').getByText(...))。getByPlaceholder()、getByAltText() — 分别用于输入框和图像。getByTestId() — 当 Hyvä 提供 data-testid 属性或添加自定义测试 ID 时使用。aria-* 属性选择器(例如,[aria-label="pagination"]、[aria-current="page"])而非基于类的选择器。当必须使用 CSS 时,限定在唯一容器内(例如,#messages .message.success)。避免: :visible 伪选择器 — 根据 Playwright 文档,“通常最好找到一种更可靠的方式来唯一标识元素。” 应限定在容器内或使用角色/属性选择器。仅当 DOM 没有提供其他方式来区分元素时,才将 :visible 作为绝对的最后手段使用。
| 模式 | 问题 | 解决方案 |
|---|---|---|
x-show 隐藏元素 | 严格模式:多个匹配项 | 限定在唯一容器内(#messages),使用角色/属性选择器 |
x-defer="intersect" | 元素在可见之前未初始化 | 交互前先执行 scrollIntoViewIfNeeded() |
x-if(模板) | 条件为真前元素不存在于 DOM 中 | 先点击触发器,再查询子元素 |
输入框上的 x-model | Alpine 在表单提交后清除值 | 不要在提交后断言输入框的值;通过成功消息验证 |
x-text / x-html 异步更新 | 购物车徽章异步更新 | 使用带超时的 web-first 断言:not.toHaveText('0', { timeout: 15_000 }) |
x-show 子菜单 | 悬停前隐藏 | 点击子项前先在父项上执行 hover() |
| Alpine 表单显示 | 字段在复选框勾选前隐藏 | 勾选复选框后使用 waitFor({ state: 'visible' }) |
输入框上的 press('Enter') | 可能意外提交 Alpine 绑定的表单 | 优先在提交按钮上使用显式的 .click() |
始终使用能够自动等待和重试的 web-first 断言:
// 推荐 — 自动重试 // 不推荐 — 无重试
await expect(loc).toBeVisible(); // expect(await loc.isVisible()).toBe(true);
await expect(loc).toContainText('X'); // expect(await loc.textContent()).toContain('X');
对于异步的 Alpine.js 更新(购物车数量、价格),在断言上使用延长的超时 — 切勿使用 waitForTimeout():
// 购物车数量通过 Alpine x-text 异步更新
await expect(page.locator('#menu-cart-icon span[x-text="summaryCount"]'))
.not.toHaveText('0', { timeout: 15_000 });
| 元素 | 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/ 目录下的代码示例。加载与当前任务相关的文件:
始终有用:
特定页面(测试该页面时加载):
每周安装量
159
代码仓库
GitHub 星标数
59
首次出现
2026年2月16日
安全审计
安装于
github-copilot158
opencode149
codex149
gemini-cli148
amp148
kimi-cli148
Hyvä replaces Luma's KnockoutJS/RequireJS/jQuery with Alpine.js + Tailwind CSS. Playwright's strict mode (rejects locators matching multiple elements) conflicts with Alpine.js DOM patterns where hidden elements exist throughout the page. This skill documents pitfalls and solutions discovered while writing Playwright tests for Hyvä storefronts.
Hyvä templates scatter elements like <div x-show="displayErrorMessage" class="message error"> throughout the DOM. These are invisible but present , so a bare selector like .message.error matches both hidden and visible instances, causing Playwright strict mode violations.
Always scope page-level messages to the#messages container:
// 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');
Never use: bare .message, .message.error, .message.success, or div.message as selectors.
Exception — inline page messages: Not all .message elements are flash messages. The search results "no results" notice (.message.notice) renders as static inline content inside #maincontent, not inside the #messages container. For these inline messages, the bare class selector is correct.
Follow Playwright's recommended locator priority:
getByRole() — always prefer — closest to how users perceive the page. Avoids text ambiguity where the same text appears in headings, links, breadcrumbs, and sr-only spans.getByLabel() — for form controls (checkboxes, inputs with associated labels).getByText() — for non-interactive elements, scoped to a container (e.g., page.locator('#maincontent').getByText(...)).getByPlaceholder() , getByAltText() — for inputs and images respectively.getByTestId() — when Hyvä provides attributes or when adding custom test IDs.Avoid: :visible pseudo-selector — per Playwright docs, "it's usually better to find a more reliable way to uniquely identify the element." Scope to a container or use role/attribute selectors instead. Only use :visible as an absolute last resort when the DOM provides no other way to distinguish elements.
| Pattern | Problem | Solution |
|---|---|---|
x-show hidden elements | Strict mode: multiple matches | Scope to unique container (#messages), use role/attribute selectors |
x-defer="intersect" | Element not initialized until visible | scrollIntoViewIfNeeded() before interacting |
x-if (template) | Elements don't exist in DOM until condition true | Click the trigger first, then query children |
x-model on inputs |
Always use web-first assertions that auto-wait and retry:
// 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');
For async Alpine.js updates (cart counts, prices), use extended timeouts on the assertion — never 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 });
| Element | Hyvä Selector | Luma Selector |
|---|---|---|
| Pagination nav | getByRole('navigation', { name: 'pagination' }) | ul.pages-items |
| Page link | getByRole('link', { name: 'Page 2' }) | .pages-items li a |
| Active page | [aria-current="page"] | <strong> element |
| Filter button |
See references/ for code examples. Load files relevant to the current task:
Always useful:
Page-specific (load when testing that page):
Weekly Installs
159
Repository
GitHub Stars
59
First Seen
Feb 16, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot158
opencode149
codex149
gemini-cli148
amp148
kimi-cli148
Skills CLI 使用指南:AI Agent 技能包管理器安装与管理教程
33,600 周安装
Lobe Chat Debug 包使用指南 - TypeScript/Node.js 调试工具教程与最佳实践
157 周安装
Bunstuff Basic 后端开发规范指南:Elysia CRUD API 模块化架构与代码规范
1 周安装
Gemini Code Assist PR 评审工具 - 自动请求 AI 代码评审与配额管理
1 周安装
Symfony超能力指南:优化架构、安全重构与工作流程提升
157 周安装
refresh 工作区刷新脚本:PR合并后自动更新依赖与构建,确保原生开发环境稳定
1 周安装
Drizzle ORM 开发指南:TypeScript 类型安全 SQL 数据库 ORM 框架
157 周安装
data-testidaria-* attribute selectors (e.g., [aria-label="pagination"], [aria-current="page"]) over class-based selectors. When CSS is necessary, scope to a unique container (e.g., #messages .message.success).| Alpine clears value after form submit |
| Don't assert input value post-submit; verify via success message |
x-text / x-html async | Cart badge updates asynchronously | Use web-first assertions with timeout: not.toHaveText('0', { timeout: 15_000 }) |
x-show submenus | Hidden until hover | hover() on parent before clicking child |
| Alpine form reveal | Fields hidden until checkbox checked | waitFor({ state: 'visible' }) after checking the checkbox |
press('Enter') on input | May submit Alpine-bound form unexpectedly | Prefer explicit .click() on submit button |
getByRole('button', { name: 'Color filter' }) |
.filter-options-title |
| Cart icon badge | #menu-cart-icon > span[x-text="summaryCount"] | .counter-number |
| Account menu | #customer-menu + nav | .customer-menu |
| Success message | #messages .message.success | .message-success |
| Error message | #messages .message-error, #messages .message.error | .message-error |
| Main menu | getByRole('navigation', { name: 'Main menu' }) | nav.navigation |
| Footer nav | getByRole('navigation', { name: 'Company Menu' }).getByRole('link', { name }) | nav ul li:nth-child(N) a |
| Product image | #gallery img[itemprop="image"] | #gallery img:visible |
| Add to Cart (card) | getByRole('button', { name: /Add to Cart/ }).first() | button.btn-primary:visible |