用 Playwright 为 WordPress 编写端到端测试:从环境配置到书评站点

原作者:Róbert Mészáros · 原载 WordPress Developer Blog · 2026 年 5 月 4 日

命名空间、编码规范和自动化单元测试,能够增强我们对项目按预期工作的信心。不过,还有一层值得补上:端到端测试(E2E)。它从不同角度检验项目,覆盖单元测试无法覆盖的情形。

本文将配置 WordPress 的 Playwright 测试,并编写针对区块变体、区块样板以及前台页面的测试。最后一个例子会先借助 REST API 准备数据,再检查前台输出。这些例子来自实际项目,可作为覆盖自己项目功能的起点。

什么是端到端测试

E2E 测试可以用于性能、无障碍和视觉测试,但最常见的形式是由界面驱动的功能测试:模拟用户在浏览器里操作 WordPress 等应用的过程。

与单元测试相比,E2E 着眼于宏观行为。即使只测试某项功能,它也会检查多个组件和应用层如何协同工作。这种范围有代价:E2E 通常更慢,有时也更脆弱。界面、网络或数据库任何一层的变化,都可能导致测试失败。因此,它更适合覆盖关键用户流程,而非穷举每一种情况。

Gutenberg 的产品形态很适合 E2E,项目从一开始就广泛使用了这类测试。后来,WordPress Core 也引入 E2E,与原本已经相当全面的 PHPUnit 测试套件互为补充。

开始之前

被测项目

本文测试的是 Justin Tadlock 在“用区块绑定构建书评网站”系列中实现的功能。建议先阅读第一部分:自定义字段与区块变体及第二部分:查询、样板与模板。两篇从头读完收获最大,不过只要了解功能,即使略过较深入的实现,也能跟上这里的测试。

前置项目说明:它是 Twenty Twenty-Four 的 TT4 Book Reviews 子主题,注册作者、评分、页数和 Goodreads URL 四个文章元数据字段。前三项绑定 Paragraph 的 content,URL 绑定 Button 的 url;字段开放给 REST API 并以单值保存。Book Review Card 样板将这些变体组合成书评卡片。前置教程以 WordPress 6.5 时代的 API 为背景;本文的测试版本以 2026 年原文给出的依赖为准。

工具与项目准备

若要跟着示例操作,需要安装 Git、Node.js 和 Docker。这里用 wp-env 启动本地 WordPress,而 Docker 是它的运行条件。安装方面可参考 Block Development Environment 指南。

下载或克隆书评站点示例仓库,从 master 分支开始。遇到问题时,可对照 feature/e2e-playwright-tests 分支中的最终测试代码。在项目目录安装已有依赖,其中包括 wp-scripts:

npm install

使用 wp-env 配置本地环境

运行测试前,需要一个已安装并启用 TT4 Book Reviews 子主题、同时具备 Twenty Twenty-Four 父主题的 WordPress 环境。

wp-env 由 WordPress 项目维护,能缩短准备时间,也方便与其他 WordPress 工具配合。它不是唯一选择:使用服务器上的专用测试站点也很常见。安装命令如下:

npm install @wordpress/env@^10.39.0 --save-dev

创建 .wp-env.json:

{
	"$schema": "https://schemas.wp.org/trunk/wp-env.json",
	"themes": [ "." ],
	"lifecycleScripts": {
		"afterStart": "THEME_SLUG=$(basename \"$PWD\"); for SERVICE in cli tests-cli; do wp-env run \"$SERVICE\" wp theme activate \"$THEME_SLUG\"; done"
	}
}

这份配置将当前目录映射为主题,生命周期脚本会在 wp-env 建立的开发环境和测试环境中分别启用它。原文环境默认提供 Twenty Twenty-Four,因此不需要另行安装父主题。

编者静态说明:afterStart 中的变量赋值、命令替换和 for 循环采用 POSIX shell 语法,不是 PowerShell 语法;在 Windows 原生 shell 下不能直接假定可用。脚本会实际改变两个环境的活动主题,主题目录名也应是可信、可预期的项目名。

启动环境:

npx wp-env start

原文展示的示例输出如下,端口和耗时只代表该次示例:

WordPress development site started at http://localhost:8888
WordPress test site started at http://localhost:8889
MySQL is listening on port 32770
MySQL for automated testing is listening on port 32771

 ✔ Done! (in 29s 136ms)

如果提示端口 8888 或 8889 已被占用,说明已有服务使用它。原文建议停止冲突服务,之后需要时再恢复;也可以为 wp-env 设置不同端口,但需要同步修改后文 Playwright 配置中的 webServer。停止服务前应确认它属于你的测试环境。

许多 WordPress 工具假定 package.json 中定义了 wp-env 脚本,接下来采用的默认 Playwright 配置也如此。在已有文件里合并以下项目:

{
	"scripts": {
		"start": "...",
		"build": "...",
		"wp-env": "wp-env"
	},
	"devDependencies": {
		// ...
	}
}

编者说明:上面是节选,... 和注释用于表示保留原有内容;含注释的版本不是合法 JSON,不能直接整段覆盖 package.json。

初次使用可继续阅读 Get started with wp-env,以及 Developer Blog 的“Quick and easy local WordPress development with wp-env”。

配置 Playwright

WordPress 使用 Playwright 进行端到端测试。这是成熟且广泛采用的方案。过去 WordPress 也使用过 Puppeteer,部分相关包仍然存在;本文采用的新项目配置基于 Playwright。

安装依赖

除了 Playwright Test,还要安装 @wordpress/e2e-test-utils-playwright。这个库提供 WordPress 专用辅助方法,让测试更容易编写:

npm install @playwright/test@^1.58.2 @wordpress/e2e-test-utils-playwright@^1.41.0 --save-dev

Playwright 还需要独立于 JavaScript 包的浏览器二进制和系统依赖,下载量可能有几百 MB。安装系统组件时可能要求管理员或 root 权限:

npx playwright install --with-deps

添加配置文件

Playwright 有很多配置选项。起步时可以使用 wp-scripts 导出的默认配置。创建 playwright.config.js:

export { default } from '@wordpress/scripts/config/playwright.config.js';

之后若要覆盖默认选项,例如修改测试文件目录,可以扩展这份基础配置:

import { defineConfig } from '@playwright/test';
import baseConfig from '@wordpress/scripts/config/playwright.config';

export default defineConfig( {
	...baseConfig,
	testDir: './tests/e2e/', // Instead of the specs directory which is the default location
} );

默认配置假定使用 wp-env,运行 E2E 测试时会自动启动它。采用其他环境时,需要自行调整 webServer 配置。不要把配置中的站点目标误指向生产环境。

检查配置

此时可以运行:

npx wp-scripts test-playwright

由于还没有测试文件,原文预期会出现:

Error: No tests found

这一步的“No tests found”符合当前阶段的预期。

第一个测试:载入仪表盘

先借用 WordPress Core E2E 测试套件中的一个简单案例,再编写项目特有测试。它访问后台仪表盘,检查是否显示“Welcome to WordPress”标题。在新建的 specs 目录中创建 main.spec.js:

import { test, expect } from '@wordpress/e2e-test-utils-playwright';

test( 'Loads WordPress dashboard', async ( { admin, page } ) => {
	await admin.visitAdminPage( '/' );

	await expect(
		page.getByRole( 'heading', { name: 'Welcome to WordPress', level: 2 } )
	).toBeVisible();
} );

再次运行:

npx wp-scripts test-playwright

原文中的预期结果是:

Running 1 test using 1 worker

  ✓  1 [chromium] › specs/main.spec.js:3:5 › Loads WordPress dashboard (831ms)

  1 passed (2.1s)

逐步理解测试

每个测试都有名称,例子中是“Loads WordPress dashboard”;具体逻辑位于传给 test() 的回调中。通常可以把测试分成准备条件、执行动作、验证结果三个部分,即 Arrange、Act、Assert(AAA)。真实 E2E 流程不一定只有一次线性三步,也可能在操作与断言之间交替进行。

这里的动作只是访问仪表盘:

await admin.visitAdminPage( '/' );

断言当然可以有其他设计,但如果在 /wp-admin/ 页面能看到层级为 2 的“Welcome to WordPress”标题,通常可以合理判断仪表盘已经加载:

await expect(
	page.getByRole( 'heading', { name: 'Welcome to WordPress', level: 2 } )
).toBeVisible();

admin.visitAdminPage() 来自 WordPress E2E Test Utils,封装了不少细节和边界处理。原文展示了其实现:

export async function visitAdminPage(
	this: Admin,
	adminPath: string,
	query?: string
) {
	await this.page.goto(
		join( 'wp-admin', adminPath ) + ( query ? `?${ query }` : '' )
	);


	// Handle upgrade required screen
	if ( this.pageUtils.isCurrentURL( 'wp-admin/upgrade.php' ) ) {
		// Click update
		await this.page.click( '.button.button-large.button-primary' );
		// Click continue
		await this.page.click( '.button.button-large' );
	}


	if ( this.pageUtils.isCurrentURL( 'wp-login.php' ) ) {
		throw new Error( 'Not logged in' );
	}


	const error = await this.getPageError();
	if ( error ) {
		throw new Error( 'Unexpected error in page content: ' + error );
	}
}

这个函数访问后台页面;如果遇到需要升级的界面,就点击升级和继续;如果跳转到登录页,则抛出未登录错误;最后还会检查页面错误。这个辅助方法不仅是简单的导航。

其余内容是标准 Playwright API:page.getByRole() 创建定位器,expect().toBeVisible() 执行可见性断言。还有一个关键前提:默认 WordPress 工具和配置会预先以管理员身份认证,所以这些测试默认站在已登录管理员的视角。需要验证访客行为时,必须另外处理认证状态。

使用 UI Mode

加上 --ui 即可启动界面模式,它会打开带有调试和检查工具的浏览器窗口:

npx wp-scripts test-playwright --ui

界面比较直观;想了解各项功能,可阅读 Playwright UI Mode 文档中的视频教程。即使平时偏爱命令行,也值得尝试。

为书评项目设计测试

本文选择三个关键方向,其他场景可以用同样的方法继续扩展:

  1. 检查 Book Author 等 Paragraph 区块变体已注册、能插入,且插入后的输出正确。
  2. 检查自定义样板已注册、能插入,并且包含预期区块。
  3. 设置文章元数据后,检查前台是否正确渲染这些值。
书评站点三个测试路径:插入 Book Author 后检查绑定属性和 Jane Austen;插入 Book Review Card 后比较 ARIA 快照;REST API 发布 Emma 后检查评分、页数、作者和 Goodreads 链接。
原创技术示意图:三个测试覆盖点及其数据流。根据原文整理,非浏览器截图或运行结果。

测试 Book Author 区块变体

TT4 Book Reviews 子主题将 Book Author 注册为 Paragraph 区块的一个变体。它使用区块绑定,将 themeslug_book_author 文章元数据的值显示为内容。注册代码如下:

registerBlockVariation( 'core/paragraph', {
	name: 'themeslug/book-author',
	title: __( 'Book Author', 'themeslug' ),
	description: __( 'Displays the book author.', 'themeslug' ),
	category: 'widgets',
	keywords: [ 'book', 'author' ],
	icon: pencil,
	scope: [ 'inserter' ],
	attributes: {
		metadata: {
			bindings: {
				content: {
					source: 'core/post-meta',
					args: {
						key: 'themeslug_book_author',
					},
				},
			},
		},
		placeholder: __( 'Book Author', 'themeslug' ),
	},
	example: {},
	isActive: ( blockAttributes ) =>
		'themeslug_book_author' ===
		blockAttributes?.metadata?.bindings?.content?.args?.key,
} );

先列出人工操作

设计由界面驱动的功能测试时,一个好起点是想清楚自己会如何手动检查,再逐步记录并转成代码。例如:新建文章,打开区块插入器,在可用区块中找到 Book Author,点击插入。

如果能完成这些动作,区块也出现在编辑器里,可以初步判断注册和插入没有问题。不过,还需要检查它是否真正显示绑定的文章元数据,不能只停在“插入成功”。先把插入动作写出来。

插入区块

可以在 specs 下新建测试文件;为简化示例,这里继续往 main.spec.js 添加“Inserts Book Author block”:

test( 'Inserts Book Author block', async ( { admin, page, editor } ) => {
	await admin.createNewPost();

	await page
		.getByRole( 'button', {
			name: 'Block Inserter',
		} )
		.click();

	await page
		.getByRole( 'region', { name: 'Block Library' } )
		.getByRole( 'listbox', { name: 'Widgets' } )
		.getByRole( 'option', { name: 'Book Author', exact: true } )
		.click();

	// Assertions
} );

使用界面模式可以观察每一步的动作和结果:

npx wp-scripts test-playwright --ui

第一步通过 WordPress 的辅助方法创建新文章:

await admin.createNewPost();

然后像人工操作一样,定位界面元素并点击:

await page
	.getByRole( 'button', {
		name: 'Block Inserter',
	} )
	.click();

await page
	.getByRole( 'region', { name: 'Block Library' } )
	.getByRole( 'listbox', { name: 'Widgets' } )
	.getByRole( 'option', { name: 'Book Author', exact: true } )
	.click();

Playwright 支持 CSS、XPath 等多种定位方式,但 WordPress 的最佳实践推荐优先采用可访问名称和语义角色。浏览器开发者工具通常能显示无障碍树,例如 Chrome DevTools 的 Accessibility 面板或完整无障碍树视图,可用来确认元素的 role 和 name。能使用语义定位时就优先使用,但它并非适用于所有情况。

验证属性

一种方法是检查插入后的区块是否带有正确属性,从而确认使用了 core/post-meta 绑定源。手动检查时,可切换到代码编辑器,观察是否有以下标记:

<!-- wp:paragraph {"placeholder":"Book Author","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_author"}}}}} -->
<p></p>
<!-- /wp:paragraph -->

这些属性都正确时,可以合理预期区块按绑定机制工作;如果属性正确却仍然异常,原文认为问题可能位于 WordPress Core。自动测试则可以读取区块结构,再与期望结构匹配:

await expect.poll( editor.getBlocks ).toMatchObject( [
	{
		name: 'core/paragraph',
		attributes: {
			metadata: {
				bindings: {
					content: {
						source: 'core/post-meta',
						args: { key: 'themeslug_book_author' },
					},
				},
			},
			placeholder: 'Book Author',
		},
	},
] );

这里采用 expect.poll() 是为处理异步区块插入。直接检查可能发生在区块尚未就绪时;轮询会重复调用回调,直到断言通过或超时,从而降低时序造成的偶发失败。

验证行为

另一种方法是直接检查 themeslug_book_author 的值是否显示为区块内容。手动操作需要打开 Editor settings,切到 Post 标签,展开 Book Review 面板,再填写 Author 字段:

await editor.openDocumentSettingsSidebar();
await page.getByRole( 'tab', { name: 'Post' } ).click();

await page
	.getByRole( 'region', { name: 'Editor settings' } )
	.getByRole( 'button', {
		name: 'Book Review',
	} )
	.click();

await page
	.getByRole( 'textbox', {
		name: 'Author',
	} )
	.fill( 'Jane Austen' );

为了设置一个值,这需要不少界面步骤。而当前测试的目标不是 Author 输入控件本身,因此可以通过 wp.data.dispatch 直接设置元数据,再选中区块验证显示内容:

await page.evaluate( () =>
	wp.data
		.dispatch( 'core/editor' )
		.editPost( { meta: { themeslug_book_author: 'Jane Austen' } } )
);

const bookAuthorBlock = editor.canvas.getByRole( 'document', {
	name: 'Block: Paragraph',
} );

await expect( bookAuthorBlock ).toHaveText( 'Jane Austen' );

page.evaluate() 会让回调在页面的 JavaScript 上下文执行,因而可以访问全局 wp.data。属性检查和行为检查并不互斥,可以同时保留。随着测试增多,你会逐渐确定项目需要的覆盖深度:哪些重复验证价值有限,哪些检查还不够稳健。

完整的 Book Author 测试如下:

test( 'Inserts Book Author block', async ( { admin, page, editor } ) => {
	await admin.createNewPost();

	await page
		.getByRole( 'button', {
			name: 'Block Inserter',
		} )
		.click();

	await page
		.getByRole( 'region', { name: 'Block Library' } )
		.getByRole( 'listbox', { name: 'Widgets' } )
		.getByRole( 'option', { name: 'Book Author', exact: true } )
		.click();

	await expect.poll( editor.getBlocks ).toMatchObject( [
		{
			name: 'core/paragraph',
			attributes: {
				metadata: {
					bindings: {
						content: {
							source: 'core/post-meta',
							args: { key: 'themeslug_book_author' },
						},
					},
				},
				placeholder: 'Book Author',
			},
		},
	] );

	await page.evaluate( () =>
		wp.data
			.dispatch( 'core/editor' )
			.editPost( { meta: { themeslug_book_author: 'Jane Austen' } } )
	);

	const bookAuthorBlock = editor.canvas.getByRole( 'document', {
		name: 'Block: Paragraph',
	} );

	await expect( bookAuthorBlock ).toHaveText( 'Jane Austen' );
} );

原文在这一阶段再次运行:

npx wp-scripts test-playwright

并展示两个测试通过的示例:

Running 2 tests using 1 worker

  ✓  1 [chromium] › specs/main.spec.js:3:5 › Loads WordPress dashboard (869ms)
  ✓  2 [chromium] › specs/main.spec.js:11:5 › Inserts Book Author block (2.6s)

  2 passed (4.8s)

测试 Book Review Card 样板

其他区块变体也可以按前面的方式测试。接下来换一个对象:主题注册的 Book Review Card 样板。它使用全部已注册的书评变体,并借助 Columns、Group 等区块把它们组织成特定布局。原文提供的样板源码如下:

<?php
/**
 * Title: Book Review Card
 * Slug: themeslug/book-review-card
 * Categories: themeslug-book-review
 * Viewport Width: 1376
 */
?>
<!-- wp:columns {"verticalAlignment":"center","align":"wide","style":{"spacing":{"padding":{"top":"var:preset|spacing|30","bottom":"var:preset|spacing|30","left":"var:preset|spacing|30","right":"var:preset|spacing|30"},"blockGap":{"top":"var:preset|spacing|40","left":"var:preset|spacing|30"}}},"backgroundColor":"accent"} -->
<div class="wp-block-columns alignwide are-vertically-aligned-center has-accent-background-color has-background" style="padding-top:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30);padding-left:var(--wp--preset--spacing--30)">

	<!-- wp:column {"verticalAlignment":"center","width":"33.33%"} -->
	<div class="wp-block-column is-vertically-aligned-center" style="flex-basis:33.33%">
		<!-- wp:post-featured-image {"aspectRatio":"3/4","style":{"border":{"radius":"0px"}}} /-->
	</div>
	<!-- /wp:column -->

	<!-- wp:column {"verticalAlignment":"center","width":"66.66%","style":{"spacing":{"blockGap":"var:preset|spacing|40"}}} -->
	<div class="wp-block-column is-vertically-aligned-center" style="flex-basis:66.66%">
		<!-- wp:group {"style":{"spacing":{"blockGap":"var:preset|spacing|10"}},"layout":{"type":"flex","orientation":"vertical"}} -->
		<div class="wp-block-group">
			<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
			<div class="wp-block-group">
				<!-- wp:paragraph -->
				<p>⭐️</p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph {"placeholder":"<?php esc_attr_e( 'Book Rating', 'themeslug' ); ?>","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_rating"}}}}} -->
				<p></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph -->
				<p><?php esc_html_e( '/ 5 Stars', 'themeslug' ); ?></p>
				<!-- /wp:paragraph -->
			</div>
			<!-- /wp:group -->

			<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
			<div class="wp-block-group">
				<!-- wp:paragraph -->
				<p><strong>📃</strong></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph {"placeholder":"<?php esc_attr_e( 'Book Length', 'themeslug' ); ?>","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_length"}}}}} -->
				<p></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph -->
				<p><?php esc_html_e( 'Pages', 'themeslug' ); ?></p>
				<!-- /wp:paragraph -->
			</div>
			<!-- /wp:group -->

			<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
			<div class="wp-block-group">
				<!-- wp:paragraph -->
				<p><?php esc_html_e( '✍️ Written by', 'themeslug' ); ?></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph {"placeholder":"<?php esc_attr_e( 'Book Author', 'themeslug' ); ?>","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_author"}}}}} -->
				<p></p>
				<!-- /wp:paragraph -->
			</div>
			<!-- /wp:group -->

			<!-- wp:buttons -->
			<div class="wp-block-buttons">
				<!-- wp:button {"metadata":{"bindings":{"url":{"source":"core/post-meta","args":{"key":"themeslug_book_goodreads_url"}}}}} -->
				<div class="wp-block-button"><a class="wp-block-button__link wp-element-button"><?php esc_html_e( 'View on Goodreads →', 'themeslug' ); ?></a></div>
				<!-- /wp:button -->
			</div>
			<!-- /wp:buttons -->
		</div>
		<!-- /wp:group -->

		<!-- wp:pullquote {"textAlign":"left","style":{"typography":{"fontSize":"1.2rem"},"spacing":{"padding":{"top":"0","bottom":"0"}}},"className":"is-style-plain"} -->
		<figure class="wp-block-pullquote has-text-align-left is-style-plain" style="padding-top:0;padding-bottom:0;font-size:1.2rem"><blockquote><p></p></blockquote></figure>
		<!-- /wp:pullquote -->
	</div>
	<!-- /wp:column -->

</div>
<!-- /wp:columns -->

当然可以像测试 Book Author 一样逐项检查它。但样板包含很多区块,测试会变得冗长而重复。这类情况适合换一种检查方法。

插入样板

验证样板是否注册、能否插入,仍然使用相同原则。在 main.spec.js 添加:

test( 'Inserts Book Review Card pattern', async ( { admin, page, editor } ) => {
	await admin.createNewPost();

	await page
		.getByRole( 'button', {
			name: 'Block Inserter',
		} )
		.click();

	await page
		.getByRole( 'tab', {
			name: 'Patterns',
		} )
		.click();

	await page.getByRole( 'tab', { name: 'Book Reviews' } ).click();

	await page
		.getByRole( 'listbox', { name: 'Book Reviews' } )
		.getByRole( 'option', { name: 'Book Review Card' } )
		.click();

	// Assertions
} );

这段代码先创建新文章,再打开插入器,选择 Patterns、Book Reviews 和 Book Review Card,以一个干净的起点执行插入。

通过 ARIA 快照验证

快照是在某个时刻捕获并保存的状态、元素或数据表示,之后可作为比较基准。样板包含多个区块时,使用快照可以一次核对整个结构。选择包裹其他区块的最外层 Columns,然后断言:

const bookReviewCardPattern = editor.canvas.getByRole( 'document', {
	name: 'Block: Columns',
} );

await expect( bookReviewCardPattern ).toMatchAriaSnapshot();

此时还没有快照基准,直接运行会出现下面的原文示例错误:

Running 3 tests using 1 worker

  ✓  1 [chromium] › specs/main.spec.js:3:5 › Loads WordPress dashboard (896ms)
  ✓  2 [chromium] › specs/main.spec.js:11:5 › Inserts Book Author block (2.6s)
  ✘  3 [chromium] › specs/main.spec.js:56:5 › Inserts Book Review Card pattern (8.0s)


  1) [chromium] › specs/main.spec.js:56:5 › Inserts Book Review Card pattern ───────────────────────

    Error: A snapshot doesn't exist at specs/__snapshots__/Inserts-Book-Review-Card-pattern-1-chromium.aria.yml, writing actual.

      80 |      } );
      81 |
    > 82 |      await expect( bookReviewCardPattern ).toMatchAriaSnapshot();
         |      ^
      83 | } );
      84 |

  1 failed
    [chromium] › specs/main.spec.js:56:5 › Inserts Book Review Card pattern ────────────────────────
  2 passed (13.0s)

界面模式下同样可以在 Errors 面板看到它。先人工确认样板正确,再用 --update-snapshots 保存快照:

npx wp-scripts test-playwright --update-snapshots

原文说明新文件会出现在 specs/__snapshots__/ 中,内容是 YAML 格式的无障碍树:

- 'document "Block: Columns"':
  - 'document "Block: Column (1 of 2)"':
    - 'document "Block: Featured Image"':
      - button "Add a featured image"
  - 'document "Block: Column (2 of 2)"':
    - 'document "Block: Stack"':
      - 'document "Block: Row"':
        - 'document "Block: Paragraph"'
        - document "Empty themeslug_book_rating; start writing to edit its value"
        - 'document "Block: Paragraph"'
      - 'document "Block: Row"':
        - 'document "Block: Paragraph"':
          - strong: 📃
        - document "Empty themeslug_book_length; start writing to edit its value"
        - 'document "Block: Paragraph"'
      - 'document "Block: Row"':
        - 'document "Block: Paragraph"'
        - document "Empty themeslug_book_author; start writing to edit its value"
      - 'document "Block: Buttons"':
        - 'document "Block: Book Goodreads Button"':
          - textbox "Button text"
    - 'document "Block: Pullquote"':
      - blockquote:
        - textbox "Pullquote text"

编者校正:原文错误信息中的文件名是 Inserts-Book-Review-Card-pattern-1-chromium.aria.yml,随后正文却写为省略 .aria 的 ...chromium.yml。这里保留原始输出,不把后一写法当作确定路径;实际名称应以所用 Playwright 版本和配置生成的文件为准。

保存后,用不带更新参数的命令执行比较:

npx wp-scripts test-playwright

只有明确要更新预期基准时,才使用 --update-snapshots。它会接受当前结构,不能把盲目更新快照当成修复回归的方法。这里的 ARIA 快照比较的是无障碍树,不等于像素截图对比。

完整的样板测试如下:

test( 'Inserts Book Review Card pattern', async ( { admin, page, editor } ) => {
	await admin.createNewPost();

	await page
		.getByRole( 'button', {
			name: 'Block Inserter',
		} )
		.click();

	await page
		.getByRole( 'tab', {
			name: 'Patterns',
		} )
		.click();

	await page.getByRole( 'tab', { name: 'Book Reviews' } ).click();

	await page
		.getByRole( 'listbox', { name: 'Book Reviews' } )
		.getByRole( 'option', { name: 'Book Review Card' } )
		.click();

	const bookReviewCardPattern = editor.canvas.getByRole( 'document', {
		name: 'Block: Columns',
	} );

	await expect( bookReviewCardPattern ).toMatchAriaSnapshot();
} );

原文再次运行命令:

npx wp-scripts test-playwright

三个测试通过的示例输出如下:

Running 3 tests using 1 worker

  ✓  1 [chromium] › specs/main.spec.js:3:5 › Loads WordPress dashboard (848ms)
  ✓  2 [chromium] › specs/main.spec.js:11:5 › Inserts Book Author block (2.8s)
  ✓  3 [chromium] › specs/main.spec.js:56:5 › Inserts Book Review Card pattern (3.1s)

  3 passed (7.7s)

测试前台与元数据

要验证前台是否显示文章元数据,既要插入区块或样板,又要准备元数据。全部通过编辑器界面完成会产生很多代码;如果文章如何创建并不是当前测试目标,可以缩短准备路径。

通过 REST API 创建文章

使用 REST API 创建文章时,一次传入样板内容以及作者、评分、页数和 Goodreads URL,便能绕过编辑器 UI。WordPress 测试工具提供了 requestUtils.createPost(),也可以直接调用 posts 等端点。在 main.spec.js 中添加以下测试准备部分:

test( 'Displays book review meta on the frontend', async ( {
	page,
	requestUtils,
} ) => {
	const newPost = await requestUtils.createPost( {
		status: 'publish',
		title: 'Emma',
		content: '<!-- wp:pattern {"slug":"themeslug/book-review-card"} /-->',
		meta: {
			themeslug_book_author: 'Jane Austen',
			themeslug_book_rating: '5',
			themeslug_book_length: '477',
			themeslug_book_goodreads_url:
				'https://www.goodreads.com/book/show/6969.Emma',
		},
	} );

	await page.goto( `?p=${ newPost.id }` );

	// Assertions
} );

编者校正与权限说明:原文这里有一次写成 main.specs.js,本文统一为前文实际创建的 main.spec.js。status: 'publish' 会创建已发布文章;这是测试环境中的真实写入,不是模拟请求。后续打开前台页面仍沿用当前 page 的认证状态,并未建立匿名访客会话。

检查前台输出

接着定位页面元素,检查文字是否可见以及链接属性是否正确:

await expect( page.getByText( '5 / 5 Stars' ) ).toBeVisible();
await expect( page.getByText( '477 Pages' ) ).toBeVisible();
await expect( page.getByText( 'Written by Jane Austen' ) ).toBeVisible();
await expect(
	page.getByRole( 'link', { name: 'View on Goodreads' } )
).toHaveAttribute(
	'href',
	'https://www.goodreads.com/book/show/6969.Emma'
);

这里不能全部使用 getByRole 之类的语义定位。例如“5 / 5 Stars”的 HTML 是:

<div class="wp-block-group is-nowrap is-layout-flex wp-container-core-group-is-layout-10 wp-block-group-is-layout-flex">
	<p>⭐️</p>
	<p>5</p>
	<p>/ 5 Stars</p>
</div>

外围 div 不具有特定语义,文字又分散在多个 p 中。因此原文采用 getByText() 查找这些内容,链接则继续用 role 定位并检查 href。完整测试如下:

test( 'Displays book review meta on the frontend', async ( {
	page,
	requestUtils,
} ) => {
	const newPost = await requestUtils.createPost( {
		status: 'publish',
		title: 'Emma',
		content: '<!-- wp:pattern {"slug":"themeslug/book-review-card"} /-->',
		meta: {
			themeslug_book_author: 'Jane Austen',
			themeslug_book_rating: '5',
			themeslug_book_length: '477',
			themeslug_book_goodreads_url:
				'https://www.goodreads.com/book/show/6969.Emma',
		},
	} );

	await page.goto( `?p=${ newPost.id }` );

	await expect( page.getByText( '5 / 5 Stars' ) ).toBeVisible();
	await expect( page.getByText( '477 Pages' ) ).toBeVisible();
	await expect( page.getByText( 'Written by Jane Austen' ) ).toBeVisible();
	await expect(
		page.getByRole( 'link', { name: 'View on Goodreads' } )
	).toHaveAttribute(
		'href',
		'https://www.goodreads.com/book/show/6969.Emma'
	);
} );

原文最后一次运行:

npx wp-scripts test-playwright

并展示四个测试全部通过的结果。以下是作者环境的输出,不代表本文编辑过程执行过这些测试:

Running 4 tests using 1 worker

  ✓  1 [chromium] › specs/main.spec.js:3:5 › Loads WordPress dashboard (848ms)
  ✓  2 [chromium] › specs/main.spec.js:11:5 › Inserts Book Author block (2.8s)
  ✓  3 [chromium] › specs/main.spec.js:56:5 › Inserts Book Review Card pattern (3.1s)
  ✓  4 [chromium] › specs/main.spec.js:85:5 › Displays book review meta on the frontend (413ms)

  4 passed (8.4s)

接下来可以深入什么

完成配置后,基础 E2E 测试没有想象中那么难;少量核心概念已经能覆盖不少真实场景。但本文只触及起点。

Playwright 文档内容全面,适合不同熟练程度的读者。可以从 Writing Tests 开始,了解更多操作和断言。然后学习如何跨文件、分组组织测试,以及使用在测试前后运行的 test hooks 简化准备和收尾。套件扩大后,也值得阅读 Playwright 最佳实践和 WordPress Core 的 E2E 建议。

真实案例可以参考 Gutenberg 的 Core 区块 E2E 测试,并浏览 WordPress Playwright 测试工具源码,了解还有哪些辅助方法。随着复用需求增多,fixtures 能把通用逻辑从单个测试中抽离;CI 指南则说明如何在每次提交或拉取请求时运行测试。

如果关注进一步的发展,可以了解 Test Agents;希望减少手写代码,也可以研究 Playwright 的测试生成工具。

原作者感谢 @greenshady 和 @bph 对本文的审阅。

示例代码的版权与许可

TT4 Book Reviews 主题的 源码头部标注 GNU GPL v2.0 or later。主题示例归原项目及其贡献者,项目背景由 Justin Tadlock 的书评站点教程提供。本文引用的 WordPress Playwright 辅助代码来自 Gutenberg;下列保留其项目版权声明及 GNU GPL 第2版全文。此项软件许可不替代文章本身及第三方图片的授权。

许可来源:Gutenberg 上游 LICENSE.md;GNU 官方 GPL 第2版。文中代码仍按对应原文展示,中文说明及标明的编者校注由本稿添加。

## Gutenberg

    Copyright 2016-2026 by the contributors

**License for Contributions (on and after April 15, 2021)**

All code contributed to the Gutenberg project is dual-licensed,
and released under both of the following licenses:

the GNU General Public License as published by the Free Software Foundation;
either version 2 of the License or (at your option) any later version (the “GPL”)
and the Mozilla Public License, Version 2.0 (the “MPL”).

**Project License**

The Gutenberg project license is not affected by the License for Contributions (as
discussed in the [Dual License section](#dual-license) below). The Gutenberg project
continues to be free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License or (at your option) any
later version (the “GPL”).

This program incorporates work covered by the following copyright and
permission notices:

    b2 is (c) 2001, 2002 Michel Valdrighi - m@tidakada.com -
    http://tidakada.com

    Wherever third party code has been used, credit has been given in the code's
    comments.

    b2 is released under the GPL

and

    WordPress - Web publishing software

    Copyright 2003-2010 by the contributors

    WordPress is released under the GPL

## GNU General Public License, Version 2

### GNU GENERAL PUBLIC LICENSE

Version 2, June 1991

    Copyright (C) 1989, 1991 Free Software Foundation, Inc.
    51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

    Everyone is permitted to copy and distribute verbatim copies
    of this license document, but changing it is not allowed.

### Preamble

The licenses for most software are designed to take away your freedom
to share and change it. By contrast, the GNU General Public License is
intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.

When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the software, or if you modify it.

For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.

We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on,
we want its recipients to know that what they have is not the
original, so that any problems introduced by others will not reflect
on the original authors' reputations.

Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at
all.

The precise terms and conditions for copying, distribution and
modification follow.

### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

**0.** This License applies to any program or other work which
contains a notice placed by the copyright holder saying it may be
distributed under the terms of this General Public License. The
"Program", below, refers to any such program or work, and a "work
based on the Program" means either the Program or any derivative work
under copyright law: that is to say, a work containing the Program or
a portion of it, either verbatim or with modifications and/or
translated into another language. (Hereinafter, translation is
included without limitation in the term "modification".) Each licensee
is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the Program
(independent of having been made by running the Program). Whether that
is true depends on what the Program does.

**1.** You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a
fee.

**2.** You may modify your copy or copies of the Program or any
portion of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

**a)** You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.

**b)** You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any part
thereof, to be licensed as a whole at no charge to all third parties
under the terms of this License.

**c)** If the modified program normally reads commands interactively
when run, you must cause it, when started running for such interactive
use in the most ordinary way, to print or display an announcement
including an appropriate copyright notice and a notice that there is
no warranty (or else, saying that you provide a warranty) and that
users may redistribute the program under these conditions, and telling
the user how to view a copy of this License. (Exception: if the
Program itself is interactive but does not normally print such an
announcement, your work based on the Program is not required to print
an announcement.)

These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

**3.** You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

**a)** Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections 1
and 2 above on a medium customarily used for software interchange; or,

**b)** Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your cost of
physically performing source distribution, a complete machine-readable
copy of the corresponding source code, to be distributed under the
terms of Sections 1 and 2 above on a medium customarily used for
software interchange; or,

**c)** Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is allowed
only for noncommercial distribution and only if you received the
program in object code or executable form with such an offer, in
accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

**4.** You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt otherwise
to copy, modify, sublicense or distribute the Program is void, and
will automatically terminate your rights under this License. However,
parties who have received copies, or rights, from you under this
License will not have their licenses terminated so long as such
parties remain in full compliance.

**5.** You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

**6.** Each time you redistribute the Program (or any work based on
the Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

**7.** If, as a consequence of a court judgment or allegation of
patent infringement or for any other reason (not limited to patent
issues), conditions are imposed on you (whether by court order,
agreement or otherwise) that contradict the conditions of this
License, they do not excuse you from the conditions of this License.
If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations,
then as a consequence you may not distribute the Program at all. For
example, if a patent license would not permit royalty-free
redistribution of the Program by all those who receive copies directly
or indirectly through you, then the only way you could satisfy both it
and this License would be to refrain entirely from distribution of the
Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

**8.** If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.

**9.** The Free Software Foundation may publish revised and/or new
versions of the General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.

Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Program does not specify a
version number of this License, you may choose any version ever
published by the Free Software Foundation.

**10.** If you wish to incorporate parts of the Program into other
free programs whose distribution conditions are different, write to
the author to ask for permission. For software which is copyrighted by
the Free Software Foundation, write to the Free Software Foundation;
we sometimes make exceptions for this. Our decision will be guided by
the two goals of preserving the free status of all derivatives of our
free software and of promoting the sharing and reuse of software
generally.

**NO WARRANTY**

**11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

**12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.

### END OF TERMS AND CONDITIONS

### How to Apply These Terms to Your New Programs

If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.

To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    one line to give the program's name and an idea of what it does.
    Copyright (C) yyyy  name of author

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

Also add information on how to contact you by electronic and paper
mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details
    type `show w'.  This is free software, and you are welcome
    to redistribute it under certain conditions; type `show c'
    for details.

The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, the
commands you use may be called something other than \`show w' and
\`show c'; they could even be mouse-clicks or menu items--whatever
suits your program.

You should also get your employer (if you work as a programmer) or
your school, if any, to sign a "copyright disclaimer" for the program,
if necessary. Here is a sample; alter the names:

    Yoyodyne, Inc., hereby disclaims all copyright
    interest in the program `Gnomovision'
    (which makes passes at compilers) written
    by James Hacker.

    signature of Ty Coon, 1 April 1989
    Ty Coon, President of Vice

This General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library,
you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
[GNU Lesser General Public
License](http://www.gnu.org/licenses/lgpl.html) instead of this
License.

---
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容