正确做法是每次测试克隆 template.content:document.importNode(template.content, true),确保各用例 DOM 独立;须在真实浏览器中测试布局、焦点、IntersectionObserver 等;定位应语义化(如 getByRole),Shadow DOM 需显式穿透。

用 <template> 声明组件结构,但别直接渲染它
HTML 的 <template> 标签本身不参与渲染,只是个“待命容器”,适合做 UI 组件的声明式模板。但很多人一上来就 document.querySelector('template').content 然后 appendChild,结果测试时 DOM 状态不可控、无法重置、多次运行冲突。
正确做法是每次测试都克隆一份干净副本:document.importNode(template.content, true)。这样每个 test case 拥有独立 DOM 实例,互不污染。
- 不要复用同一个
template.content—— 它是 live node,修改会影响后续用例 - 避免在模板里写内联事件(如
onclick="doSomething()"),会绑定到全局作用域,难 mock 也难隔离 - 模板中尽量只含结构和原生属性(
required、aria-label、data-testid),行为逻辑全部交给 JS 控制
测试时必须进真实浏览器环境,jsdom 不够用
如果你用 Jest + jsdom 跑 <template> 渲染出的组件,会漏掉关键问题:CSS 布局是否触发、:focus 样式是否生效、offsetHeight 是否为 0、IntersectionObserver 是否回调——这些 jsdom 全都不支持。
真正能验证“用户看到什么”的,只有 Playwright 或 Puppeteer 这类真实浏览器驱动。例如测一个基于 <template> 构建的折叠面板:
立即学习“前端免费学习笔记(深入)”;
- 得点开再收起,观察
clientHeight变化,而不是只断言 class 名 - 要按 Tab 键验证键盘可访问性路径,jsdom 的
.focus()不触发真实焦点样式 - 禁用 CSS 后检查语义结构是否仍成立(比如靠
<h3>而不是 font-size 区分层级)
给组件加可测试锚点:role + name + state,别依赖 class 名
用 querySelector('.btn-primary') 定位按钮,测试脆弱得像纸糊的——UI 改个 class 就全挂。而 screen.getByRole('button', { name: '保存' }) 是语义化定位,同时强制你补全可访问性信息。
模板里就得提前埋好这些锚点:
- 所有交互元素必须有
role(或原生语义标签如<button>)和明确的name(aria-label/aria-labelledby/ 文本内容) - 状态变更要同步到 ARIA 属性,比如展开/收起用
aria-expanded="true/false",禁用用aria-disabled="true"(而非仅靠disabled属性) - 避免用
data-test-id当万能 selector——它绕过了可访问性验证,等于把测试和真实用户割裂开
Shadow DOM 组件必须显式穿透,否则测试查不到内部
如果组件封装在自定义元素里,并用了 Shadow DOM(比如 element.attachShadow({mode: 'open'})),Playwright 默认查不到内部节点。直接 page.getByRole('button') 会失败,因为按钮在 shadow root 里。
两种解法选其一:
- 用
shadow: true选项:page.getByRole('button', { name: '提交', shadow: true })(Playwright v1.30+) - 手动进入:
const shadow = await page.locator('my-custom-card').evaluate(el => el.shadowRoot),再用shadow.locator() - 注意:若用
mode: 'closed',Playwright 无法访问,必须改用'open'才可测
Shadow DOM 不是黑盒,而是测试必须主动打开的门——漏掉这步,90% 的内部逻辑就等于没测。



















