Playwright 是一个强大的前端自动化测试工具。
一、跨浏览器支持
二、强大的自动化能力
三、易于使用和集成
pnpm dlx create-playwright
e2e-auto
┣ node_modules
┣ tests
┃ ┗ example.spec.ts
┣ tests-examples
┃ ┗ demo-todo-app.spec.ts
┣ .gitignore
┣ package.json
┣ playwright.config.ts
┗ pnpm-lock.yaml
npx playwright install
npx playwright test
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 测试目录
testDir: './tests',
// 是否并发运行测试
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
// 测试失败用例重试次数
retries: process.env.CI ? 2 : 0,
// 测试时使用的进程数,进程数越多可以同时执行的测试任务就越多。不设置则尽可能多地开启进程。
workers: process.env.CI ? 1 : undefined,
// 指定测试结果如何输出
reporter: 'html',
// 测试 project 的公共配置,会与与下面 projects 字段中的每个对象的 use 对象合并。
use: {
// 测试时各种请求的基础路径
baseURL: 'http://127.0.0.1:3000',
// 生成测试追踪信息的规则,on-first-retry 意为第一次重试时生成。
trace: 'on-first-retry',
},
// 定义每个 project,示例中将不同的浏览器测试区分成了不同的项目
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
以下是一个简单的 Playwright 测试示例:
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects the URL to contain intro.
await expect(page).toHaveURL(/.*intro/);
});
// test('测试环境', async ({ page }) => {
// // 与 playwright.config.ts 一样,测试脚本中也可以访问环境变量
// await page.goto('/');
// const url = await page.url();
// if(process.env.TEST_MODE === 'production') {
// await expect(url).toBe('https://github.com/')
// } else {
// await expect(url).toBe('https://gitee.com/')
// }
// });
// playwright.config.ts
export default defineConfig({
// 指定测试产物(追踪信息、视频、截图)输出路径
outputDir: 'test-results',
//...
// reporter: 'html',
reporter: [
// 在命令行中同步打印每条用例的执行结果
['list'],
// 输出 html 格式的报告,并将报告归档与指定路径
['html', {
outputFolder: 'playwright-report',
}],
],
// ...
use: {
//...
// 非 CI 环境下,第一次失败重试时生成追踪信息。非 CI 环境下,总是生成追踪信息
trace: process.env.CI ? 'on-first-retry' : 'on',
// 非 CI 环境下,第一次失败重试时生成视频。非 CI 环境下,总是生成视频
video: process.env.CI ? 'on-first-retry' : 'on',
},
});
借鉴 Vite 的 环境变量 的解决方案,在测试工程目录中建立多个环境变量文件:
e2e-auto
┣ ...
┣ .env # 所有情况下都会加载
┣ .env.dev # 本地开发环境下加载
┣ .env.test # 测试环境下加载
┣ .env.test.local # 测试环境下加载,但是只在本地有效不会入仓,可以用于存放一些不该入仓的敏感配置。其他环境也可以有 .local 配置文件。
┗ .env.production # 生产环境下加载
pnpm i -D cross-env
{// ...
"scripts": {
"test:development": "cross-env TEST_MODE=development playwright test",
"test:test": "cross-env TEST_MODE=test playwright test",
"test:production": "cross-env TEST_MODE=production playwright test"
}
}
pnpm i -D dotenv
// playwright.config.ts
import dotenv from 'dotenv';
// TEST_MODE 的值决定了加载哪个环境的文件
const modeExt = process.env.TEST_MODE || 'development';
// 先加载入仓的配置文件,再加载本地的配置文件
dotenv.config({ path: '.env' });
dotenv.config({ path: `.env.${modeExt}`, override: true });
dotenv.config({ path: '.env.local', override: true });
dotenv.config({ path: `.env.${modeExt}.local`, override: true });
export default defineConfig({
// ...
});
我们可以验证一下效果,将 test 环境中的 url 设置为码云,将 production 环境中的 url 设置为 Github:
# .env.test
WEBSITE_URL = https://gitee.com/
# .env.production
WEBSITE_URL = https://github.com/
分别运行 pnpm run test:test --ui 和 pnpm run test:production --ui
我们使用 Playwright 来打开 Chromium 浏览器,访问一个示例网站,并检查页面标题是否正确。下面我们来说说一些使用Playwright的常见操作。
一、滚动到页面底部
可以使用 page.evaluate
方法结合 JavaScript 来滚动到页面底部。
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
二、滚动到特定元素
const element = await page.$('#your-element-id');
await element.scrollIntoViewIfNeeded();
三、模拟滚动行为
可以使用 page.mouse
来模拟鼠标滚轮滚动。
await page.mouse.wheel(0, 100); // 垂直方向向下滚动 100 像素
四、在特定的框架内滚动
如果页面中有 iframe,可以先切换到该 iframe,然后进行滚动操作。
const frame = await page.frameLocator('iframe[src="your-frame-url"]');
await frame.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
一、使用**async/await
**
Playwright 的 API 通常返回 Promise 对象,所以可以很自然地使用async/await
来处理异步操作。例如:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const element = await page.$('some-selector');
if (element) {
// 对元素进行操作
}
await browser.close();
})();
二、等待特定条件
page.waitForSelector
等待特定元素出现,page.waitForNavigation
等待页面导航完成等。 await page.waitForSelector('some-selector');
page.waitForFunction
来执行自定义的 JavaScript 函数,直到该函数返回true
。 await page.waitForFunction(() => {
return document.querySelector('some-selector') && document.querySelector('some-selector').innerText === 'expected text';
});
三、处理多个异步操作的顺序
async
函数中依次使用await
。 await operation1();
await operation2();
await operation3();
Promise.all
并行执行它们,以提高效率。 const [result1, result2] = await Promise.all([operation1(), operation2()]);
四、错误处理
try/catch
块来捕获异步操作中的错误。 try {
await someAsyncOperation();
} catch (error) {
console.error('Error occurred:', error);
}
finally
块中执行资源释放操作。 let browser;
try {
browser = await chromium.launch();
// 其他操作
} catch (error) {
console.error('Error occurred:', error);
} finally {
if (browser) {
await browser.close();
}
}
添加.github/workflows
playwright.yml
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.36.0-jammy
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm i
- name: Run Playwright tests
run: npm run e2e:ci
# - name: Install Playwright Browsers
# run: npx playwright install --with-deps
# - name: Run Playwright tests
# run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
配置json e2e:ci:result
// ...
"scripts": {
"test:development": "cross-env TEST_MODE=development playwright test",
"test:test": "cross-env TEST_MODE=test playwright test",
"test:production": "cross-env TEST_MODE=production playwright test",
"e2e": "playwright test",
"e2e:ci": "cross-env CI=1 CI_WORKERS=1 yarn e2e:ci:run",
"e2e:ci:run": "playwright test",
"e2e:ci:result": "ts-node ./ci/ci-result.ts",
"e2e:report": "playwright show-report"
},
// ...
ci-result.ts
/* eslint-disable no-console */
import fs from 'fs';
import os from 'os';
import path from 'path';
import {minimatch} from 'minimatch';
import { fileURLToPath } from 'url'
const __filenameNew = fileURLToPath(import.meta.url)
const __dirnameNew = path.dirname(__filenameNew)
// import axios from 'axios';
import type { JSONReport, JSONReportSuite } from '@playwright/test/reporter';
// @ts-ignore
import { getMiniReportTitle, getModuleMatches, getModuleSource } from './ci-module-map.ts';
const reportDir = path.resolve(__dirnameNew, '../playwright-report');
const resultJsonFile = path.resolve(reportDir, 'results.json');
const miniTestExportJsonFile = path.resolve(reportDir, 'mini_test_report.json');
const resultExportFile = path.resolve(reportDir, 'results-export.sh');
// const postHost = '';
// const ignorePostPassRate = !!process.env.E2E_IGNORE_POST_PASS_RATE;
const readResultJson = (): JSONReport | undefined => {
if (!fs.existsSync(resultJsonFile)) {
return;
}
return JSON.parse(fs.readFileSync(resultJsonFile, 'utf8'));
};
const summaryPassRateShell = (resultJson: JSONReport, extraShellText: string) => {
if (os.platform() === 'win32') {
return;
}
let passed = 0;
let failed = 0;
const walkSuites = (suites?: JSONReportSuite[]) => {
suites?.forEach(suite => {
suite.specs.forEach(spec => {
// 用例执行结果
const { ok } = spec;
if (ok) {
passed += 1;
} else {
failed += 1;
}
});
walkSuites(suite.suites);
});
};
walkSuites(resultJson.suites);
const total = passed + failed;
const passedPercent = total ? `${((passed * 100) / total).toFixed(1)}%` : 'NaN%';
const passedRate = total ? (passed / total).toFixed(3) : 0;
console.log(`passed ${passed}, failed ${failed}, passed percent ${passedPercent}`);
fs.writeFileSync(
resultExportFile,
`
#!/bin/sh
export E2E_TEST_RESULT_PASSED=${passed}
export E2E_TEST_RESULT_FAILED=${failed}
export E2E_TEST_RESULT_PASSED_PERCENT=${passedPercent}
export E2E_TEST_RESULT_PASSED_RATE=${passedRate}
${extraShellText}
`,
);
};
// 兼容 mini-test-report html
type MiniTestResult = {
caseId: string;
caseName: string;
casePath: string;
caseLocation?: string;
fullName: string;
replayResult: 'failed' | 'passed';
replayResults: [];
duration?: number;
};
type ModuleMatch = ReturnType<typeof getModuleMatches>[0];
type ModuleResult = ModuleMatch & {
passed: number;
failed: number;
testResults: MiniTestResult[];
};
const findMatchModule = (suite: JSONReportSuite, moduleResults: ModuleResult[]) => {
for (const module of moduleResults) {
const { patterns } = module;
// 最后一个
if (!patterns) {
return module;
}
// 正常匹配
const matched = patterns.some(pattern => minimatch(suite.file, pattern));
if (matched) {
return module;
}
}
return null;
};
const reportModulePassRate = async (resultJson: JSONReport, moduleResults: ModuleResult[]) => {
// 统计 passed failed
const walkSuites = (suites?: JSONReportSuite[]) => {
suites?.forEach(suite => {
const module = findMatchModule(suite, moduleResults);
if (!module) {
return;
}
suite.specs.forEach(spec => {
// 用例执行结果
const { id, title, ok, file, line, column, tests } = spec;
if (ok) {
module.passed += 1;
} else {
module.failed += 1;
}
const results = tests[tests.length - 1]?.results || [];
const testResult: MiniTestResult = {
caseId: id,
caseName: title,
casePath: file,
caseLocation: `${file}:${line}:${column}`,
fullName: title,
replayResult: ok ? 'passed' : 'failed',
replayResults: [],
duration: results[results.length - 1]?.duration,
};
module.testResults.push(testResult);
});
walkSuites(suite.suites);
});
};
walkSuites(resultJson.suites);
// 生成结果reporter json
fs.writeFileSync(
miniTestExportJsonFile,
JSON.stringify({ type: 'playwright', title: getMiniReportTitle(), moduleResults }, undefined, 2),
);
// 上报 modulePassMap
console.log('moduleResults', moduleResults);
const postData = moduleResults
.map(module => {
const { failed, passed, teamTag, expectTotal } = module;
const total = passed + failed;
const passedRate = total ? passed / total : 0;
return {
tag: teamTag,
module: module.name,
caseCount: total,
autoTestCaseCount: total,
autoTestCasePassCount: passed,
passRate: passedRate,
source: getModuleSource(),
expectTotal: Math.max(expectTotal || 0, total),
};
})
.filter(res => res.caseCount > 0);
console.log('Post data', postData);
// if (ignorePostPassRate) {
// return;
// }
// return axios
// .post(postHost, {
// business: '',
// type: '',
// data: postData,
// })
// .then(
// _res => console.log('Post grafana dashboard passed rate: OK'),
// e => {
// console.log('Post grafana dashboard passed rate: FAILED', e);
// throw e;
// },
// );
};
const run = async () => {
const resultJson = readResultJson();
if (!resultJson) {
return;
}
const moduleResults: ModuleResult[] = getModuleMatches().map(m => ({
...m,
passed: 0,
failed: 0,
testResults: [],
}));
let extraShellText = 'export E2E_TEST_RESULT_POST_PASS_RATE=OK\n';
try {
await reportModulePassRate(resultJson, moduleResults);
} catch (e) {
extraShellText = 'export E2E_TEST_RESULT_POST_PASS_RATE=FAIL\n';
}
summaryPassRateShell(resultJson, extraShellText);
};
run();
最后Playwright还有很多强大的功能,以上是简单介绍,感兴趣的小伙伴可以学习使用,当作横向知识储备。测试CI Demo代码地址自动化测试结果,还可以保存录屏回放。同时VSCode也有相应Playwright的插件Playwright Test for VSCode
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。