news 2026/9/23 2:52:57

Stencil 组件测试实战指南:使用 WebdriverIO 编写真实浏览器组件测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Stencil 组件测试实战指南:使用 WebdriverIO 编写真实浏览器组件测试
  • 开发工具
  • 前端
  • 前端构建

【免费下载链接】stencil

A toolchain for building scalable, enterprise-ready component systems on top of TypeScript and Web Component standards. Stencil components can be distributed natively to React, Angular, Vue, (+ more) and traditional web applications from a single, framework-agnostic codebase.

项目地址:https://gitcode.com/gh_mirrors/st/stencil
点击查看免费下载

导读

Web Components 组件只有在真实浏览器环境中运行,才能暴露诸如属性序列化、Shadow DOM 插槽分配、Scoped 样式隔离、生命周期与用户交互之间的竞态条件等"编译期发现不了、单测测不出"的问题。本指南以 Stencil 仓库自带的端到端组件测试目录 test/wdio 为核心,系统讲解如何基于 WebdriverIO 的@wdio/browser-runner为 Stencil 组件编写、运行与调试真实浏览器测试,涵盖测试目录规划、构建流程、渲染 API、异步匹配器与多浏览器配置,并深入源码佐证其底层实现,帮助你为组件系统搭建一套可复用的真实浏览器测试基础设施。


一、认识 WebdriverIO 组件测试体系

test/wdio目录存放的是一组 Stencil 组件测试,用于验证涉及用户交互组件渲染的各种场景。与纯 Node 环境的 Jest 单元测试不同,这里的测试运行在真实浏览器中,能够覆盖:

  • 用户点击、输入等交互触发组件重渲染后的 DOM 状态;
  • 属性(@Prop)序列化、反射到属性(reflect)等浏览器侧行为;
  • Shadow DOM 与 Scoped 封装下的插槽(slot)内容分发;
  • 生命周期钩子、监听器(listener)在真实事件循环下的执行顺序;
  • CSS 变量、自定义样式、全局脚本等浏览器特性。

从目录结构可以看到,test/wdio 下按行为主题划分了几十个测试套件:attribute-basic(属性基础)、slot-nested-order(嵌套插槽顺序)、slot-hide-content(无目标插槽时隐藏内容)、shadow-dom-basicevent-listener-capturescoped-slot-*系列、lifecycle-*系列等。每个子目录内的组件和测试文件同处一地,保持了良好的内聚性。

可用脚本一览

test/wdio/package.json 中定义了三个核心脚本:

脚本作用
npm run build使用 Stencil 编译器将所有组件构建为lazy-loaded bundle(同时构建主应用、global-script、prerender、invisible-prehydration、es2022、auto-loader、test-sibling 等多个变体)
npm run wdio运行 WebdriverIO 测试
npm test依次执行buildwdioend-to-end

如果从仓库根目录运行,package.json 中还有两条封装命令:

npm run test.wdio # cd test/wdio && npm ci && npm run test npm run test.wdio.testOnly # 跳过构建,仅执行 wdio 测试
"test.wdio": "cd test/wdio && npm ci && npm run test", "test.wdio.testOnly": "cd test/wdio && npm ci && npm run wdio"

筛选与调试

可以通过--spec参数只运行特定场景:

npm run wdio -- --spec conditional-basic # 等价于运行 test/wdio/conditional-basic/cmp.test.tsx

调试时推荐开启 watch 模式,让测试文件修改后自动重跑:

npm run wdio -- --spec conditional-basic --watch

需要注意的是:watch 模式只监听测试文件的改动。如果修改了被测组件(cmp.tsx),需要另开一个终端手动重新执行npm run build把组件重新编译成 lazy bundle,测试才能感知到变更。这正是"组件必须先构建、后测试"这一设计约束的体现。


二、测试运行原理:组件如何进入浏览器

2.1 两步式流程

WebdriverIO 组件测试遵循一个关键前提:所有组件必须预先编译成 lazy-loaded Stencil 组件,才能执行测试。整个流程分两步:

  1. npm run build:由node ../../bin/stencil build --es5等命令编译组件。构建产物包括distdist-custom-elements(输出到test-components目录,customElementsExportBehavior: 'bundle')、dist-hydrate-script(输出到hydrate目录)等,见 test/wdio/stencil.config.ts。
  2. npm run wdio:WebdriverIO 的浏览器 runner 启动后,通过 Mocha 的require钩子加载设置脚本 test/wdio/setup.ts。

2.2 setup.ts 做了什么

test/wdio/setup.ts 是连接"构建产物"与"测试用例"的桥梁,其核心逻辑是:

const testRequiresManualSetup = window.__wdioSpec__.includes('custom-elements-output-tag-class-different') || window.__wdioSpec__.includes('custom-elements-delegates-focus') || window.__wdioSpec__.includes('custom-elements-output') || window.__wdioSpec__.includes('no-external-runtime') || window.__wdioSpec__.includes('global-script') || window.__wdioSpec__.endsWith('custom-tag-name.test.tsx') || // ... if (!testRequiresManualSetup) { await import('./dist/testapp/testapp.esm.js'); } if (window.__wdioSpec__.includes('global-script.test.tsx')) { await import('./www-global-script/build/testglobalscript.esm.js'); }

它依据当前运行 spec 的文件名(通过window.__wdioSpec__注入)判断:

  • 绝大多数测试套件:直接动态import('./dist/testapp/testapp.esm.js'),把全部编译好的自定义组件注册到浏览器,测试文件里无需任何额外 import即可使用<my-component>标签;
  • 少数特殊套件(如custom-elements-outputno-external-runtimeglobal-script等)需要测试自己手动设置组件,因此被排除在自动注册之外;
  • 涉及全局脚本的测试,还会额外加载独立的testglobalscript.esm.js构建产物。

该脚本顶部注释还引用了 WebdriverIO 官方wdio-browser-runner的 setup 实现(packages/wdio-browser-runner/src/browser/setup.ts)作为该做法的依据,说明这是浏览器 runner 的标准加载机制。

2.3 wdio.conf.ts 的 runner 配置

test/wdio/wdio.conf.ts 中,runner 被配置为'browser'并使用preset: 'stencil'

runner: [ 'browser', { preset: 'stencil', viteConfig: { resolve: { alias: { '@stencil/core/internal': path.resolve(__dirname, '..', '..', 'internal'), '@stencil/core': path.resolve(__dirname, '..', '..', 'internal'), }, }, }, }, ],

关键点:

  • preset: 'stencil'是 WebdriverIO 为 Stencil 提供的官方预设,负责把测试中的 JSX/TSX 正确编译并在浏览器中渲染组件;
  • 通过 Vitealias@stencil/core指向仓库自身的internal目录,保证测试使用当前仓库正在开发的运行时而非 npm 上发布的版本,这是仓库自测(self-hosting)的核心技巧;
  • 测试文件通过specs: [['./**/*.test.tsx', './**/*.test.ts']]匹配,框架为 Mocha(ui: 'bdd'),并通过mochaOpts.require: ['./setup.ts']注入 2.2 节所述设置脚本;
  • maxInstances: 10控制并行 worker 数,waitforTimeout: 3000是异步匹配的默认超时,specFileRetries: isCI ? 1 : 0在 CI 环境对失败 spec 重试一次。

2.4 多浏览器支持

配置通过BROWSER环境变量控制运行目标浏览器:

BROWSER=CHROME npm run wdio # 默认值,仅 Chrome BROWSER=FIREFOX npm run wdio # 仅 Firefox BROWSER=EDGE npm run wdio # 仅 Edge BROWSER=ALL npm run wdio # Chrome + Edge 全量

对应能力(capability)的注册逻辑位于 test/wdio/wdio.conf.ts 末尾:

if (['CHROME', 'ALL'].includes(BROWSER_CONFIGURATION)) { (config.capabilities as WebdriverIO.Capabilities[]).push({ browserName: 'chrome', browserVersion: 'stable', 'wdio:enforceWebDriverClassic': true, // < this is 3x faster? }); } // FIREFOX 与 EDGE 分支同理

值得注意的是,配置中明确注释"Disable FF tests due to issues in the WebDriver protocol",且ALL组合仅包含 Chrome 与 Edge,Firefox 需要显式通过BROWSER=FIREFOX单独触发——这是仓库实测得出的兼容性取舍,并非所有浏览器都默认纳入全量回归。


三、创建新的测试套件

3.1 目录与命名约定

测试套件存放在test/wdio下的子目录中,与被测组件同处一地(colocated)。创建步骤如下:

第一步:创建一个描述性的目录名,最好是能简洁概括被测行为的名词或动词

mkdir test/wdio/my-excellent-new-test-suite

仓库中现成的命名示例:svg-classslot-nested-orderslot-hide-contentconditional-basicevent-listener-capture等,均一目了然地描述被测行为。

第二步:在目录内创建被测组件cmp.tsx,只保留足以复现目标行为的最小实现。标签名(tag)与类名可自定义,但尽量与测试主题相关(例如更偏好<slot-relocation>而非<my-test-component-2>)。

[!IMPORTANT]组件标签名必须全局唯一,不得与其他任何测试套件的组件标签冲突。因为setup.ts会把所有构建产物一次性注册进浏览器,重名标签会互相覆盖、导致测试结果不可信。

第三步:创建测试文件,必须以.test.tsx结尾。约定命名方式为"主组件文件名换扩展名":若组件写在cmp.tsx,测试就写在cmp.test.tsx

第四步:运行npm run build(或从根目录执行npm run test.wdio,它会自动先构建),新组件才会进入 lazy bundle 供测试使用。

3.2 从零看一个真实套件:attribute-basic

以 test/wdio/attribute-basic/cmp.tsx 为例,被测组件覆盖了多种属性形态:

import { Component, h, Prop } from '@stencil/core'; @Component({ tag: 'attribute-basic', }) export class AttributeBasic { private _getter = 'getter'; @Prop() single = 'single'; @Prop() multiWord = 'multiWord'; @Prop({ attribute: 'my-custom-attr' }) customAttr = 'my-custom-attr'; @Prop() get getter() { return this._getter; } set getter(newVal: string) { this._getter = newVal; } render() { return ( <div> <div class="single">{this.single}</div> <div class="multiWord">{this.multiWord}</div> <div class="customAttr">{this.customAttr}</div> <div class="getter">{this.getter}</div> <div> <label class="htmlForLabel" htmlFor={'a'}> htmlFor </label> <input type="checkbox" id={'a'}></input> </div> </div> ); } }

对应测试文件 test/wdio/attribute-basic/cmp.test.tsx:

import { h } from '@stencil/core'; import { render } from '@wdio/browser-runner/stencil'; import { $, expect } from '@wdio/globals'; describe('attribute-basic', () => { before(async () => { render({ template: () => <attribute-basic-root></attribute-basic-root>, }); }); it('button click rerenders', async () => { await $('attribute-basic.hydrated').waitForExist(); await expect($('.single')).toHaveText('single'); await expect($('.multiWord')).toHaveText('multiWord'); await expect($('.customAttr')).toHaveText('my-custom-attr'); await expect($('.htmlForLabel')).toHaveAttribute('for', 'a'); await expect($('.getter')).toHaveText('getter'); const button = await $('button'); await button.click(); await expect($('.single')).toHaveText('single-update'); await expect($('.multiWord')).toHaveText('multiWord-update'); await expect($('.customAttr')).toHaveText('my-custom-attr-update'); await expect($('.getter')).toHaveText('getter-update'); }); });

这个套件同时验证了属性默认值渲染、@Prop({ attribute })自定义属性名映射、getter/setter 形式的@Prop,以及交互点击后组件重渲染输出新文本的完整链路。


四、编写测试用例

4.1 用render渲染组件

渲染组件使用@wdio/browser-runner/stencil提供的render辅助方法:

import { render } from '@wdio/browser-runner/stencil'; render({ template: () => <my-component></my-component>, });

由于 setup.ts 已经注册了所有编译好的组件,测试里不需要 import 组件本身,模板中直接写 JSX 标签即可。

4.2 组织测试结构

推荐使用常见的describe/it语法组织测试,用before/beforeEach钩子把组件渲染进页面。完整的简单示例:

import { h } from '@stencil/core'; import { render } from '@wdio/browser-runner/stencil'; import { $, expect } from '@wdio/globals'; describe('attribute-basic', function () { before(async () => { render({ template: () => <attribute-basic-root></attribute-basic-root>, }); }); it('button click rerenders', async () => { await expect($('.single')).toHaveText('single'); // ... }); });

$选择器与expect断言均来自@wdio/globals,与 WebdriverIO 的 e2e 测试 API 保持一致。

4.3 真实套件解析:slot-hide-content

test/wdio/slot-hide-content/cmp.test.tsx 展示了更复杂的用法——在beforeEach中渲染带插槽内容的组件、为按钮绑定 DOM 事件、并直接操作document查询节点:

import { Fragment, h } from '@stencil/core'; import { render } from '@wdio/browser-runner/stencil'; describe('slot-hide-content', function () { beforeEach(async () => { render({ template: () => ( <> <slot-hide-content-scoped className="test-cmp"> <p id="slotted-1">Hello</p> </slot-hide-content-scoped> <slot-hide-content-open className="test-cmp"> <p id="slotted-2">Hello</p> </slot-hide-content-open> <button type="button">Enable slot</button> </> ), }); await $('button').waitForExist(); document.querySelector('button').addEventListener('click', () => { document.querySelectorAll('.test-cmp').forEach((ref) => ref.setAttribute('enabled', true)); }); }); describe('scoped encapsulation', () => { it('should hide content when no slot is provided', async () => { const host = document.body.querySelector('slot-hide-content-scoped'); const slottedContent = host.querySelector('#slotted-1'); expect(slottedContent).toBeDefined(); expect(slottedContent.hasAttribute('hidden')).toBe(true); expect(slottedContent.parentElement.tagName).toContain('SLOT-HIDE-CONTENT-SCOPED'); document.querySelector('button').click(); await browser.pause(); expect(slottedContent.hasAttribute('hidden')).toBe(false); expect(slottedContent.parentElement.classList).toContain('slot-wrapper'); }); }); });

注意这里既使用了await expect($('button')).toExist()这类异步匹配(等待按钮渲染就绪),也允许在钩子里直接操作真实 DOM 事件,再通过browser.pause()等待渲染更新后同步断言——两种方式可以按场景混用。

4.4 条件渲染测试:conditional-basic

test/wdio/conditional-basic/cmp.test.tsx 是交互驱动重渲染的极简范式:初始状态下结果区为空文本,点击按钮后变为Content

describe('conditional-basic', () => { beforeEach(async () => { render({ template: () => <conditional-basic></conditional-basic>, }); }); it('contains a button as a child', async () => { await expect($('button')).toBeExisting(); }); it('button click rerenders', async () => { const button = $('button'); const results = $('div.results'); await expect(results).toHaveText(''); await button.click(); await expect(results).toHaveText('Content'); }); });

两个测试共享同一个beforeEach渲染,describe/it结构把"按钮存在性"与"点击后重渲染"拆成两个独立断言场景。


五、异步匹配器:避免竞态条件的正确姿势

5.1 为什么必须用异步匹配

组件渲染是异步的:元素可能尚未挂载、文本可能尚未更新。若用同步断言直接读取 DOM:

// 👎 组件此刻可能尚未渲染,元素不存在或文本不对 expect(document.querySelector('.single').textContent).toBe('single');

这一行在"元素还没渲染出来"时就会直接抛错,属于典型的竞态条件(race condition)

而 WebdriverIO 的异步匹配器会自动重跑断言,直到条件满足或超时失败:

// 👍 让 WebdriverIO 反复抓取并断言组件内容,直到条件满足 await expect($('.single')).toHaveText('single');

其语义是:在超时窗口内(默认waitforTimeout: 3000ms,可在 wdio.conf.ts 调整)反复求值$('.single')的文本是否为'single',期间组件完成渲染/重渲染即通过,从而彻底规避时序问题。仓库中几乎每个.test.tsx文件的断言都遵循这一写法。

5.2 常见异步匹配器速查

匹配器用途仓库使用示例
toHaveText(text)断言元素文本内容await expect($('.single')).toHaveText('single')
toHaveAttribute(name, value)断言属性值await expect($('.htmlForLabel')).toHaveAttribute('for', 'a')
toBeExisting()/toExist()断言元素已存在await expect($('button')).toBeExisting()
waitForExist()显式等待元素出现await $('attribute-basic.hydrated').waitForExist()

配合 WebdriverIO 官方文档中的组件测试、API 与 Expect Matchers 手册(见原文档 Resources 一节所列主题)可继续深入。


六、从源码印证:这套测试在验证什么

test/wdio中约 100 个测试套件覆盖的正是 Stencil 运行时(src/runtime)与编译器(src/compiler)中最容易出问题的行为面,从套件命名即可映射到对应实现模块:

  • 插槽与 scoped 封装slot-*scoped-slot-*shadow-dom-*系列):验证 src/runtime/slot-polyfill-utils.ts 与 src/utils/shadow-css.ts 中的内容分发与样式作用域逻辑;
  • 属性与反射attribute-*reflect-*property-serializer):对应 src/runtime/parse-property-value.ts 的属性序列化与 src/runtime/set-value.ts 的赋值路径;
  • 生命周期与监听器lifecycle-*listen-*event-*):对应 src/runtime/initialize-component.ts、src/runtime/host-listener.ts;
  • 条件/重渲染conditional-*async-rerenderkey-reorder):对应 src/runtime/update-component.ts 与 src/runtime/vdom 的虚拟 DOM 协调逻辑;
  • 样式css-variablesdynamic-css-variablesslotted-cssstyle-plugin):验证 src/runtime/styles.ts 的样式注入与 CSS 变量继承。

这些测试由 wdio.conf.ts 统一驱动,Mocha 框架(timeout: 60000retries: 1)保证每个用例在真实浏览器中得到充分执行,是 Stencil 发布流程(test.prod包含test.wdio)不可或缺的一环。


七、本地运行与调试清单

按以下顺序即可在本地完整跑通:

# 1. 进入测试目录并安装依赖 cd test/wdio && npm ci # 2. 构建全部测试组件(lazy bundle 及各变体) npm run build # 3. 只跑一个场景(调试期推荐) npm run wdio -- --spec conditional-basic # 4. 调试模式:修改测试文件自动重跑 npm run wdio -- --spec conditional-basic --watch # 5. 从仓库根目录一键运行(自动 npm ci + build + wdio) npm run test.wdio

常见注意事项:

  • 修改被测组件cmp.tsx后必须手动重跑npm run build,watch 模式不会监听组件文件;
  • 新增测试套件时确保组件标签名全局唯一(见 setup.ts 的全量注册机制);
  • 断言一律使用await expect($(...)).toXxx()异步匹配形式,避免同步读 DOM 引入竞态;
  • 需要多浏览器验证时通过BROWSER环境变量选择(Chrome 为默认,Firefox 需显式指定,全量ALL组合为 Chrome + Edge)。

结语

以 WebdriverIObrowserrunner 为核心、以"先编译 lazy bundle、再在真实浏览器中渲染断言"为基本盘的这套测试体系,是 Stencil 在 test/wdio 目录下沉淀出的成熟实践:renderAPI 屏蔽了组件注册细节,异步匹配器消除了时序竞态,多浏览器 capability 支持跨引擎回归。无论你是 Stencil 组件库的维护者,还是希望为自有组件系统引入真实浏览器测试,都可以直接复用 test/wdio/wdio.conf.ts 与 test/wdio/setup.ts 的组合,快速搭建起同样可构建、可筛选、可调试、可并行扩展的组件测试流水线。

  • 开发工具
  • 前端
  • 前端构建

【免费下载链接】stencil

A toolchain for building scalable, enterprise-ready component systems on top of TypeScript and Web Component standards. Stencil components can be distributed natively to React, Angular, Vue, (+ more) and traditional web applications from a single, framework-agnostic codebase.

项目地址:https://gitcode.com/gh_mirrors/st/stencil
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 2:48:08

四款主流AI编程工具深度对比:Cursor、Claude Code、Codex与Copilot选型指南

1. 四款主流 AI 编程工具的真实定位过去大半年&#xff0c;我几乎把市面上叫得上名字的 AI 编程工具都深度用了一遍。Cursor、Claude Code、Codex、GitHub Copilot&#xff0c;这四个名字在开发者圈子里被反复提起&#xff0c;但真正把它们放在同一个工作流里对比、并且持续用上…

作者头像 李华
网站建设 2026/9/23 2:45:45

Allegro库迁移实战:Samacsys Loader解决路径、依赖与参数继承难题

1. 这不是“一键导入”&#xff0c;而是Allegro工程师终于能喘口气的实操方案Cadence Allegro 17.4发布后&#xff0c;很多老用户第一反应不是新功能多炫酷&#xff0c;而是——“我的元器件库还在吗&#xff1f;”这个问题背后藏着三重现实压力&#xff1a;一是企业级封装库动…

作者头像 李华
网站建设 2026/9/23 2:45:30

基于CNN的猫狗图像识别:从数据准备到模型部署的完整实战

简介&#xff1a;这份Python实战项目基于CNN的猫狗图像识别检测分类源码包&#xff0c;面向正在完成期末大作业、毕业设计或需要项目实战的计算机专业学生。项目由导师指导并高分通过&#xff0c;评审得分98分&#xff0c;源码均在本地编译调试可运行&#xff0c;难度适中&…

作者头像 李华