playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Feature]: have toHaveAttribute(name, value) behave like toHaveText(value) when parameter is an array

Open Oscaruzzo opened this issue 10 months ago • 4 comments
trafficstars

🚀 Feature Request

The assertion expect(locator).toHaveText(value) accepts arrays as parameter and will check that the locator matches n elements and check that the nth element has the nth text value.

A similar behavior for other assertions would be very useful. In particular I'm in need of said behavior for the toHaveAttribute assertion. See example below.

Example

Given this document

<div >
   <div class="theClass" someAttr="a"></div>
   <div class="theClass" someAttr="b"></div>
   <div class="theClass" someAttr="c"></div>
</div>

I'd like this code to succeed

await expect(page.locator(".theClass")).toHaveAttribute("someAttr", ["a", "b", "c"])

and this code to fail

// unexpected "b" in DOM, "d" not found, "a" found in wrong position
await expect(page.locator(".theClass")).toHaveAttribute("someAttr", ["c", "d", "a"])

Motivation

I can write a function that does what I described, using a combination of expect.poll() and locator.getAttribute but I think this is a general enough case that deserves support out of the box. Motivation is basically ease of use.

Oscaruzzo avatar Dec 23 '24 11:12 Oscaruzzo

Addendum: at the moment I'm doing this, and it works, but I feel it's a bit clunky

await expect.poll(async () => {
  const headers = await page.getByRole('columnheader').all();
  const headersIds = await Promise.all(headers.map(l => l.getAttribute("col-id")));
  return headersIds;
}).toEqual(['FirstName', 'LastName', 'Age']);

Oscaruzzo avatar Dec 23 '24 13:12 Oscaruzzo

Another thing that should work is this:

await expect.soft(page.locator(".theClass").nth(0)).toHaveAttribute("someAttr", "a")
await expect.soft(page.locator(".theClass").nth(1)).toHaveAttribute("someAttr", "b")
await expect.soft(page.locator(".theClass").nth(2)).toHaveAttribute("someAttr", "c")
await expect(page.locator(".theClass")).toHaveCount(3)

Note the use of soft assertions, so that you'll see the values of all elements if one of them has the wrong attribute.

Skn0tt avatar Dec 23 '24 16:12 Skn0tt

@Oscaruzzo would an array of dictionaries be as good(i.e. instead of : await expect(page.locator(".theClass")).toHaveAttribute("someAttr", ["a", "b", "c"]) we'll have: await expect(page.locator(".theClass")).toHaveAttribute({"someAttr":"a"},{ "someAttr":"b"},{"someAttr": "c"}])?

Opher-Lubzens avatar Jan 05 '25 17:01 Opher-Lubzens

@Oscaruzzo would an array of dictionaries be as good(i.e. instead of : await expect(page.locator(".theClass")).toHaveAttribute("someAttr", ["a", "b", "c"]) we'll have: await expect(page.locator(".theClass")).toHaveAttribute([{"someAttr":"a"},{ "someAttr":"b"},{"someAttr": "c"}]) ?

I have to say I'm not a fan. Looks quite verbose and I'm not sure about the advantages, unless you have to assert something about multiple different attributes.

Oscaruzzo avatar Jan 05 '25 18:01 Oscaruzzo

Why was this issue closed?

Thank you for your involvement. This issue was closed due to limited engagement (upvotes/activity), lack of recent activity, and insufficient actionability. To maintain a manageable database, we prioritize issues based on these factors.

If you disagree with this closure, please open a new issue and reference this one. More support or clarity on its necessity may prompt a review. Your understanding and cooperation are appreciated.

pavelfeldman avatar Sep 04 '25 01:09 pavelfeldman

For posterity: ATM I'm doing this. It could probably be improved (I'm not happy about the way errors are reported, but I don't know how to improve it)

  export const expect = baseExpect.extend({
      async toHaveAttributeArray(locator: Locator, attribute: string, expected: string[], options?: { timeout?: number }) {
          const assertionName = "toHaveAttributeArray";
          let pass: boolean;
          let msg: string;
          try {
              await baseExpect.poll(async () => {
                  const locators = await locator.all();
                  const attributeValues = await Promise.all(locators.map(l => l.getAttribute(attribute)));
                  return attributeValues;
              }, options).toEqual(expected);
              pass = true;
          } catch (e: unknown) {
              pass = false;
              if (e instanceof Error) {
                  msg = e.message;
              }
          }
  
          const message = () => msg;
  
          return {
              message,
              pass,
              name: assertionName,
              expected,
          };
      },
  });

Oscaruzzo avatar Sep 04 '25 08:09 Oscaruzzo