一、playwright介绍及安装:
1、介绍:
端到端测试(End to end)是现代应用中的一种常见的测试方式,它模仿用户在客户端的 UI 界面执行操作。与单元测试(Unit Test)不同,前者主要通过测试函数的输入、输出、抛出错误等数据,确保其能够可靠完成工作;而后者更加关注在应用客户端场景下,一个完整的操作链是否能够完成,界面的内容信息、布局样式是否符合预期。
相比起经典的测试工具 Cypress
、Selenium
,Playwright
具有非常大的优势:
- API 设计更加合理,甚至提供了可视化生成测试代码的能力,使用难度较低;
- 支持多线程执行测试,具有更快的测试执行速度;
- 支持所有的现代浏览器,包括
Chromium
、WebKit
、Firefox
等;
2、安装:
pnpm create playwright
安装好后的项目目录
3、默认浏览器内核配置:
浏览器内核:chromium firefox webkit
4、运行测试案例:
里面自带有测试案例 example.spec.ts,执行命令:
npx playwright test
5、报错及解决方案:
发现报错了...
这是因为 playwright 默认的timeout时间为30秒,过了30秒测试案例没跑完,测会抛出异常;
解决办法:
- 延长超时时间
- 给访问页面的goto方法,配置参数 { waitUntil: 'domcontentloaded' };
思考:在实际项目中,使用playwright的goto方法访问一个页面时,容易碰到load未加载导致超时的问题,如何解决?
解答:这是因为默认情况下,goto 方法默认通过页面的 load 事件判定访问成功,而 load 事件的触发是要求页面中所有资源加载完成的,而 domcontentloaded 只要求 HTML 文档完成解析;
1、DOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成之后,在页面能显示内容的时候,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载。
2、load:当整个页面及所有依赖资源(JS,CSS,图片等全部加载完)如样式表和图片都已完成加载时,将触发 load 事件。它与 DOMContentLoaded 不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载。
所以解决方案就是在使用goto方法的时候,配置waitUntil: 'domcontentloaded';这样goto方法不需要等到页面所有资源都加载完成才有返回值;
6、HTML测试报告:
7、在UI模式下运行示例测试:
npx playwright test --ui
8、在Head模式下运行浏览器:
要在 head 模式下运行测试,请使用 --headed
标志。 这将使你能够直观地看到 Playwright 如何与网站交互。
npx playwright test --headed
相当于在测试的时候,打开了浏览器,实时展示测试的场景;
9、在不同浏览器上的测试:
要指定要在哪个浏览器上运行测试,请使用 --project
标志,后跟浏览器名称。
npx playwright test --project webkit
要指定多个浏览器来运行测试,请多次使用 --project
标志,后跟每个浏览器的名称。
npx playwright test --project webkit --project firefox
10、运行特定测试
要运行单个测试文件,请传入要运行的测试文件的名称。
npx playwright test landing-page.spec.ts
要从不同目录运行一组测试文件,请传入要在其中运行测试的目录的名称。
npx playwright test tests/todo-page/ tests/landing-page/
要运行具有特定标题的测试,请使用 -g
标志,后跟测试标题。
npx playwright test -g "add a todo item"
11、跟踪查看器
Playwright Trace Viewer 是一个 GUI 工具,可让你探索测试中记录的 Playwright 跟踪,这意味着你可以前后浏览测试的每个操作,并直观地查看每个操作期间发生的情况。
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0, // set to 2 when running on CI
// ...
use: {
trace: 'on-first-retry', // 记录每个测试的第一次重试的跟踪
},
});
执行命令:
npx playwright test --trace on
打开报告页面:
npx playwright show-report
点击标题,可以打开详情页面
滚动条下拉,点击trace,可以下载一个压缩文件;
返回上一页面,点击View trace,可以打开如下页面,可以详细看到测试轨迹;
12、Vscode playwright插件安装:
在vscode扩展里面搜索 Playwright Test for VSCode,并安装:
13、安装 Playwright Test for VSCode 插件遇到的问题:
思考:分享vscode playwright插件安装碰到的问题与解决过程;
解答:
1、playwright在vscode里面有一个插件,Playwright Test for VSCode,是一个非常有用的插件,使用起来很方便;
2、在vscode里面安装好Playwright Test for VSCode后,打开测试界面,发现是空白的!,以为是插件没安装好,重复卸载安装几次后,在测试界面依然是空白的...,重启vscode,再次打开测试界面,发现有内容了,第一坑。。。
3、编写好一个组件测试案例,再次打开vscode的测试界面,发现刚才的测试案例加载出来了,但是点击测试图标,没有反应。那是什么问题?依旧重启vscode,没有解决。上网搜索,发现需要安装@playwright/test依赖,安装好后问题解决,第二坑。。。
安装好后的示意图:
这样,你写的测试案例,都会检索出来根据一定的层级在这里列出来
二、写测试案例
1、运行代码生成器:
使用 codegen
命令运行测试生成器,后跟要为其生成测试的网站的 URL。 URL 是可选的,你始终可以在没有它的情况下运行命令,然后将 URL 直接添加到浏览器窗口中。
npx playwright codegen demo.playwright.dev/todomvc
执行上面命令,会打开两个窗口;你在做边页面做的所有操作,都会记录到右边;
codegen记录的页面操作:
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc/');
await page.goto('https://demo.playwright.dev/todomvc/#/');
await page.locator('html').click();
await page.getByPlaceholder('What needs to be done?').fill('学习');
await page.getByPlaceholder('What needs to be done?').press('Enter');
await page.getByPlaceholder('What needs to be done?').fill('休息');
await page.getByPlaceholder('What needs to be done?').press('Enter');
await page.getByPlaceholder('What needs to be done?').fill('吃午餐');
await page.getByPlaceholder('What needs to be done?').press('Enter');
await page.getByText('学习').click();
await page.getByText('休息').click();
await page.getByRole('link', { name: 'Completed' }).click();
await page.getByText('All Active Completed').click();
await page.getByText('吃午餐').click();
await page.getByText('All Active Completed').click();
await page.getByRole('link', { name: 'All' }).click();
await page.getByRole('link', { name: 'Active' }).click();
await page.getByText('休息').click();
await page.getByText('休息').click({
button: 'right'
});
await page.getByText('休息').click();
await page.getByText('学习').click();
});
思考:playwright测试代码生成器优缺点与适用场景?
解答:测试代码生成器 是 Playwright 的一个主要卖点,生成器会根据我们在浏览器中的交互与操作,自动生成对应的测试代码。
缺点:
1、对页面元素的定位不够精准,难以适应 DOM 结构的修改。(使用 testid 可以大幅改善)
2、生成的代码只有元素定位与交互操作的部分,只能证明“流程可以跑通”,但是由于缺少断言,无法确保“流程正确”。
3、生成的代码重复度很高,不利于后期的修改调整,往往只能推倒重来。
适用场景:
1、补充存量业务用例时,当前迭代周期交付压力大,编写测试用例的时间不足,至少能够以这种方式实现“从无到有”。
2、新开发的特性,后续存在着大幅修改和迭代的可能,但至少要确保业务能够跑通。
3、这种低规格的用例作为一种临时方案,在时间充裕或者业务稳定后,会被更加精细化的用例逐步替代。
2、生成定位器
你可以使用测试生成器生成 locators。
- 按
'Record'
按钮停止录音,然后会出现'Pick Locator'
按钮。 - 单击
'Pick Locator'
按钮,然后将鼠标悬停在浏览器窗口中的元素上,即可查看每个元素下方高亮的定位器。 - 要选择定位器,请单击要定位的元素,该定位器的代码将显示在“选择定位器”按钮旁边的定位器在线运行中。
- 然后,你可以在定位器在线运行中编辑定位器以对其进行微调,并查看浏览器窗口中高亮的匹配元素。
- 使用复制按钮复制定位器并将其粘贴到你的代码中。
3、配置:
playwright.config.ts
3.1、基本配置:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 在“tests”目录中查找相对于此配置文件的测试文件。
testDir: 'tests',
// 并行运行所有测试。
fullyParallel: true,
// 如果您不小心离开了测试,则使构建在CI上失败。只在源代码中。
forbidOnly: !!process.env.CI,
// 只能在CI上重试。
retries: process.env.CI ? 2 : 0,
// 选择退出CI上的并行测试。
workers: process.env.CI ? 1 : undefined,
// 报告者使用
reporter: 'html',
use: {
// 在' await page.goto('/') '等操作中使用的基本URL。
baseURL: 'http://127.0.0.1:3000',
// 重新尝试失败的测试时收集跟踪。
trace: 'on-first-retry',
},
// 为主要浏览器配置项目。
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
// 在开始测试之前运行本地开发服务器。
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
选项 | 描述 |
---|---|
testConfig.forbidOnly | 如果任何测试被标记为 test.only ,是否会错误退出。 对 CI 很有用。 |
testConfig.fullyParallel | 让所有文件中的所有测试并行运行。 详细信息请参见 /并行性和分片。 |
testConfig.projects | 在多种配置或多个浏览器上运行测试 |
testConfig.reporter | 报告器使用。 请参阅 测试报告器 了解有关哪些报告器可用的更多信息。 |
testConfig.retries | 每次测试的最大重试次数。 请参阅 测试重试 了解有关重试的更多信息。 |
testConfig.testDir | 包含测试文件的目录。 |
testConfig.use | 带有 use{} 的选项 |
testConfig.webServer | 要在测试期间启动服务器,请使用 webServer 选项 |
testConfig.workers | 用于并行测试的最大并发工作进程数。 也可以设置为逻辑 CPU 核心的百分比,例如 '50%'. 。 详细信息请参见 /并行性和分片。 |
3.2、过滤测试
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Glob模式或正则表达式来忽略测试文件。
testIgnore: '*test-assets',
// 匹配测试文件的全局模式或正则表达式。
testMatch: '*todo-tests/*.spec.ts',
});
3.3、高级配置:
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Folder for test artifacts such as screenshots, videos, traces, etc.
outputDir: 'test-results',
// path to the global setup files.
globalSetup: require.resolve('./global-setup'),
// path to the global teardown files.
globalTeardown: require.resolve('./global-teardown'),
// Each test is given 30 seconds.
timeout: 30000,
});
testConfig.globalSetup | 全局安装文件的路径。 在所有测试之前将需要并运行该文件。 它必须导出单个函数。 |
testConfig.globalTeardown | 全局拆卸文件的路径。 在所有测试之后将需要并运行该文件。 它必须导出单个函数。 |
testConfig.outputDir | 用于测试工件的文件夹,例如屏幕截图、视频、痕迹等。 |
testConfig.timeout | Playwright 对每次测试强制执行 timeout,默认为 30 秒。 测试函数、fixtures、beforeEach 和 afterEach 钩子所花费的时间包含在测试超时中。 |
3.4、期望选项
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
// expect()应该等待条件满足的最大时间。
timeout: 5000,
toHaveScreenshot: {
// 可以不同的可接受的象素数量,默认情况下不设置。
maxDiffPixels: 10,
},
toMatchSnapshot: {
// 不同的像素的可接受比例
// 像素的总数,介于0和1之间。
maxDiffPixelRatio: 0.1,
},
},
});
选项 | 描述 |
---|---|
testConfig.expect | 网络优先断言 和 expect(locator).toHaveText() 一样,默认都有 5 秒的单独超时。 这是 expect() 等待条件满足的最长时间。 了解有关 测试并预期超时 以及如何为单个测试设置它们的更多信息。 |
expect(page).toHaveScreenshot() | expect(locator).toHaveScreeshot() 方法的配置。 |
expect(value).toMatchSnapshot() | expect(locator).toMatchSnapshot() 方法的配置。 |
4、测试使用选项:
1、基本选项:
设置所有测试的基本 URL 和存储状态:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// 在' await page.goto('/') '等操作中使用的基本URL。
baseURL: 'http://127.0.0.1:3000',
// 使用给定的存储状态填充上下文。
storageState: 'state.json',
},
});
选项 | 描述 |
---|---|
testOptions.baseURL | 用于上下文中所有页面的基本 URL。 允许仅使用路径进行导航,例如 page.goto('/settings') 。 |
testOptions.storageState | 使用给定的存储状态填充上下文。 对于轻松验证很有用,了解更多。 |
其它如下,略。。。
模拟选项
网络选项
录音选项
其它选项
更多浏览器和上下文选项
显式上下文创建和选项继承
配置范围
测试使用选项 | Playwright 中文网 (nodejs.cn)
5、注释功能:
Playwright Test 支持测试注释来处理失败、不稳定、跳过、焦点和标签测试:
- test.skip() 将测试标记为不相关。 Playwright 测试不运行这样的测试。 当测试在某些配置中不适用时使用此注释。
- test.fail() 将测试标记为失败。 Playwright Test 将运行此测试并确保它确实失败。 如果测试没有失败,Playwright 测试将会诉说。
- test.fixme() 将测试标记为失败。 与
fail
注释相反,Playwright 测试不会运行此测试。 当运行测试缓慢或崩溃时使用fixme
。 - test.slow() 将测试标记为慢速并将测试超时增加三倍。
注释可以用于单个测试或一组测试。 注释可以是有条件的,在这种情况下,它们在条件为真时应用。 注释可能取决于测试装置。 同一个测试可能有多个注释,可能处于不同的配置中。
5.1、集中测试:
你可以集中一些测试。 当存在集中测试时,仅运行这些测试。
test.skip('skip this test', async ({ page }) => {
// This test is not run
});
5.2、有条件得跳过测试:
你可以根据情况跳过某些测试。
test('skip this test', async ({ page, browserName }) => {
test.skip(browserName === 'firefox', 'Still working on it');
});
5.3、群组测试
你可以对测试进行分组,为它们指定一个逻辑名称,或者在组的钩子之前/之后确定范围。
import { test, expect } from '@playwright/test';
test.describe('two tests', () => {
test('one', async ({ page }) => {
// ...
});test('two', async ({ page }) => {
// ...
});
});
5.4、标签测试
有时你想将测试标记为 @fast
或 @slow
,并且只运行具有特定标记的测试。 我们建议你为此使用 --grep
和 --grep-invert
命令行标志:
import { test, expect } from '@playwright/test';
test('Test login page @fast', async ({ page }) => {
// ...
});
test('Test full report @slow', async ({ page }) => {
// ...
});
然后你将只能运行该测试:
npx playwright test --grep @fast
或者,如果你想要相反的结果,你可以跳过带有特定标签的测试:
npx playwright test --grep-invert @slow
要运行包含任一标签(逻辑 OR
运算符)的测试:
npx playwright test --grep "@fast|@slow"
5.5、有条件地跳过一组测试:
test.describe('chromium only', () => {
test.skip(({ browserName }) => browserName !== 'chromium', 'Chromium only!');
test.beforeAll(async () => {
// This hook is only run in Chromium.
});
test('test 1', async ({ page }) => {
// This test is only run in Chromium.
});
test('test 2', async ({ page }) => {
// This test is only run in Chromium.
});
});
5.6、在 beforeEach
钩子中使用 fixme
为了避免运行 beforeEach
钩子,你可以在钩子本身中添加注释。
test.beforeEach(async ({ page, isMobile }) => {
test.fixme(isMobile, '设置页面还不能在手机上工作');
await page.goto('http://localhost:3000/settings');
});
test('user profile', async ({ page }) => {
await page.getByText('My Profile').click();
// ...
});
6、模拟:
Playwright 为选定的台式机、平板电脑和移动设备提供了使用 playwright.devices 的 设备参数注册表。 它可用于模拟特定设备的浏览器行为,例如用户代理、屏幕尺寸、视口以及是否启用了触摸。 所有测试都将使用指定的设备参数运行。
模拟 | Playwright 中文网 (nodejs.cn)
7、重试:
Playwright 支持 测试重试。 启用后,失败的测试将重试多次,直到通过,或达到最大重试次数。 默认情况下,不会重试失败的测试。
# 对失败的测试重试3次
npx playwright test --retries=3
也可以在配置文件中配置
import { defineConfig } from '@playwright/test';
export default defineConfig({
// 对失败的测试重试3次
retries: 3,
});
Playwright 测试将测试分类如下:
- "passed" - 第一次运行时通过的测试;
- "flaky" - 第一次运行失败,但重试时通过的测试;
- "failed" - 第一次运行失败且所有重试均失败的测试。
7.1、串行模式
使用 test.describe.serial() 对相关测试进行分组,以确保它们始终按顺序一起运行。 如果其中一项测试失败,则跳过所有后续测试。 组中的所有测试都会一起重试。
import { test } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => { /* ... */ });
test('first good', async ({ page }) => { /* ... */ });
test('second flaky', async ({ page }) => { /* ... */ });
test('third good', async ({ page }) => { /* ... */ });
当没有 retries 运行时,失败后的所有测试都会被跳过:
7.2、在测试之间重用单页
Playwright Test 为每个测试创建一个独立的 页面 对象。 但是,如果你想在多个测试之间重用单个 页面 对象,你可以在 test.beforeAll() 中创建自己的对象并在 test.afterAll() 中关闭它。
import { test, type Page } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test('runs first', async () => {
await page.goto('https://playwright.nodejs.cn/');
});
test('runs second', async () => {
await page.getByText('Get Started').click();
});
8、超时:
Playwright Test 对于各种任务有多个可配置的超时。
暂停 | 默认 | 描述 |
---|---|---|
测试超时 | 30000 毫秒 | 每个测试的超时,包括测试、钩子和夹具: 默认设置 config = { timeout: 60000 } 覆盖 test.setTimeout(120000) |
期望超时 | 5000 毫秒 | 每个断言的超时: 默认设置 config = { expect: { timeout: 10000 } } 覆盖 expect(locator).toBeVisible({ timeout: 10000 }) |
8.1、测试超时
Playwright Test 强制每个测试超时,默认为 30 秒。 测试函数、装置、beforeEach
和 afterEach
钩子所花费的时间包含在测试超时中。
8.1.1、在配置中设置测试超时
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 5 * 60 * 1000,
});
8.1.2、设置单个测试的超时时间
import { test, expect } from '@playwright/test';
test('slow test', async ({ page }) => {
test.slow(); // 将默认超时设置为三倍的简单方法
// ...
});test('very slow test', async ({ page }) => {
test.setTimeout(120000);
// ...
});
8.1.3、更改 beforeEach
钩子的超时
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }, testInfo) => {
// Extend timeout for all tests running this hook by 30 seconds.
testInfo.setTimeout(testInfo.timeout + 30000);
});
8.1.4、更改 beforeAll
/afterAll
钩子的超时
beforeAll
和 afterAll
钩子具有单独的超时,默认情况下等于测试超时。 你可以通过在钩子内调用 testInfo.setTimeout() 为每个钩子单独更改它。
import { test, expect } from '@playwright/test';
test.beforeAll(async () => {
// Set timeout for this hook.
test.setTimeout(60000);
});
8.2、期望超时
像 expect(locator).toHaveText()
这样的 Web 优先断言有一个单独的超时,默认为 5 秒。 断言超时与测试超时无关。 它会产生以下错误:
example.spec.ts:3:1 › basic test ===========================
Error: expect(received).toHaveText(expected)
Expected string: "my text"
Received string: ""
Call log:
- expect.toHaveText with timeout 5000ms
- waiting for "locator('button')"
8.2.1、在配置中设置期望超时
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
timeout: 10 * 1000,
},
});
8.3、全局超时
Playwright Test 支持整个测试运行超时。 这可以防止出现问题时过度使用资源。 没有默认的全局超时,但你可以在配置中设置合理的超时,例如一小时。 全局超时会产生以下错误:
Running 1000 tests using 10 workers
514 skipped
486 passed
Timed out waiting 3600s for the entire test run
你可以在配置中设置全局超时:
// playwright.config.ts
import { defineConfig } from '@playwright/test';export default defineConfig({
globalTimeout: 60 * 60 * 1000,
});
8.4、高级:底层超时
略
超时 | Playwright 中文网 (nodejs.cn)
8.5、思考:playwright 的几种超时时间的区别,及设置方法总结。
解答:playwright 有6种超时时间,
1、测试超时:运行每个测试案例的时间,默认30秒;
在playwright.config.ts配置文件中配置timeout,也可以设置单独的每个test案例的测试时间,使用test.slow()方法或者test.setTimeout(120000)等方法
2、期望超时:只expect的执行超时时间,默认5秒;
在playwright.config.ts配置文件中配置expect:{timeout:10000};
或者在断言里面设置:await expect(page.getByRole('button')).toHaveText('Sign in', { timeout: 10000 });
3、全局超时:支持整个测试运行超时,这可以防止出现问题时过度使用资源;
在playwright.config.ts配置文件中配置globalTimeout: 60 * 60 * 1000;
4、操作超时:
在playwright.config.ts配置文件中配置actionTimeout: 10 * 1000,
5、导航超时:
在playwright.config.ts配置文件中配置navigationTimeout: 30 * 1000,
6、底层超时:这些是测试运行程序预先配置的底层超时,你不需要更改它们。
三、行动:
1、文本输入:
使用 locator.fill() 是填写表单字段的最简单方法。 它聚焦元素并使用输入的文本触发 input
事件。 它适用于 <input>
、<textarea>
和 [contenteditable]
元素。
// Text input
await page.getByRole('textbox').fill('Peter');
// Date input
await page.getByLabel('Birth date').fill('2020-02-02');
// Time input
await page.getByLabel('Appointment time').fill('13:15');
// Local datetime input
await page.getByLabel('Local time').fill('2020-03-02T05:15');
2、复选框和单选按钮
使用 locator.setChecked() 是选中和取消选中复选框或单选按钮的最简单方法。 此方法可用于 input[type=checkbox]
、
// Check the checkbox
await page.getByLabel('I agree to the terms above').check();
// Assert the checked state
expect(page.getByLabel('Subscribe to newsletter')).toBeChecked();
// Select the radio button
await page.getByLabel('XL').check();
3、选择选项
使用 locator.selectOption() 选择 <select>
元素中的一个或多个选项。 你可以指定选项 value
或 label
进行选择。 可以选择多个选项。
// Single selection matching the value or label
await page.getByLabel('Choose a color').selectOption('blue');
// Single selection matching the label
await page.getByLabel('Choose a color').selectOption({ label: 'Blue' });
// Multiple selected items
await page.getByLabel('Choose multiple colors').selectOption(['red', 'green', 'blue']);
4、鼠标点击
执行简单的人工点击。
// Generic click
await page.getByRole('button').click();
// Double click
await page.getByText('Item').dblclick();
// Right click
await page.getByText('Item').click({ button: 'right' });
// Shift + click
await page.getByText('Item').click({ modifiers: ['Shift'] });
// Hover over element
await page.getByText('Item').hover();
// Click the top left corner
await page.getByText('Item').click({ position: { x: 0, y: 0 } });
4.1、强制点击
有时,应用使用不平凡的逻辑,其中悬停元素会将其与拦截点击的另一个元素重叠。 此行为与元素被覆盖且点击被分派到其他地方的错误没有区别。 如果你知道正在发生这种情况,则可以绕过 actionability 检查并强制单击:
await page.getByRole('button').click({ force: true });
4.2、程序化点击
如果你对在真实条件下测试应用不感兴趣,并且希望通过任何可能的方式模拟点击,则可以通过简单地使用 locator.dispatchEvent() 在元素上调度点击事件来触发 HTMLElement.click() 行为:
await page.getByRole('button').dispatchEvent('click');
5、输入字符
提醒:
大多数时候,你应该使用 locator.fill() 输入文本。 请参阅上面的 文本输入 部分。 仅当页面上有特殊键盘处理时才需要键入字符。
在字段中逐个字符输入,就好像用户使用 locator.pressSequentially() 的真实键盘一样。
// Press keys one by one
await page.locator('#area').pressSequentially('Hello World!');
此方法将发出所有必要的键盘事件,以及所有 keydown
、keyup
、keypress
事件。 你甚至可以在按键之间指定可选的 delay
以模拟真实的用户行为。
6、按键和快捷键
// Hit Enter
await page.getByText('Submit').press('Enter');
// Dispatch Control+Right
await page.getByRole('textbox').press('Control+ArrowRight');
// Press $ sign on keyboard
await page.getByRole('textbox').press('$');
locator.press() 方法聚焦所选元素并产生单个击键。 它接受在键盘事件的 keyboardEvent.key 属性中发出的逻辑键名称:
locator.press() 方法聚焦所选元素并产生单个击键。 它接受在键盘事件的 keyboardEvent.key 属性中发出的逻辑键名称:
英The locator.press() method focuses the selected element and produces a single keystroke. It accepts the logical key names that are emitted in the keyboardEvent.key property of the keyboard events:
Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape,
ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight,
ArrowUp, F1 - F12, Digit0 - Digit9, KeyA - KeyZ, etc.
- 你也可以指定要生成的单个字符,例如
"a"
或"#"
。 - 还支持以下修改快捷方式:
Shift, Control, Alt, Meta
。
简单版本产生单个字符。 该字符区分大小写,因此 "a"
和 "A"
将产生不同的结果。
// <input id=name>
await page.locator('#name').press('Shift+A');
// <input id=name>
await page.locator('#name').press('Shift+ArrowLeft');
还支持 "Control+o"
或 "Control+Shift+T"
等快捷方式。 当使用修饰符指定时,修饰符将被按下并在按下后续键的同时保持不变。
请注意,你仍然需要在 Shift-A
中指定大写 A
来生成大写字符。 Shift-a
产生一个小写字母,就像你切换了 CapsLock
一样。
7、上传文件
你可以使用 locator.setInputFiles() 方法选择要上传的输入文件。 它期望第一个参数指向类型为 "file"
的 输入元素。 可以在数组中传递多个文件。 如果某些文件路径是相对的,则它们将相对于当前工作目录进行解析。 空数组会清除选定的文件。
// Select one file
await page.getByLabel('Upload file').setInputFiles(path.join(__dirname, 'myfile.pdf'));
// Select multiple files
await page.getByLabel('Upload files').setInputFiles([
path.join(__dirname, 'file1.txt'),
path.join(__dirname, 'file2.txt'),
]);
// Remove all the selected files
await page.getByLabel('Upload file').setInputFiles([]);
// Upload buffer from memory
await page.getByLabel('Upload file').setInputFiles({
name: 'file.txt',
mimeType: 'text/plain',
buffer: Buffer.from('this is test')
});
如果你手头没有输入元素(它是动态创建的),你可以处理 page.on('filechooser') 事件或在操作时使用相应的等待方法:
// Start waiting for file chooser before clicking. Note no await.
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByLabel('Upload file').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'myfile.pdf'));
8、焦点元素
对于处理焦点事件的动态页面,你可以使用 locator.focus() 将给定元素聚焦。
await page.getByLabel('Password').focus();
9、拖放
你可以使用 locator.dragTo() 执行拖放操作。 该方法将:
- 将鼠标悬停在要拖动的元素上。
- 按鼠标左键。
- 将鼠标移动到将接收掉落的元素。
- 释放鼠标左键。
await page.locator('#item-to-be-dragged').dragTo(page.locator('#item-to-drop-at'));
9.1、手动拖动
如果你想精确控制拖动操作,请使用更底层的方法,例如 locator.hover()、mouse.down()、mouse.move() 和 mouse.up()。
await page.locator('#item-to-be-dragged').hover();
await page.mouse.down();
await page.locator('#item-to-drop-at').hover();
await page.mouse.up();
四、断言:
默认情况下,断言超时设置为 5 秒。 了解有关 各种超时 的更多信息。
1、自动重试断言:
以下断言将重试,直到断言通过或达到断言超时。 请注意,重试断言是异步的,因此你必须对它们进行 await
。
2、不重试断言
这些断言允许测试任何条件,但不会自动重试。 大多数时候,网页异步显示信息,并且使用非重试断言可能会导致不稳定的测试。 auto-retrying
尽可能首选自动重试断言。 对于需要重试的更复杂的断言,请使用 expect.poll 或 expect.toPass。
3、否定匹配器
一般来说,通过在匹配器前面添加 .not
,我们可以预期相反的情况成立:
expect(value).not.toEqual(0);
await expect(locator).not.toContainText('some text');
4、软断言
默认情况下,失败的断言将终止测试执行。 Playwright 还支持软断言: 失败的软断言 不要 终止测试执行,但将测试标记为失败。
// 做一些检查,当测试失败时不会停止。
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');
// …然后继续测试,检查更多的东西。
await page.getByRole('link', { name: 'next page' }).click();
await expect.soft(page.getByRole('heading', { name: 'Make another order' })).toBeVisible();
在测试执行期间的任何时候,你都可以检查是否存在任何软断言失败:
// Make a few checks that will not stop the test when failed...
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');
// Avoid running further if there were soft assertion failures.
expect(test.info().errors).toHaveLength(0);
请注意,软断言仅适用于 Playwright 测试运行程序。
5、自定义期望消息
你可以指定自定义错误消息作为 expect
函数的第二个参数,例如:
await expect(page.getByText('Name'), 'should be logged in').toBeVisible();
这同样适用于软断言:
expect.soft(value, 'my soft assertion').toBe(56);
6、expect.configure
你可以创建自己的预配置 expect
实例以拥有自己的默认值,例如 timeout
和 soft
。
const slowExpect = expect.configure({ timeout: 10000 });
await slowExpect(locator).toHaveText('Submit');
// 总是做soft的断言。
const softExpect = expect.configure({ soft: true });
await softExpect(locator).toHaveText('Submit');
7、expect.poll
你可以使用 expect.poll
将任何同步 expect
转换为异步轮询。
以下方法将轮询给定函数,直到返回 HTTP 状态 200:
await expect.poll(async () => {
const response = await page.request.get('https://api.example.com');
return response.status();
}, {
// 自定义错误消息,可选。
message: 'make sure API eventually succeeds', // 定制错误消息
// 投票10秒;默认为5秒。通过0来禁用超时。
timeout: 10000,
}).toBe(200);
你还可以指定自定义轮询间隔:
await expect.poll(async () => {
const response = await page.request.get('https://api.example.com');
return response.status();
}, {
// Probe, wait 1s, probe, wait 2s, probe, wait 10s, probe, wait 10s, probe
// ... Defaults to [100, 250, 500, 1000].
intervals: [1_000, 2_000, 10_000],
timeout: 60_000
}).toBe(200);
8、expect.toPass
你可以重试代码块,直到它们成功通过。
await expect(async () => {
const response = await page.request.get('https://api.example.com');
expect(response.status()).toBe(200);
}).toPass();
9、断言示例:
import { test, expect } from '@playwright/test';
test('搜索测试', async ({ page }) => {
// 访问掘金首页
await page.goto('https://juejin.cn/');
// 在搜索输入框中输入 Vue 并确认
await page.getByPlaceholder('探索稀土掘金').click();
await page.getByPlaceholder('搜索文章/小册/标签/用户').fill('Vue');
await page.getByRole('search').getByRole('img').click();
// 默认选中的标签应该是【综合】,并通过类名检查综合标签的选中样式是否符合预期
const common = page.getByRole('link', { name: '综合', exact: true })
await expect(common.locator('..')).toHaveClass(/router-link-exact-active/);
// 检查页面 url,路径:/search,查询参数:query=Vue&type=0
await expect(page).toHaveURL(/.*\/search\?query=Vue&type=0/);
// 在搜索结果页面切换类别标签
const article = page.getByRole('link', { name: '文章', exact: true });
await article.click();
await expect(article.locator('..')).toHaveClass(/router-link-exact-active/);
await expect(page).toHaveURL(/.*\/search\?query=Vue&type=2/);
const classes = page.getByRole('main').getByRole('link', { name: '课程', exact: true });
await classes.click();
await expect(classes.locator('..')).toHaveClass(/router-link-exact-active/);
await expect(page).toHaveURL(/.*\/search\?query=Vue&type=12/);
const tags = page.getByRole('link', { name: '标签', exact: true });
await tags.click();
await expect(tags.locator('..')).toHaveClass(/router-link-exact-active/);
await expect(page).toHaveURL(/.*\/search\?query=Vue&type=9/);
const users = page.getByRole('link', { name: '用户', exact: true });
await users.click();
await expect(users.locator('..')).toHaveClass(/router-link-exact-active/);
await expect(page).toHaveURL(/.*\/search\?query=Vue&type=1/);
});
五、定位器
定位器本身也是一种断言,也能起到等待的效果。当定位器暂时没选取到任何元素时,测试进程也会保持等待与重试,直到选中目标元素或者测试超时、用例失败。
1、选择定位器
locator
方法是定位元素最通用的方法,用法与 jquery 的 $
方法类似,接收 css 选择器 或 xpath 选择器,返回选中的元素列表。
import { test } from '@playwright/test';
test('test', async ({ page }) => {
// 选取 class 为 item 的元素
page.locator('.item')
// 选取 id 为 app 的 <div></div> 元素
page.locator('div#app')
// 支持 xPath 选择器
// 参考:https://playwright.dev/docs/other-locators#xpath-locator
// 参考:https://playwright.dev/docs/locators#locate-by-css-or-xpath
const target = page.locator('xpath=//button');
// xpath 选取父元素
target.locator('..')
})
2、语义定位
在定位 API 中还有一系列通过语义实现定位的方法。
import { test } from '@playwright/test';
test('test', async ({ page }) => {
/**
* 通过元素内部的文字定位元素
*
* https://playwright.dev/docs/locators#locate-by-text
*/
page.getByText('首页')
/**
* 通过 alt / title 属性定位元素
*
* https://playwright.dev/docs/locators#locate-by-alt-text
*
* https://playwright.dev/docs/locators#locate-by-title
*
* 例如:
*
* <img src="..." alt="稀土掘金" class="logo-img">
*
* <img src="..." title="稀土掘金" class="logo-img">
*/
page.getByAltText('稀土掘金');
page.getByTitle('稀土掘金');
/**
* 通过输入框的占位文本定位元素
*
* https://playwright.dev/docs/locators#locate-by-placeholder
*
* 例如:
*
* <input type="search" maxlength="64" placeholder="探索稀土掘金" class="search-input isResourceVisible">
*/
page.getByPlaceholder('探索稀土掘金');
/**
* 通过无障碍技术 ARIA 角色来定位元素
*
* https://playwright.dev/docs/locators#locate-by-role
*/
page.getByRole('link', { name: '首页' });
/**
* 通过约定好的测试 id 定位元素(推荐)
*
* https://playwright.dev/docs/locators#locate-by-test-id
*
* 例如:
*
* <button data-testid="submit-btn">Button</button>
*/
page.getByTestId('submit-btn');
});
语义定位比起选择器定位在使用上要简便一些,并且由于选择的依据更偏向于业务属性,不容易因为页面 DOM 结构的调整而导致定位失效。
其中,Locator.getByTestId
用起来比较方便,这种方式需要测试人员与前端开发人员沟通,为需要测试的元素加上 data-testid
属性,这样 Locator.getByTestId
就可以唯一地选中我们期望的对象。这种方式增加了前端人员与测试人员的沟通成本,但好处是不言而喻的。
3、设置自定义测试 id 属性
配置:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});
定义:
<button data-pw="directions">Itinéraire</button>
使用:
await page.getByTestId('directions').click();
六、前/后置操作钩子
前/后置操作钩子 前/后置操作钩子一般用于处理测试用例的公共逻辑,或设置用例之间共享的资源。
test.beforeEach
每个测试用例执行前触发test.afterEach
每个测试用例执行后触发test.beforeAll
所有测试用例执行前触发test.afterAll
所有测试用例执行完触发
import { test } from '@playwright/test';
// 全局钩子对每一个用例都生效
test.beforeEach(async ({ page }) => {
// 没个用例执行前先跳转到起始 url
await page.goto('https://juejin.cn/');
});
test.describe('group A', () => {
// 分组内的钩子只对当前分组中的用例生效
test.beforeEach(async ({ page }) => {
// ...
});
test('my test', async ({ page }) => {
// ...
});
// 在 my test 用例完成后执行,可以用来删除测试遗留数据
test.afterEach(async ({ page }) => {
// ...
});
});
七、案例
1、使用codegen,--save记住登录信息,下次直接使用load的时候可以直接跳过登录
存储登录信息
npx playwright codegen --save-storage=auth.json http://localhost:4000
使用登录信息,直接跳过登录页面
npx playwright codegen --load-storage=auth.json?http://localhost:4000
执行第一条--save命令的时候,你输入的登录信息,会记录在项目更目录下的auto.json文件中;
退出,再次输入第二条命令,会直接跳过登录;
2、登录测试鉴权
很多系统的功能都需要先完成用户登录后才能够使用。因此在端到端测试中,处理用户登录是一个经典的问题。
首先在 playwright.config.ts
中,定义一个新的测试工程 setup
,专门用于登录鉴权的初始化。其他正式的测试工程都必须在其后执行。
规定 .auth
目录存放浏览器缓存,setup
工程将登录后的浏览器缓存往其中写入,正式测试工程执行前将从中读出缓存。
playwright配置根目录:
// playwright.config.ts
baseURL: 'http://localhost:4000',
// playwright.config.ts
export default defineConfig({
// ...
projects: [
+ // setup 工程只执行 tests 目录下以 .setup.ts 结尾的文件。在所有正式测试执行前先完成鉴权初始化
+ { name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
- use: { ...devices['Desktop Chrome'] },
+ use: {
+ ...devices['Desktop Chrome'],
+ // setup 完成鉴权后,浏览器缓存状态会保存在此,正式的测试工程在执行前通过此文件恢复浏览器缓存,进而获取了用户登录态
+ storageState: '.auth/user.json',
+ },
+ // 必须在 setup 完成鉴权后执行
+ dependencies: ['setup'],
},
{
name: 'firefox',
- use: { ...devices['Desktop Firefox'] },
+ use: {
+ ...devices['Desktop Firefox'],
+ storageState: '.auth/user.json',
+ },
+ dependencies: ['setup'],
},
{
name: 'webkit',
- use: { ...devices['Desktop Safari'] },
+ use: {
+ ...devices['Desktop Safari'],
+ storageState: '.auth/user.json',
+ },
+ dependencies: ['setup'],
},
]
});
.auth
目录不应该入仓,应补充到 .gitignore
中。
# .gitignore
# ...
+/.auth
登录文件配置:也可以采用env来配置账号密码这些
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = '.auth/user.json';
setup('authenticate', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('#/login');
await page.getByPlaceholder('Please enter participant code').fill('CP004');
await page.getByPlaceholder('Please enter Username').fill('admin');
await page.getByPlaceholder('Please enter your password').fill('Aa@123456');
await page.getByRole('button', { name: 'Submit' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('#/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
// await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: authFile });
});
配置好了之后,就可以写其它测试案例了,会自动执行登录功能;
3、API接口测试
import { test } from '@playwright/test';
test('接口检查', async ({ page }) => {
// 访问掘金首页
await page.goto('https://juejin.cn/');
const promise = page.waitForResponse(
(res) => res.url().includes('/user_api/v1/author/recommend'));
const res = await promise;
const data = await res.json();
expect(data.err_no).toBe(0);
})
4、CM系统用户管理的增删查改功能测试案例
配置好鉴权、启动CM系统本地服务器,然后就可以执行这个案例了;
import { test,expect,Page } from '@playwright/test';
// 获取请求数据的方法
const getDataList = async (page:Page) => {
const promise = await page.waitForResponse((res) => {
return res.url().includes('/user/list');
});
const res = await promise;
const data = await res.json();
return data.list.length;
}
test.describe('新增用户表单测试', () => {
let startLength = 0, endLength = 0;
test.beforeEach(async ({ page }) => {
test.setTimeout(120000);
await page.goto('#/');
await page.locator('li').filter({ hasText: 'Administration' }).locator('i').first().click();
await page.getByText('User Management').click();
startLength = await getDataList(page);
});
test('不录入内容', async ({page}) => {
await page.getByRole('button', { name: 'Create User' }).click();
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(await page.getByText('Please enter your password')).toBeVisible();
await expect(await page.getByText('Please select user role')).toBeVisible();
await expect(await page.getByText('Please enter Username')).toBeVisible();
// await page.getByRole('button', { name: 'Cancel' }).click();
})
test('录入密码不符合规范', async ({page}) => {
await page.getByRole('button', { name: 'Create User' }).click();
await page.getByPlaceholder('Please enter your password').fill('Aa');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(await page.getByText('Minimum length of 8')).toBeVisible();
// await page.getByRole('button', { name: 'Cancel' }).click();
})
test('正确的录入,并新增用户', async ({page}) => {
await page.getByRole('button', { name: 'Create User' }).click();
await page.getByPlaceholder('Please enter your password').fill('Aa@123456');
await page.getByPlaceholder('Please select a role').click();
await page.getByRole('tooltip', { name: 'user' }).getByRole('listitem').click();
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByRole('button', { name: 'OK' }).click();
endLength = await getDataList(page);
console.log(2,'endLength',endLength)
expect(endLength - startLength).toBe(1);
})
test('删除用户', async ({ page }) => {
await page.getByRole('button', { name: 'Delete' }).first().click();
await page.getByRole('button', { name: 'Yes' }).click();
endLength = await getDataList(page);
expect(endLength - startLength).toBe(-1);
});
test('修改用户密码', async ({ page }) => {
await page.getByRole('button', { name: 'Reset password' }).first().click();
await page.getByPlaceholder('Please enter your password').fill('Aa@1234567');
await page.getByPlaceholder('Please enter Confirm Password').fill('Aa@1234567');
await page.getByLabel('Reset Password').getByRole('button', { name: 'Reset password' }).click();
});
})
5、组件测试
5.1、elememt-ui Select组件测试
// components/Components.ts
import { Page, Locator } from '@playwright/test';
export class Component {
protected page: Page;
protected locator: Locator;
/**
* @param page 组件所在的页面对象
* @param locator 如何定位组件的根元素,传入字符串通过 getByTestId 获取,或者直接传入一个 locator
*/
constructor(page: Page, locator: Locator | string = '') {
this.page = page;
if (typeof locator === 'string') {
this.locator = locator ? page.getByTestId(locator) : page.locator('body');
} else {
this.locator = locator ? page.locator('body').locator(locator) : page.locator('body');
}
}
}
接着建立 Select
类继承组件基类,正式处理下拉多选框的测试逻辑。这里用到了相对灵活的 Locate.evaluate 方法获取页面元素的实例对象,为下拉框(后续要被移动到 body 下)提前设置 testid
,方便之后的定位。
// components/Select.ts
import { Locator, expect } from '@playwright/test';
import { Component } from './Component';
export class Select extends Component {
/** 记录已选中的选项 */
selected = new Set<string>();
/** 用当前日期生成下拉框的 testid */
popupId = new Date().getTime().toString();
/** 是否激活下拉框 */
private isPopupInitialized = false;
/** 选择框主体,用于触发点击交互 */
get el() {
return this.locator.getByRole('textbox');
}
/**
* 激活下拉框,并且遍历指定的 option 选项
* @param selections 每个选项的文字
* @param callback 对每个遍历到的选项的处理方式
*/
private async traceSelections(
selections: string[],
callback: (data: SelectionData) => Promise<void>,
) {
if (!this.isPopupInitialized) {
/**
* 下拉框还未激活时,处于组件根元素内部,第一次点击激活后才会被移动到 body 中。
*
* 在此之前先完成定位,并通过 evaluate 方法提前给下拉框设置 testid,方便之后定位
*
* evaluate 方法说明:https://playwright.dev/docs/api/class-locator#locator-evaluate
*/
await this.locator.locator('.el-select-dropdown').evaluate((node, id) => {
node.dataset.testid = id;
}, this.popupId);
this.isPopupInitialized = true;
}
// 点击激活下拉框
await this.el.click();
// 用设置好的 testid 可以轻松定位下拉框
const popover = this.page.getByTestId(this.popupId);
for (let i = 0; i < selections.length; i++) {
const target = popover.getByText(selections[i], { exact: true });
const text = await target.textContent();
if (!text) {
throw new Error('Selection has not text content!');
}
// 定位到文字的父元素,即容器元素,容器元素上有各种类名,可以用来判断选项的状态
const wrapper = target.locator('..');
const isSelected = this.selected.has(text);
const classNames = await wrapper.getAttribute('class');
// 容器元素有 is-disabled 类代表不可选
const isDisabled = classNames ? classNames.includes('is-disabled') : false;
// 执行自定义处理方法
await callback({
target,
wrapper,
text,
isSelected,
isDisabled,
});
}
await this.el.blur();
}
/** 选中目标选项 */
async checkSelections(...selections: string[]) {
await this.traceSelections(selections, async ({
target, wrapper, text, isSelected, isDisabled,
}) => {
if (!isSelected) {
await target.click();
this.selected.add(text);
}
if (isDisabled) {
throw new Error('Option is disabled!');
}
// 通过容器是否具有 selected 类,判断是否成功选中选项
await expect(wrapper).toHaveClass(/selected/);
});
}
/** 取消目标选项 */
async uncheckSelections(...selections: string[]) {
await this.traceSelections(selections, async ({
target, wrapper, text, isSelected, isDisabled,
}) => {
if (isSelected) {
await target.click();
this.selected.delete(text);
}
if (isDisabled) {
throw new Error('Option is disabled!');
}
await expect(wrapper).not.toHaveClass(/selected/);
});
}
}
interface SelectionData {
target: Locator;
wrapper: Locator;
text: string;
isSelected: boolean;
isDisabled: boolean;
}
完成封装后,我们建立 tests/element.spec.ts
文件验证一下。
// tests/element.spec.ts
import { test } from '@playwright/test';
import { Select } from '../components/Select'
test('测试多选框', async ({ page }) => {
await page.goto('https://element.eleme.cn/#/zh-CN/component/select', { waitUntil: 'domcontentloaded' });
const target = page.locator('.el-select').nth(4);
const select = new Select(page, target);
await select.checkSelections('黄金糕', '双皮奶');
})
5.2、测试element ui表单功能
// components/Input.ts
import { Component } from './Component';
export class Input extends Component {
/** 定位 el-input 组件中的 <input> 原生元素 */
get el() {
return this.locator.getByRole('textbox');
}
/** 输入内容 */
async input(value: string) {
await this.el.fill(value);
}
}
// components/FormItem.ts
import { expect, Page, Locator } from '@playwright/test';
import { Component } from './Component';
export class FormItem<T extends Component> extends Component {
/** form-item 内部的输入元素,可以为任意输入组件 */
target: T;
constructor(
InputConstructor: typeof Component,
page: Page,
locator: Locator | string = '',
) {
super(page, locator);
this.target = new InputConstructor(page, locator) as T;
}
/** 定位到 el-form-item 的校验对象 */
get errorEl() {
return this.locator.locator('.el-form-item__error');
}
/** el-form-item 校验成功 */
async checkSuccess() {
await expect(this.locator).toHaveClass(/is-success/);
}
/** el-form-item 校验失败 */
async checkError(errTxt: RegExp | string = '') {
await expect(this.locator).toHaveClass(/is-error/);
if (errTxt) {
await expect(this.errorEl).toContainText(errTxt);
}
}
}
// components/index.ts
export * from './Component';
export * from './Input';
export * from './Select';
export * from './FormItem';
接着我们创建 views
目录,存放对页面的封装。同样要注意在 tsconfig.json
中的 include
字段中补充此目录。
与组件相同,我们定义页面的基类,对页面测试共有的逻辑进行封装。因为页面本身是复杂一些的组件,因此继承了组件类。
// views/View.ts
import { Page } from '@playwright/test';
import { Component } from '../components';
export class View extends Component {
protected url: string;
constructor(page: Page, url: string) {
super(page);
this.url = url;
}
/** 页面的访问操作时公共行为,适合放在基类中 */
async visit() {
await this.page.goto(this.url, { waitUntil: 'domcontentloaded' });
}
}
// views/LoginView.ts
import { Page } from '@playwright/test';
import { Input, FormItem } from '../components';
import { View } from './View'
export class LoginView extends View {
/** 页面的第五个案例作为我们的测试区域 */
get root() {
return this.page.locator('.source').nth(4)
}
/** 获取区域内所有的表单项 */
get formItems() {
return this.root.locator('.el-form-item');
}
/** 页面由组件组成 */
pwd = new FormItem<Input>(Input, this.page, this.formItems.nth(0));
confirmPwd = new FormItem<Input>(Input, this.page, this.formItems.nth(1));
age = new FormItem<Input>(Input, this.page, this.formItems.nth(2));
submitBtn = this.root.getByRole('button', { name: '提交' });
resetBtn = this.root.getByRole('button', { name: '重置' });
constructor(page: Page) {
super(page, 'https://element.eleme.cn/#/zh-CN/component/form');
}
/** 表单输入操作 */
async input(
pwd: string = '',
confirmPwd: string = '',
age: string = '',
) {
await this.pwd.target.input(pwd);
await this.confirmPwd.target.input(confirmPwd);
await this.age.target.input(age);
}
/** 表单输入并提交 */
async inputAndSubmit(
pwd: string = '',
confirmPwd: string = '',
age: string = '',
) {
await this.input(pwd, confirmPwd, age);
await this.submitBtn.click();
}
}
// views/index.ts
export * from './View';
export * from './LoginView';
// views/index.ts
export * from './View';
export * from './LoginView';
最后,在 tests/element.spec.ts
里面补充用例代码,可以看到 inputAndSubmit
方法高频被复用,代码非常简洁,语义也比较清晰。
// tests/element.spec.ts
import { test, expect } from '@playwright/test';
import { LoginView } from '../views';
// 省略前面的无关用例
// 新增用例
test.describe('测试表单', () => {
let view: LoginView
test.beforeEach(async ({ page }) => {
view = new LoginView(page);
await view.visit();
});
test('不同的密码', async () => {
await view.inputAndSubmit('123', '1234', '19');
await view.confirmPwd.checkError(/密码不一致/);
})
test('年龄不满18岁', async () => {
await view.inputAndSubmit('123', '123', '15');
await view.age.checkError(/18岁/);
})
test('存在未填项', async () => {
await view.inputAndSubmit('', '', '19');
await view.pwd.checkError(/输入密码/);
await view.confirmPwd.checkError(/输入密码/);
})
test('输入内容重置', async () => {
await view.input('123', '123', '19');
await view.resetBtn.click();
await expect(view.pwd.target.el).toHaveValue('');
await expect(view.confirmPwd.target.el).toHaveValue('');
await expect(view.age.target.el).toHaveValue('');
})
test('成功输入', async () => {
await view.inputAndSubmit('123', '123', '19');
await view.pwd.checkSuccess();
await view.confirmPwd.checkSuccess();
await view.age.checkSuccess();
})
})
创建一个vite+vue3的项目
pnpm create vite
按照playwright
pnpm create playwright@latest -- --ct
显示测试报告
pnpm exec playwright show-report
请通过运行 `npm i --save-dev @playwright/test` 安装 Playwright Test
快捷键如何打开?
主动更新快照(好像作用不大)
pnpm test-ct --update-snapshots
打开报告
pnpm test-ct --trace on自动记录用户在页面的操作,生成自动测试案例(缺点:只有操作,没有验证)
http://localhost:5175/:当前要记录的浏览器所在页面的地址(根据实际情况变化)
npx playwhite codegen http://localhost:3333
存储登录信息
npx playwright codegen --save-storage=auth.json http://localhost:3333
使用登录信息,直接跳过登录页面
npx playwright codegen --load-storage=auth.json?http://localhost:3333