2

In Cypress this is easy because I can use .and and I can chain 2 assertion:

checkFields() {
  this.heading().should('be.visible').and('contain.text', 'test')
}

But in Playwright I get the error: Property 'toContainText' does not exist on type 'Promise<void>'

async checkFields() {
  await expect(this.heading).toBeVisible().toContainText('test')
}

How can I do assertion in the same way like in Cypress without using the second line?

2
  • 1
    How is this.heading defined? If you select by text, then you can chain two assertions in one line, essentially, something like await expect(page.getByRole("h1", {name: "test"}).toBeVisible(). Also, if you're ultimately taking an action on it, like clicking, running a .click() action implicitly checks visibility. So the question needs more info/context to be fully answerable. There's also the .and() locator method which lets you chain locators, which are implicit assertions. Commented Dec 20, 2024 at 16:08
  • Not sure why this has been closed, seems to me a valid question for anybody coming from Cypress background. Commented Dec 23, 2024 at 1:48

3 Answers 3

9

Playwright doesn't have chained assertions. You just need to use as many asserts as you have.

checkFields(){
  await expect(heading).toBeVisible();
  await expect(heading).toHaveText("test");
}

If you want to have both of them validated even if one may fail - use expect.soft https://playwright.dev/docs/test-assertions#soft-assertions

checkFields(){
  // Make a few checks that will not stop the test when failed...
  await expect.soft(heading).toBeVisible();
  await expect.soft(heading).toHaveText("test");

  // Avoid running further if there were soft assertion failures.
  expect(test.info().errors).toHaveLength(0);
}
Sign up to request clarification or add additional context in comments.

1 Comment

"Playwright doesn't have chained assertions" is a bit of an oversimplification, no? Playwright has chained implicit assertions with locators--just no chained expects out of the box.
2

"Playwright doesn't have chained assertions", mentioned by the other answer, is technically true, but is missing a critical point: all Playwright locator actions are implicit assertions, and will lead to clear error messages when they fail. And since locators are chainable, Playwright does have chained assertions.

When you do

await page.getByRole("h1", {text: "test"}).click();

if no <h1>text</h1> element is found within the allotted timeout, the test will fail as expected with a descriptive log, just as if an expect() assertion had failed.

Playwright will also throw if the element was found in the document, but the .click() action on that element could not be completed, likely because the element wasn't actionable (in a disabled state, not visible, etc). .click() is an implicit assertion.

If you look at a typical Playwright test case, even if there are only a few explicit expect() assertions, there are probably 8-10 implicit assertions that need to be true before control flow even reaches the explicit expect()s at the end of the test.

With that in mind, how this.heading was defined matters. If it's defined as

this.heading = page.getByRole("h1", {name: "test"});
// ...
await expect(this.heading).toBeVisible();

using the best practice .getByRole(), which is an accessibility-first, user-visible assertion, then all of OP's conditions are asserted in one line. This reduces to the one-liner:

await expect(page.getByRole("h1", {name: "test"})).toBeVisible();

Which validates a few things:

  • The element is an <h1> element.
  • The element has text content containing "test" (you can pass exact: true if you don't want a substring match).
  • The element is visible (does not have other elements on top of it and has a non-zero bounding box).

On the other hand, if this.heading was defined more broadly, for example, page.locator("h1"), then .locator() or .filter() would need to be used to assert its text as well as visibility. This can be done in a chained manner (but I don't recommend doing this; better to use getByRole):

import {expect, test} from "@playwright/test"; // ^1.46.1

test("Header has text 'test'", async ({page}) => {
  await page.setContent("<h1>test</h1>");
  const heading = page.locator("h1"); // kinda vague, not recommended

  // Use chaining on the *locator*, not on the assertion!
  await expect(heading.locator("text=test")).toBeVisible();
});

Of course, you can make this as complex as you want:

test("Header has text 'test' and some other stuff for demonstration", async ({
  page,
}) => {
  await page.setContent('<h1 class="foo">test <span>bar</span></h1>');
  const heading = page.locator("h1"); // kinda vague, not recommended

  // Use chaining on the *locator*, not on the assertion!
  await expect(
    heading
      .locator("text=test")
      .filter({has: page.locator("span").locator("text=bar")})
      .locator(".foo:scope")
      .or(page.locator("etc etc"))
  ).toBeVisible();
});

This shows that Playwright does have chained implicit assertions, only in a different place than expected.

Now, I don't really recommend this pattern. If the assertion fails, it'll be unclear which predicate failed:

Running 1 test using 1 worker

  ✘  1 …1 › Header has text 'test' and some other stuff for demonstration (5.1s)


  1) pw1.test.js:3:1 › Header has text 'test' and some other stuff for demonstration 

    Error: Timed out 5000ms waiting for expect(locator).toBeVisible()

    Locator: locator('h1').locator('text=test').filter({ has: locator('span').locator('text=bar') }).locator('.foo').or(locator('etc etc'))
    Expected: visible
    Received: <element(s) not found>
    Call log:
      - expect.toBeVisible with timeout 5000ms
      - waiting for locator('h1').locator('text=test').filter({ has: locator('span').locator('text=bar') }).locator('.foo').or(locator('etc etc'))


      14 |       .locator(".foo")
      15 |       .or(page.locator("etc etc"))
    > 16 |   ).toBeVisible();
         |     ^
      17 | });
      18 |

So although this proves the concept, it's still better to break operations onto different lines and keep locators tight, user-visible and simple, without too much chaining.


As somewhat of a consequence of all this, a common antipattern in Playwright tests are redundant assertions. If you can click an element, you don't need to assert that it's visible, since visibility checks are already part of .click().

With this in mind, you'll find that many chained assertions aren't necessary in the first place--Playwright is doing a lot of asserting at every step to make sure the app behavior is as you intend it to be.

Taking another step back, it's OK to use less chaining nowadays. Playwright uses it in defining locators, but in modern async JS programming, using await on every line, using plenty of intermediate variables, and not chaining things is considered perfectly readable and elegant. Playwright intentionally uses a more imperative style.

Cypress is an older library, written when JS was (arguably somewhat unhealthily) obsessed with fluent programming and heavy chaining. The ecosystem has moved away from that somewhat, taking a more mature, balanced approach to different programming styles and paradigms.

9 Comments

Anyone care to explain what the issue is on the downvotes here? This shows that locators are implicit assertions, and chaining them is actually supported by PW without hacks--OPs assertion can be done in a single line quite easily. Seems like totally useful analysis.
Not a downvoter, but I think you are missing a critical distinction: locators are not fundamentally assertions in test automation. While Playwright's locators perform implicit checks (like waiting for visibility or interactability), they are not assertions in the traditional sense of verifying expected outcomes in tests.
@VishalAggarwal As the post states, locators are implicit assertions (please read this article). If you use await locator.click() you've fundamentally asserted that the element is visible and clickable, and writing expect(locator).toBeVisible() is superfluous. You can write entire tests without expect(), and you get the exact same outcome--guaranteeing that the application behaves as you expect. Locator actions that fail provide the same output as an expect failure. Even goto() is an implicit assertion that a URL is navigable.
Separation of Concerns: Keeping actions and assertions separate helps in maintaining the test code. Actions like click should focus on interactions, while assertions should focus on validation. Mixing the two can lead to tightly coupled tests that are harder to refactor or extend
Who said locators and assertions were the same thing? This isn't really a matter of choosing to separate concerns or not, Playwright will throw an error if a locator action fails. Taking any throwable action in a test is an assertion you're making and testing. By taking an action, you expect the application to have a specific state, and you get a test failure if that expectation fails. The only difference between await expect(locator).toBeVisible() and await locator.waitFor() is syntax, not semantics.
|
1

Playwright does not currently provide a built-in mechanism for chaining assertions in a single fluent-style statement like .and() or similar. Each assertion, such as toBeVisible() or toHaveText(), must be made individually or grouped logically in custom ways(like in the following example).

Custom Fluent-Style Assertion Chaining

If you really want a chaining syntax, you can create a custom helper class for chained assertions.

Here's an example:

import { expect, Locator } from "@playwright/test";

class FluentExpect {
  constructor(private locator: Locator) {}

  async toBeVisible() {
    await expect(this.locator).toBeVisible();
    return this; // Return the object for chaining
  }

  async toHaveText(expectedText: string | RegExp) {
    await expect(this.locator).toHaveText(expectedText);
    return this; // Return the object for chaining
  }

  async toHaveCount(expectedCount: number) {
    await expect(this.locator).toHaveCount(expectedCount);
    return this; // Return the object for chaining
  }
}

// Usage Example
import { test } from "@playwright/test";

test("fluent assertion example", async ({ page }) => {
  await page.goto("https://example.com");

  const header = page.locator("h1");
  const fluentExpect = new FluentExpect(header);

  await (
    await (await fluentExpect.toBeVisible()).toHaveText("Example Domain")
  ).toHaveCount(1);
});

Please note that this code probably will be difficult to maintain and less readable.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.