使用 playwright 检查元素类是否包含字符串

问题描述 投票:0回答:3

我正在尝试获取第 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: 无法读取未定义的属性“包含”。你能帮我指出我在这里做错了什么吗?

javascript dom playwright
3个回答
17
投票

看来你可以写了

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')

6
投票

您必须在浏览器内对其进行评估。

$
将返回一个 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();
})();

1
投票

对于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调用可能是在撰写本文时最好的解决方案。
    

© www.soinside.com 2019 - 2024. All rights reserved.