我正在尝试获取第 18 天的元素,并检查它的类上是否有 disabled。
<div class="react-datepicker__day react-datepicker__day--tue" aria-label="day-16" role="option">16</div>
<div class="react-datepicker__day react-datepicker__day--wed react-datepicker__day--today" aria-label="day-17" role="option">17</div>
<div class="react-datepicker__day react-datepicker__day--thu react-datepicker__day--disabled" aria-label="day-18" role="option">18</div>
这是我的代码,假设
this.xpath = 'xpath=.//*[contains(@class, "react-datepicker__day") and not (contains(@class, "outside-month")) and ./text()="18"]'
async isDateAvailable () {
const dayElt = await this.page.$(this.xpath)
console.log(dayElt.classList.contains('disabled'))) \\this should return true
我似乎无法让它发挥作用。错误提示 TypeError: 无法读取未定义的属性“包含”。你能帮我指出我在这里做错了什么吗?
看来你可以写了
expect(page.locator('.selector-name')).toHaveClass(/target-class/)
/target-class/
- 需要斜杠,因为它是 RegExp
为了通过一个调用检查几个类,我使用这个助手(这是因为 api 方式对我不起作用https://playwright.dev/docs/test-assertions#locator-assertions-to-have-class) :
async function expectHaveClasses(locator: Locator, className: string) {
// get current classes of element
const attrClass = await locator.getAttribute('class')
const elementClasses: string[] = attrClass ? attrClass.split(' ') : []
const targetClasses: string[] = className.split(' ')
// Every class should be present in the current class list
const isValid = targetClasses.every(classItem => elementClasses.includes(classItem))
expect(isValid).toBeTruthy()
}
在
className
中,你可以编写几个用空格分隔的类:
const result = await expectHaveClasses(page.locator('.item'), 'class-a class-b')
您必须在浏览器内对其进行评估。
$
将返回一个 ElementHandle,它是浏览器 DOM 元素的包装器,因此您必须使用例如evaluate
然后就可以了。或者简单地使用 $eval
查找元素,将其传递到在浏览器 JavaScript 引擎内执行的回调中。这意味着类似的事情会起作用:
// @ts-check
const playwright = require("playwright");
(async () => {
const browser = await playwright.chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.setContent(`
<div id="a1" class="foo"></div>
`)
console.log(
await page.$eval("#a1", el => el.classList.contains("foo1"))
)
await browser.close();
})();
对于OP的原始代码,XPath通常不是推荐的在Playwright中选择元素的方法。更喜欢定位器。我可能会按文本和角色进行选择,然后断言类存在。话又说回来,类不是“用户可见”的,因此那里可能还有更好的断言机会。 如果剧作家测试不符合OP的情况,
这个答案正确地提供了evaluate
,尽管我会用定位器写它:
const isDisabled = await page.getByLabel("day-18")
.evaluate(el => el.classList.contains("disabled"));
将此线程视为断言元素类的规范,很好地展示了如何使用正则表达式来匹配单个类,但也提倡在其expectHaveClasses
帮助器中使用antipattern:
const isValid = targetClasses.every(classItem => elementClasses.includes(classItem))
expect(isValid).toBeTruthy()
问题是失败时的错误消息将不清楚并且可能难以调试。此外,当元素位于 DOM 中时,断言不会等待类更改为正确的类,而是会异步调整其类列表。
类列表通常不会那么长,所以我会分别枚举每个类名:
const loc = page.locator("p")
await expect(loc).toHaveClass(/\bfoo\b/);
await expect(loc).toHaveClass(/\bbar\b/);
await expect(loc).toHaveClass(/\bbaz\b/);
这是一个很好的例子,WET 和避免过早抽象可能比DRY
更好,因为正确干燥代码的门槛相当高。 从技术上讲,前瞻也是可能的:
await expect(loc).toHaveClass(/(?=.*\ba\b)(?=.*\bb\b)(?=.*\bc\b)/);
但是这是不可读的,所以如果你要这样做,那么助手可能会有意义:
import {expect, test} from "@playwright/test"; // ^1.30.0
test.beforeEach(({page}) => page.setContent('<p class="a b c d"></p>'));
const hasWordsRegex = (...a) =>
new RegExp(a.map(e => `(?=.*\\b${e}\\b)`).join(""));
test("has classes 'a', 'b' and 'c'", async ({page}) => {
await expect(page.locator("p")).toHaveClass(hasWordsRegex("a", "b", "c"));
});
请注意,这并不严格,因此存在类
d
是可以的。这是常见情况。
如果您真的认真对待这个模式,您可以将其放入自定义匹配器中:import {expect, test} from "@playwright/test"; // ^1.30.0
test.beforeEach(({page}) => page.setContent('<p class="a b c d"></p>'));
const hasWordsRegex = (...a) =>
new RegExp(a.map(e => `(?=.*\\b${e}\\b)`).join(""));
expect.extend({
async toHaveAllClasses(received, ...classes) {
const className = await received.evaluate(el => el.className);
if (hasWordsRegex(...classes).test(className)) {
return {
message: () => "passed",
pass: true,
};
}
return {
message: () =>
`failed: element class '${className}' ` +
`did not match expected '${classes.join(" ")}'`,
pass: false,
};
},
});
test("has classes 'a', 'b' and 'c'", async ({page}) => {
await expect(page.locator("p")).toHaveAllClasses("a", "b", "c");
});
test("is missing at least one of 'a', 'b', 'c' or 'x'", async ({page}) => {
await expect(page.locator("p")).not.toHaveAllClasses("a", "b", "c", "x");
});
除了可读性之外,正则表达式的另一个问题是忘记转义正则表达式特殊字符。谨慎使用正则表达式!在上面的示例中,可能不清楚在幕后使用正则表达式,从而导致令人困惑的失败。它可以在没有正则表达式的情况下重写:
expect.extend({
async toHaveAllClasses(received, ...classes) {
const [classList, className] =
await received.evaluate(el => [[...el.classList], el.className]);
const missing = classes.filter(e => !classList.includes(e));
if (missing.length) {
return {
message: () =>
`failed: element class '${className}' ` +
`did not contain '${missing.join(", ")}'`,
pass: false,
};
}
return {
message: () => "passed",
pass: true,
};
},
});
如果您想在存在额外类时失败,您可以添加相反方向的检查:
const extra = classList.filter(e => !classes.includes(e));
if (extra.length) {
return {
message: () =>
`failed: element class '${className}' ` +
`had extra class '${extra.join(", ")}' ` +
`that wasn't part of the expected class '${classes.join(" ")}'`,
pass: false,
};
}
这可以让您以任意顺序准确检查多个类。
(希望如此)这里的最后一个问题是
evaluate
不会等待类通过测试——它会立即检查一次,然后成功或失败。
诸如waitForFunction
、
轮询或重试之类的东西可以帮助创建等待,但希望这些小烦恼足以激发为什么使用一些
toHaveClass
调用可能是在撰写本文时最好的解决方案。