hyva-playwright-test
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWriting Playwright Tests for Hyvä + Alpine.js
为Hyvä + Alpine.js编写Playwright测试
Overview
概述
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ä 使用Alpine.js + Tailwind CSS替代了Luma主题的KnockoutJS/RequireJS/jQuery。Playwright的严格模式(拒绝匹配多个元素的定位器)与Alpine.js的DOM模式存在冲突,因为Alpine.js页面中随处可见隐藏元素。本文档记录了在为Hyvä前端编写Playwright测试时发现的陷阱及解决方案。
The #1 Rule: Hidden Alpine Elements
第一条规则:隐藏的Alpine元素
Hyvä templates scatter elements like throughout the DOM. These are invisible but present, so a bare selector like matches both hidden and visible instances, causing Playwright strict mode violations.
<div x-show="displayErrorMessage" class="message error">.message.errorAlways scope page-level messages to the container:
#messagestypescript
// 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 , , , or as selectors.
.message.message.error.message.successdiv.messageException — inline page messages: Not all elements are flash messages. The search results "no results" notice () renders as static inline content inside , not inside the container. For these inline messages, the bare class selector is correct.
.message.message.notice#maincontent#messagesHyvä模板在DOM中散布着类似的元素。这些元素不可见但确实存在,因此像这样的裸选择器会同时匹配隐藏和可见的实例,导致Playwright严格模式违规。
<div x-show="displayErrorMessage" class="message error">.message.error请始终将页面级消息限定在容器内:
#messagestypescript
// 错误写法 —— 匹配DOM中所有隐藏的Alpine x-show元素
await expect(page.locator('.message.success')).toContainText('Added to cart');
await expect(page.locator('.message-error')).toContainText('Error');
// 正确写法 —— 限定在可见的消息容器内
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.successdiv.message例外情况——内联页面消息: 并非所有元素都是弹窗消息。搜索结果中的“无结果”提示()是渲染在内的静态内联内容,而非容器中。对于这类内联消息,直接使用类选择器是正确的。
.message.message.notice#maincontent#messagesSelector Strategy
选择器策略
- — always prefer — closest to how users perceive the page. Avoids text ambiguity where the same text appears in headings, links, breadcrumbs, and
getByRole()spans.sr-only - — for form controls (checkboxes, inputs with associated labels).
getByLabel() - — for non-interactive elements, scoped to a container (e.g.,
getByText()).page.locator('#maincontent').getByText(...) - ,
getByPlaceholder()— for inputs and images respectively.getByAltText() - — when Hyvä provides
getByTestId()attributes or when adding custom test IDs.data-testid - CSS selectors — last resort, only when user-facing locators aren't available. Prefer attribute selectors (e.g.,
aria-*,[aria-label="pagination"]) over class-based selectors. When CSS is necessary, scope to a unique container (e.g.,[aria-current="page"]).#messages .message.success
Avoid: 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 as an absolute last resort when the DOM provides no other way to distinguish elements.
:visible:visible- —— 优先使用 —— 最贴近用户对页面的感知方式。避免文本歧义,比如相同文本出现在标题、链接、面包屑和
getByRole()跨度标签中的情况。sr-only - —— 用于表单控件(复选框、带有关联标签的输入框)。
getByLabel() - —— 用于非交互元素,限定在容器内(例如:
getByText())。page.locator('#maincontent').getByText(...) - 、
getByPlaceholder()—— 分别用于输入框和图片。getByAltText() - —— 当Hyvä提供
getByTestId()属性或添加自定义测试ID时使用。data-testid - CSS选择器 —— 最后手段,仅当没有面向用户的定位器时使用。优先使用属性选择器(例如
aria-*、[aria-label="pagination"])而非基于类的选择器。当必须使用CSS时,限定在唯一容器内(例如[aria-current="page"])。#messages .message.success
避免使用: 伪选择器 —— 根据Playwright文档,“通常最好找到更可靠的方式来唯一识别元素”。改为限定在容器内或使用角色/属性选择器。仅当DOM没有其他方式区分元素时,才将作为绝对最后的手段。
:visible:visibleAlpine.js Interaction Patterns
Alpine.js交互模式
| Pattern | Problem | Solution |
|---|---|---|
| Strict mode: multiple matches | Scope to unique container ( |
| Element not initialized until visible | |
| Elements don't exist in DOM until condition true | Click the trigger first, then query children |
| Alpine clears value after form submit | Don't assert input value post-submit; verify via success message |
| Cart badge updates asynchronously | Use web-first assertions with timeout: |
| Hidden until hover | |
| Alpine form reveal | Fields hidden until checkbox checked | |
| May submit Alpine-bound form unexpectedly | Prefer explicit |
| 模式 | 问题 | 解决方案 |
|---|---|---|
| 严格模式下匹配多个元素 | 限定在唯一容器( |
| 元素在可见前未初始化 | 交互前调用 |
| 元素在条件为真前不存在于DOM中 | 先点击触发元素,再查询子元素 |
输入框上的 | Alpine在表单提交后清除值 | 提交后不要断言输入框值;通过成功消息验证 |
| 购物车徽章异步更新 | 使用带超时的web-first断言: |
| 悬停前隐藏 | 点击子元素前先对父元素执行 |
| Alpine表单显示 | 字段在复选框勾选前隐藏 | 勾选复选框后调用 |
输入框上的 | 可能意外提交绑定Alpine的表单 | 优先使用显式的 |
Assertions
断言
Always use web-first assertions that auto-wait and retry:
typescript
// 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()typescript
// 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 });始终使用web-first断言,它会自动等待并重试:
typescript
// 推荐写法 —— 自动重试 // 不推荐写法 —— 无重试机制
await expect(loc).toBeVisible(); // expect(await loc.isVisible()).toBe(true);
await expect(loc).toContainText('X'); // expect(await loc.textContent()).toContain('X');对于Alpine.js的异步更新(购物车数量、价格),在断言中使用延长的超时时间——绝不要使用:
waitForTimeout()typescript
// 购物车数量通过Alpine x-text异步更新
await expect(page.locator('#menu-cart-icon span[x-text="summaryCount"]'))
.not.toHaveText('0', { timeout: 15_000 });Hyvä vs Luma Selector Differences
Hyvä与Luma的选择器差异
| Element | Hyvä Selector | Luma Selector |
|---|---|---|
| Pagination nav | | |
| Page link | | |
| Active page | | |
| Filter button | | |
| Cart icon badge | | |
| Account menu | | |
| Success message | | |
| Error message | | |
| Main menu | | |
| Footer nav | | |
| Product image | | |
| Add to Cart (card) | | |
| 元素 | Hyvä选择器 | Luma选择器 |
|---|---|---|
| 分页导航 | | |
| 页码链接 | | |
| 当前页码 | | |
| 筛选按钮 | | |
| 购物车图标徽章 | | |
| 账户菜单 | | |
| 成功消息 | | |
| 错误消息 | | |
| 主导航 | | |
| 页脚导航 | | |
| 商品图片 | | |
| 添加到购物车(卡片) | | |
References
参考资料
See for code examples. Load files relevant to the current task:
references/Always useful:
- page-object-patterns.md — Page object structure, navigation, form submits, redirects
- selector-patterns.md — Before/after selector fixes (messages, text ambiguity, forms)
Page-specific (load when testing that page):
- cart-patterns.md — Cart spinner wait, quantity changes, mini cart
- product-patterns.md — Bundle quantities, gallery images
- account-patterns.md — Password change (Alpine checkbox reveal)
- category-patterns.md — Filters (x-defer scroll), pagination (ARIA)
查看目录下的代码示例。加载与当前任务相关的文件:
references/常用文件:
- page-object-patterns.md —— 页面对象结构、导航、表单提交、重定向
- selector-patterns.md —— 选择器修复前后对比(消息、文本歧义、表单)
页面特定文件(测试对应页面时加载):
- cart-patterns.md —— 购物车加载等待、数量修改、迷你购物车
- product-patterns.md —— 捆绑商品数量、图库图片
- account-patterns.md —— 密码修改(Alpine复选框显示)
- category-patterns.md —— 筛选(x-defer滚动)、分页(ARIA)