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

00jrzges  于 2023-04-19  发布在  Java
关注(0)|答案(3)|浏览(377)

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

ac1kyiln

ac1kyiln1#

看起来你可以直接写

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

7gcisfzg2#

你必须在浏览器内部评估它。$将返回一个ElementHandle,它是浏览器DOM元素的 Package 器,所以你必须使用例如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();
})();
vngu2lb8

vngu2lb83#

关于OP的原始代码,在Playwright中,XPath通常不是选择元素的推荐方法。更喜欢定位器。我可能会通过文本和角色进行选择,然后Assert类存在。然后,类不是用户可见的,所以那里可能也有更好的Assert机会。
如果Playwright test不在图片中,就像OP的情况一样,this answer正确地提供了evaluate,尽管我会用定位器编写它:

const isDisabled = await page.getByLabel("day-18")
  .evaluate(el => el.classList.contains("disabled"));

this answer将此线程视为Assert元素类的规范,很好地展示了如何使用正则表达式来匹配单个类,但也在其expectHaveClasses助手中提倡反模式:

const isValid = targetClasses.every(classItem => elementClasses.includes(classItem))
expect(isValid).toBeTruthy()

问题是失败时的错误消息不清楚,可能很难调试。此外,当元素在DOM中时,Assert不会等待类更改为正确的类,而是会异步调整其类列表。
类列表通常不会很长,所以我会分别列举每个类名:

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更好,这是一个很好的例子,因为正确DRYing出代码的标准相当高。
从技术上讲,lookaheads也是可能的:

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调用可能是最好的解决方案。

相关问题