安装
# 初始化安装
npm init playwright@latest
# 更新playwright
npm install -D @playwright/test@latest
# 安装浏览器
npx playwright install --with-deps
执行测试
# 运行 Playwright 测试套件中的所有测试
npx playwright test
# 在ui模式
npx playwright test --ui
# 运行测试并启用追踪功能
npx playwright test --trace on
# 展示最近一次测试运行的报告
npx playwright show-report
# 运行测试生成器
npx playwright codegen <url>
导航
// 导航到指定页面
await page.goto("https://juejin.cn/");
// 等待导航到指定页面 大多数情况可以省略 因为框架会自动适应
await page.waitForURL("**/frontend");
// 导航到指定页面,点击交互,但是页面打开新选项卡或窗口
const popupPromise = page.waitForEvent("popup");
await page.goto("https://www.baidu.com/");
await page.getByRole("link", { name: "新闻" }).click();
const newPage = await popupPromise;
await newPage.waitForLoadState("domcontentloaded");
console.log(await newPage.title());
定位元素
这些是推荐的内置定位器:
page.getByRole()
通过显式和隐式可访问性属性进行定位。page.getByText()
按文本内容定位。page.getByLabel()
通过关联标签的文本定位表单控件。awpage.getByPlaceholder()
通过占位符定位输入。page.getByAltText()
通过文本替代来定位元素(通常是图片)。page.getByTitle()
通过标题属性来定位元素。page.getByTestId()
根据data-testid
属性定位元素(可以配置其他属性)。
示例代码:
test("测试定位器 - 使用百度", async ({ page }) => {
await page.goto("https://www.baidu.com/");
await page.getByRole("link", { name: "新闻" }).focus();
await page.getByText("换一换").click();
await page.getByRole("link", { name: "登录" }).click();
await page.getByLabel("阅读并接受").locator("visible=true").click();
await page.getByPlaceholder("手机号/用户名/邮箱").fill("13111111111");
await page.getByTitle("微信").click();
});
test("测试定位器 - 自定义页面", async ({ page }) => {
await page.goto("http://127.0.0.1:5500/demo.html");
await page.getByTestId("testid").click();
expect(await page.getByTestId("testid").getAttribute("class")).toBe("red-bg");
await page.getByAltText("test").click();
expect(await page.getByAltText("test").getAttribute("style")).toContain(
"border: 10px solid red"
);
});
除了使用推荐的定位器,我们还可以直接使用page.locator
结合 css 选择器进行定位。
示例代码:
test("测试locator定位器 - 自定义页面", async ({ page }) => {
await page.goto("http://127.0.0.1:5500/demo.html");
// 使用css选择器
await page.locator(".test-class").click();
// 使用id选择器
await page.locator("#test-id").click();
// 使用css选择器
await page.locator("css=button").first().click();
// 使用标签选择器
await page.locator("button").first().click();
// 使用has-text选择器
await page.locator("button:has-text('submit')").click();
// 使用text选择器
await page.locator("button:text('submit')").click();
// 使用后代选择器
await page.locator(".btn-wrapper :has-text('submit')").click();
// 使用nth选择器
await page.locator(".btn-wrapper2 button").locator("nth=1").click();
// 匹配可见元素
await page.locator(".btn-wrapper2 button:visible").click();
await page.waitForTimeout(5000);
});
交互
上面我们我们讲述了如何定位到元素,接下来我们需要了解元素的交互。
常见的交互有:
locator.check()
检查输入复选框locator.uncheck()
取消选中输入复选框locator.hover()
将鼠标悬停在元素上locator.fill()
填写表单字段,输入文本locator.focus()
聚焦元素locator.click()
单击该元素locator.press()
按单个键locator.setInputFiles()
选择要上传的文件locator.selectOption()
在下拉菜单中选择选项
示例代码:
test("测试交互 - 自定义页面", async ({ page }) => {
await page.goto("http://127.0.0.1:5500/demo.html");
await page.locator(".interaction").getByRole("checkbox").check();
await page.locator(".interaction").getByRole("checkbox").uncheck();
await page.locator(".interaction").getByTestId("hover").hover();
await page
.locator(".interaction")
.getByTestId("fill")
.locator("input")
.fill("test");
await page
.locator(".interaction")
.getByTestId("fill")
.locator("input")
.focus();
await page
.locator(".interaction")
.getByTestId("setInputFiles")
.locator("input")
.setInputFiles(path.join(__dirname, "test.txt"));
await page
.locator(".interaction")
.getByTestId("selectOption")
.locator("select")
.selectOption("option2");
await page.waitForTimeout(5000);
});
断言
断言用于验证选定元素的实际状态是否与预期结果相符。
常见的断言有:
test("测试断言 - 自定义页面", async ({ page }) => {
await page.goto("http://127.0.0.1:5500/demo.html");
// 自动重试断言 - 将重试,直到断言通过或达到断言超时
// 容器是空的
await expect(page.getByTestId("toBeEmpty")).toBeEmpty();
// 元素可见
await expect(page.getByTestId("toBeVisible")).toBeVisible();
// 元素包含文本
await expect(page.getByTestId("toContainText")).toContainText(
"toContainText"
);
// 元素有属性
await expect(page.getByTestId("toContainText")).toHaveAttribute(
"data-testid",
"toContainText"
);
// 元素有文本 - 需要准确匹配,可以使用正则
await expect(page.getByTestId("toContainText")).toHaveText("toContainText");
// 输入元素有值
await expect(page.getByTestId("toHaveValue")).toHaveValue("test");
// 不重试断言 - 允许测试任何条件,但不会自动重试
expect(1 + 1).toBe(2);
expect(1 + 1).toBeDefined();
expect(1 + 1).toBeTruthy();
expect(1 + 1).toBeGreaterThan(1);
expect(1 + 1).toBeLessThan(3);
// 否定匹配器 -通过在匹配器前面添加`.not`,预期相反的情况成立
expect(1 + 1).not.toBeNull();
expect(1 + 1).not.toBeUndefined();
expect(1 + 1).not.toBeFalsy();
expect(1 + 1).not.toBeNaN();
await expect(page.getByTestId("toHaveValue")).not.toHaveValue("test1");
// 软断言 - 断言失败不会抛出错误,而是记录在案,继续执行
await expect.soft(page.getByTestId("toHaveValue")).toHaveValue("test1");
console.log("++++++++++++++++++++first");
// 软断言
});
鉴权
详细内容可以参考官方文档Authentication 部分。有点复杂。
我个人建议直接使用test.beforeAll
/test.beforeEach
。方便省事。
参考
附录
下面是教程用到的demo.html
的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
font-size: 30px;
}
.red-bg {
background-color: red;
}
[data-testid="hover"]:hover {
color: red;
}
</style>
</head>
<body>
<div>
<div data-testid="testid" onclick="this.classList.toggle('red-bg')">
<h1>TEST ID</h1>
</div>
<div>
<img
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
alt="test"
style="width: 400px; height: 400px"
onclick="this.style.border='10px solid red'"
/>
</div>
<div class="test-class" onclick="this.classList.toggle('red-bg')">
test-class
</div>
<div id="test-id" onclick="this.classList.toggle('red-bg')">test-id</div>
<div class="btn-wrapper">
<button onclick="console.log('submit')">submit</button>
</div>
<div class="btn-wrapper2">
<button onclick="console.log('button2')" style="visibility: hidden">
button2
</button>
<button onclick="console.log('button1')">button1</button>
</div>
<div class="interaction">
<label>
多选框
<input type="checkbox" />
</label>
<div data-testid="hover">hover</div>
<div data-testid="fill">
<input type="text" />
</div>
<div data-testid="setInputFiles">
<input type="file" />
</div>
<div data-testid="selectOption">
<select id="dropdown">
<option value="option1">选项 1</option>
<option value="option2">选项 2</option>
<option value="option3">选项 3</option>
</select>
</div>
</div>
<div data-testid="toBeEmpty"></div>
<div data-testid="toBeVisible">toBeVisible</div>
<div data-testid="toContainText">toContainText</div>
<input type="text" data-testid="toHaveValue" value="test" />
</div>
<div style="height: 500px"></div>
</body>
</html>