继续这一系列的小技巧,在 #1、#2 和 #3 大获成功后,迎来了我们的最新内容 #4。希望大家喜欢,别忘了也要阅读代码片段中的注释。
- 如何拦截相同路由的多个请求?
我记得在 Cypress 中,这个功能非常容易实现,但在 Playwright 中,要找到这种特定场景的详细信息并不那么简单。想象一下,你在浏览一个 Web 应用时,想验证多个具有相同路由的请求。例如,当你打开产品页面时,执行了一个 API 调用到 api/id/1
来获取数据,然后稍后你需要拦截同样的 api/id/1
调用。如何拦截两次请求并验证它们的数据呢?
你可以尝试使用 waitForRequest()
,但有一个更通用的方式,这将为你打开更多可能性:
import { test } from "@playwright/test";
test("验证相同路由的多次调用", async ({ page }) => {
const requests: string[] = [];
// 使用 route 来区分网络请求
await page.route("**/api/**", async (route) => {
// 使用 url().includes 来过滤出你需要的特定调用
if (route.request().url().includes("id/1")) {
await route.continue();
const response = await (await route.request().response())?.json();
requests.push(response);
} else await route.continue();
});
// 上面的代码相当于设置了一个监听器,放在触发网络调用之前
await page.goto("www.yourapp.com");
// 假设这一步触发了第一次 id/1 调用
await product.click();
// 这一步触发了第二次 id/1 调用
await productExtraDetails.click();
// 现在你可以等待它们并做任何需要的断言
await expect(async () => {
expect(requests.length).toBe(2);
}).toPass({
intervals: [1_000, 2_000, 5_000],
timeout: 15_000,
});
});
// 以下是一个额外步骤
// 因为有时候你的测试会比应用的响应更快完成,所以需要取消路由
test.afterEach("取消所有路由", async ({ page }) => {
await page.unrouteAll({ behavior: "wait" });
});
这种场景最常见的用途是验证追踪器。
- 在断言元素数量之前,不要使用
.all()
假设你有一个选择器会返回多个元素,你想遍历这些元素并断言某些值。如果不先断言元素的数量,你的测试可能会出现误报。原因如下:
如果你有 3 个元素,并且执行了以下操作:
import { test } from "@playwright/test";
test("在 .all() 上的误报", async ({ page }) => {
const expectedValues = ["a", "b", "c"];
// 其他操作
const allElements = await page.getByTestId(locator).all();
for (const item of allElements) {
await expect(item).toHaveAttribute("any-attribute", expectedValues);
}
});
测试会按预期工作。然而,如果你的定位器找不到页面上的任何元素,测试仍然会通过。因为 .all()
在数量为零时不会报错,并且如果没有元素,forEach
也不会运行。
为了正确处理这个问题,你必须在迭代之前先断言元素的数量。记得使用自动重试的 toHaveCount
,而不是像 (allElements.length).toBe(3)
这样的代码。
import { test, expect } from "@playwright/test";
test("正确使用 .all()", async ({ page }) => {
const expectedValues = ["a", "b", "c"];
// 其他操作
await expect(page.getByTestId(locator)).toHaveCount(5);
const allElements = await page.getByTestId(locator).all();
for (const item of allElements) {
await expect(item).toHaveAttribute("any-attribute", expectedValues);
}
});
即使是 Playwright 官方文档也警告过 .all()
的使用问题,但现在你知道一个具体场景,如果在使用 .all()
之前没有断言数量,会导致错误。
专业提示:在 Playwright 的 async/await
风格中,不推荐使用 forEach
。使用 for...of
可以避免问题。
- 在点击之前无需断言
toBeVisible
我觉得有必要说这个,因为我见到这种情况太多次了。大家习惯在与元素交互之前断言其可见性,这是在任何教程或课程中学到的一种行为,曾经是每个人必须学习的重要步骤。但是在 Playwright 中,有一个特定场景不需要这样做,那就是点击 (click()
) 操作。下面是 Playwright 在点击元素之前所做的事情:
现在这些被称为可操作性检查 (actionability checks
),它们不仅限于点击操作。
一个关键点是,不要将其与 toHaveText
的断言混淆。使用 toHaveText
时要小心,因为 toHaveText
不会先等待元素可见。
- 如何只运行我在开发的测试?
大多数关注 Playwright 更新的人已经知道这个功能,但仍然有部分人不知道你可以使用一个特殊的 CLI 命令,只运行你修改过的测试。
假设你执行了 git pull
,然后你创建了自己的分支来开始开发一些测试,但你修复了多个测试中的错误,甚至创建了多个新的测试。那么与其像这样运行测试:
npx playwright test path/to/test1.spec.ts path/to/test2.spec.ts path/to/test3.spec.ts path/to/test4.spec.ts path/to/test5.spec.ts path/to/test6.spec.ts path/to/test7.spec.ts
你可以这样做:
npx playwright test --only-changed=main
这将对比你的分支与主分支,并运行在你的分支中被修改过的所有测试。
- 如何在 Playwright 中验证表格数据?
假设你有一个表格,需要验证渲染的值。这里不是验证某个文本是否存在于任意单元格中,而是验证某个值是否出现在指定列和指定行的精确单元格中。
你可以通过创建一个辅助函数来实现这个目标。为了方便大家自己运行下面的代码进行测试,我将所有内容都放在了一个文件中,当然你可以按照自己的方式存储工具/辅助函数。
这个辅助方法如何工作呢?你给它三个参数,第一个是表头的文本(列),第二个是你要验证的行,第三个是你想要的单元格中的期望文本。
import { test, expect } from "@playwright/test";
class WebTableHelper {
constructor(page) {
this.page = page;
}
async validateCellValueReferenceToHeader(
headerText,
rowToValidate,
expectedValue
) {
const elementsOfHeader = await this.page
.getByRole("table")
.getByRole("row")
.first()
.getByRole("cell")
.all();
const elementsOfNRow = await this.page
.getByRole("table")
.getByRole("row")
.nth(rowToValidate)
.getByRole("cell")
.all();
for (let i = 0; i < elementsOfHeader.length; i++) {
const headerValueFound = await elementsOfHeader[i].innerText();
if (headerValueFound.toLowerCase() === headerText.trim().toLowerCase()) {
const cellValue = await elementsOfNRow[i].innerText();
expect(cellValue).toBe(expectedValue);
return;
}
}
throw new Error(`未找到表头文本 "${headerText}"`);
}
}
test("测试表格", async ({ page }) => {
await page.goto("https://cosmocode.io/automation-practice-webtable");
const helper = new WebTableHelper(page);
await expect(page.getByRole("table").getByRole("row")).toHaveCount(197);
await helper.validateCellValueReferenceToHeader("Currency", 4, "Euro");
});
可能还需要一些数据清理和修剪,具体取决于表格中的数据。但对于大多数情况,这个方法可以开箱即用。
最初的代码其实是由 Srdan Mutlak 创建的。所以,功劳归于他。他挑战我创建类似的东西,他先给了我他的代码版本,然后我尝试用自己的风格实现。
现在我也来挑战你。你能让它变得更好吗?创建一个 Github gist 并在评论中写下你的想法。
- 如何在 Playwright 中获取元素的背景色?
我看到很多例子中,人们会用类似下面的代码来获取背景颜色:
const color = await btn.evaluate((element) =>
window.getComputedStyle(element).getPropertyValue("background-color")
);
expect(color).toBe("rgb(69, 186, 75)");
但其实不需要再这样做了,我们可以直接用:
await expect(locator).toHaveCSS('background-color',"rgb(69, 186, 75)");