如何在 Playwright 中创建动态定位器(locator)?
· 1,123 词 · 6 分钟 读完 playwright进阶 翻译
这是一个在 Playwright Discord 帮助频道中经常被问到的热门问题。我认为这个问题如此受欢迎的主要原因之一是,没有人想为同一操作反复编写相似的代码,因为每次都只是输入略有不同,写重复的代码会显得比较啰嗦。处理这个问题有很多不同的方法,我将分享我的方法。
我使用的示例网站并不像其他网站那样直观,因为我将从网络请求中获取一个唯一 ID,并将其传递给动态定位器。我测试的网站是 https://practicesoftwaretesting.com。
在这个网站上,页面从 API 接口获取数据,并填充了一个可以交互的产品网格。我想创建一个动态定位器,允许我传入一个参数,根据传入的内容动态选择不同的元素。下面是"Combination Pliers"项目的 HTML 代码。
<a
class="card"
style="text-decoration: none; color: black"
data-test="product-01H8ABYWDYXDAYW9HM37N5FC5F"
href="#/product/01H8ABYWDYXDAYW9HM37N5FC5F"
></a>
<div class="card-img-wrapper">
<img class="card-img-top" src="assets/img/products/pliers01.jpeg" />
</div>
<div class="card-body">
<h5 data-test="product-name" class="card-title">Combination Pliers</h5>
</div>
<div class="card-footer">
<span class="float-end text-muted">
<span data-test="product-price">$14.15</span>
</span>
</div>
在这个例子中,我们将使用链接上的 data-test
属性,其值为 product-{unique_id}
。我们将构建一个动态定位器,允许我们传入 unique_id
来与我们想要交互的项目进行交互。下面的代码出现在我的 homePage.ts
文件中。我将 productId
设置为一个动态定位器。请看下面的使用示例。
// lib/pages/homePage.ts
import { Page } from "@playwright/test";
export class HomePage {
readonly productId = (id: string) =>
this.page.locator(`[data-test="product-${id}"]`);
readonly addToCart = this.page.locator('[data-test="add-to-cart"]');
readonly navCart = this.page.locator('[data-test="nav-cart"]');
async goto() {
await this.page.goto("/#/");
}
constructor(private readonly page: Page) {}
}
在 spec 文件中,我们需要实际拥有要传入的 unique_id
。await homePage.productId(productId).click();
为了获取唯一的 productId,我使用 page.route()
来拦截网络流量并在我的测试中使用这些值。你可以在这里阅读一个简单的例子以了解更多信息。
// tests/checkout.spec.ts
import { expect } from "@playwright/test";
import { test, CheckoutPage, HomePage } from "@pages";
import { getLoginToken } from "@datafactory/login";
import { productIdRoute } from "@fixtures/productPageRoute";
test.describe("Basic UI Checks", () => {
const username = process.env.CUSTOMER_01_USERNAME || "";
const password = process.env.CUSTOMER_01_PASSWORD || "";
let productId;
test.beforeEach(async ({ page }) => {
// 通过API调用获取登录token
const token = await getLoginToken(username, password);
// 在本地存储中设置登录令牌,使用户保持登录状态
await page.addInitScript((value) => {
window.localStorage.setItem("auth-token", value);
}, token);
productId = await productIdRoute(page);
});
test("Add to Cart and Checkout", async ({ page }) => {
const homePage = new HomePage(page);
const checkoutPage = new CheckoutPage(page);
await homePage.goto();
await homePage.productId(productId).click();
await homePage.addToCart.click();
await homePage.navCart.click();
await checkoutPage.proceed1.click();
...
});
});
在这个例子中,我用异步方法实现了 page.route()
。
// lib/fixtures/productPageRoute.ts
import { HomePage } from "@pages";
/**
* 设置一个路由来从API响应中检索产品ID。
* @param page - Playwright页面对象。
* @param name - 可选的字符串名称,用于获取特定ID。
* @returns 产品ID。
* @example
* const page = await browser.newPage();
* const productId = await productIdRoute(page); // 获取第二个产品ID
*
* const productId = await productIdRoute(page, "Pliers") // 获取名为"Pliers"的产品ID
*/
export async function productIdRoute(page: any, name?: string) {
let productId;
await page.route(
"https://api.practicesoftwaretesting.com/products?between=price,1,100&page=1",
async (route) => {
let body;
const response = await route.fetch();
body = await response.json();
if (name) {
productId = findIdByName(body, name);
console.log("pid: " + productId);
} else {
// 获取列表中的第二个产品
productId = body.data[1].id;
}
route.continue();
}
);
const homePage = new HomePage(page);
await homePage.goto();
return productId;
}
function findIdByName(json: any, name: string): string | undefined {
const data = json.data;
for (let i = 0; i < data.length; i++) {
if (data[i].name === name) {
return data[i].id;
}
}
return undefined;
}
我构建的路由器会拦截返回 JSON 的网络流量,并返回 id
。我添加了一个可选参数 name
,如果传入该参数,它将动态遍历请求返回的 json 字符串,如果产品名称完全匹配(包括大小写),则返回该 id
以供后来使用。
通过这种方式,我可以按名称搜索,获取底层的 data-id
,然后使用该 data-id
动态定位页面上的元素。这只需几行代码就能提供极大的灵活性。示例代码可以在下面找到。
添加动态定位器和路由以通过名称获取 productId by BMayhew
感谢阅读!如果您觉得这篇文章有帮助,请在 LinkedIn 上与我联系,或考虑为我买杯咖啡。如果您想接收更多内容直接发送到您的收件箱,请在下方订阅。