Rayrun
← Back to Discord Forum

page.getByTestId().all() returns empty array with 6 matching elements on the page

boutchersj
boutchersj

Is this something we are allowed to do in Playwright?

There are 6 elements on the screen with the same data-testid attribute.

Trying to locate 1 by the data-testid (page.getByTestId()) throws an error and shows all 6 of these elements' markup in the error message.

I wasn't able to get page.getByTestId().all() to return more than an empty array.

This thread is trying to answer question "Is it possible to use getByTestId().all() in Playwright to return an array of elements with the same data-testid attribute?"

25 replies
sashaknits
sashaknits

Perhaps it assumes the id will be unique? I've never tried .all with getByTestId before Can you try this with the relevant id you are looking for and see if you get a populated array?

await page.locator('[data-testid=youridhere]').all()

Even better would be getting your testids updated so they are unique, if possible.

Did you miss the await keyword in await locator.all()?

I think you can't have duplicate IDs.

data-testids should be unique. This is widely accepted best practice, and it could be that playwright is assuming the same under the hood with the .getByTestId() method

sashaknits
sashaknits
@shivaguy: Oops yes I did!

Oops yes I did!

boutchersj
boutchersj
@shivaguy: Here's my approach: ``` async elements(strategy: string, selector: string): Promise<Locator[]> { switch(strategy) { case id: return this.page.getByTestId(selector).all() } } ``` `selector` is a `data-testid` string. Here's where I'm trying to implement it in the page object: `partModals = this.elements('id', 'part_modal')` Here's where I'm trying to use it in a test: ``` test('All 6 parts can be inspected', async () => { await expect(await PartsListRightPanel.partModals).toHaveLength(6) }) ```

Here's my approach:

async elements(strategy: string, selector: string): Promise<Locator[]> {
    switch(strategy) {
      case id:
        return this.page.getByTestId(selector).all()
    }
  }

selector is a data-testid string.

Here's where I'm trying to implement it in the page object:

partModals = this.elements('id', 'part_modal')

Here's where I'm trying to use it in a test:

test('All 6 parts can be inspected', async () => {
    await expect(await PartsListRightPanel.partModals).toHaveLength(6)
  })
boutchersj
boutchersj

I'm not sure it's always bad to have duplicate data-testid attributes.

There are many buttons with the same role attribute, and that's OK because it's a user-facing attribute and the user sees many buttons, too.

Since data-testid is useful primarily from a test development perspective, and since we maintain our own data-testid values, they're not subject to the same rules as ids, which are technically part of the "user-facing code". We should be able to use them however we want to make writing tests easier and more maintainable. That's just my take, and I've seen it work on my last team.

boutchersj
boutchersj

I'm kind of tempted to try document.querySelectorAll() to work around this. Not sure if it'll work, but what y'all are saying makes sense to me. Playwright probably prohibits this under the hood.

Belive the id property should be unique, at least according to the W3C spec, but many don't follow the spec regarding this anymore... And when i've had or seen discussion about the data-test-id think it was pointed there is no requirement for them to be unique. I just handle them as any other locator, only thing that differs is the search criteria, best practice would say there should be a getById() which goes for the id='xxx' attribute and it be unique. Best practice would be to follow the HMTL W3C spec and have a getById(), but when the "TestLibray" stuff was merged in, argument after argument... Bah...

boutchersj
boutchersj

Okay, I'm grateful to be learning about what the spec says is best practice, because I've very much learned this on-the-job and without reading anything in that spec. So it's cool you're willing to share that with me.

That said, PHRASED DIFFERENTLY, here is my problem, and @d3333333 I'm curious how you'd handle it if not with ids:

You have 6 divs on a page. Each div has several elements inside of it, which are the same inside all of them. Yep, it's a component. The dev is reusing it across the page. Each div, let's say, represents a blog post preview. Elements within it are such as "title", "timestamp", "author", etc.

How do you cleanly locate & verify the data within all 6 divs in your test?

boutchersj
boutchersj

You can see what I'm trying to do is Page.findAllCopiesOfThisComponentByTestId() (pseudo) and then, as you might guess, I'd loop through each of these container divs, verifying the common data within all of them using Component.someSharedElement.text...which, to me, beats the heck out of uniquely identifying each one and having to verify them independently

I would start ask what uniquely identifies your items if there is nothing, how can anything now infer what is not present? If you can identify what is unique, you have your search criteria? If not, it is a discussion with your developers to make them unique?

boutchersj
boutchersj

That just feels like trying to do the job of an id without using an id. Is there no clean & robust way recommended by the W3C spec to test UI components? It feels like the recommendation for this problem always includes a tradeoff:

  • use an absolute locator like ids but have tons of code duplication
  • use xpath, css selector, or some other relative locator, and hope a code change doesn't break your locator

Honestly don't think many consider or regard the W3C spec, who needs standards after all, i have an app to write to support my family, just let me get my stuff done, works fo "ME". In your case you get 'n' (6?) elements back why not loop: loc = page.locator(...") let cnt = await loc.Count(); for( i < cnt) { switch i: case 0;
verify_0(loc.nth(i)); case 1: verify_1(loc.nth(i)); }

Seems like the way you'll need to do, how much dup'ed code that is up to you now how you write your code...?

boutchersj
boutchersj

This feels good. I like your approach.

boutchersj
boutchersj

But I goofed, y'all.

boutchersj
boutchersj

I haven't been assigning the page object at the right time. I just switched over to POM and didn't realize I was passing the window object to the page class too early

boutchersj
boutchersj

This is the "console.log" debugger of Playwright that I should've used much sooner to figure this out:

await page.content().then(content => console.log(content))

boutchersj
boutchersj

Thanks for sitting through this and helping me out xD

@boutchersj: The pattern for testIDs on collections of templated components would be to prepend/append the test ID with unique data. EG: ```html <div data-testid="WidgetOkButton_001"> Widget 1 </div> <div data-testid="WidgetOkButton_002"> Widget 2 </div> <div data-testid="WidgetOkButton_003"> Widget 3 </div> ``` and the playwright... ```ts test('example test', async ({page}) => { const widgetElements = await page .locator('div[data-testid^="WidgetOkButton_"]') .all(); }); ``` The component might looks something like this (I'll use vue) ```html <template> <div class = "container text-center"> <div class = "row"> <div class = "col-md-8 col-lg-8 offset-lg-2 offset-md-2"> <div class = "card mt-5"> <div class = "card-body"> <ul class = "list-group" data-testid = "Widgets"> <li :data-testid = "`WidgetOkButton_${widget.id}`" class = "list-group-item" v-for = "widget in widgets" :key = "widget.id"> {{ widget.name }} <div class = "float-right"> <button :data-testid = "`EditButton_${widget.id}`" class = "btn btn-sm btn-primary mr-2" @click = "editTodo(widget)">Edit</button> <button :data-testid = "`DeleteButton_${widget.id}`" class = "btn btn-sm btn-danger" @click = "deleteTodo(widget)">Delete</button> </div> </li> </ul> </div> </div> </div> </div> </div> </template> ```

The pattern for testIDs on collections of templated components would be to prepend/append the test ID with unique data. EG:

<div data-testid="WidgetOkButton_001"> Widget 1 </div>
<div data-testid="WidgetOkButton_002"> Widget 2 </div>
<div data-testid="WidgetOkButton_003"> Widget 3 </div>

and the playwright...

test('example test', async ({page}) => {
    const widgetElements = await page
      .locator('div[data-testid^="WidgetOkButton_"]')
      .all();
  });

The component might looks something like this (I'll use vue)

<template>
  <div class = "container text-center">
    <div class = "row">
      <div class = "col-md-8 col-lg-8 offset-lg-2 offset-md-2">
        <div class = "card mt-5">
          <div class = "card-body">
            <ul class = "list-group" data-testid = "Widgets">
              <li :data-testid = "`WidgetOkButton_${widget.id}`" class = "list-group-item" v-for = "widget in widgets" :key = "widget.id">
                {{ widget.name }}
                <div class = "float-right">
                  <button :data-testid = "`EditButton_${widget.id}`" class = "btn btn-sm btn-primary mr-2" @click = "editTodo(widget)">Edit</button>
                  <button :data-testid = "`DeleteButton_${widget.id}`" class = "btn btn-sm btn-danger" @click = "deleteTodo(widget)">Delete</button>
                </div>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
boutchersj
boutchersj

I was originally using this exact approach (no really, it's a Vue frontend lol). Maybe I'm being unreasonable, but it seemed like having 1 id for each component would make it more difficult to assert on every one of them at the same time.

I got an approach working over the weekend that does this with a shared id, and can share when I go back to work tomorrow

All depends on the structure of the page? Practically speaking if this is a collection, wouldn't expect an id on any of the divs but the parent/containing control have an ID generally.

boutchersj
boutchersj

Okay, here's an example of how I use a shared id to test multiple instances of a component:

// Page
export default class ExamplePage extends BasePage {
    constructor(page: Page) {
        super(page)
    }

    // Elements
    blogPosts = this.elements('id', 'container_element')
}

// Section
export default class ExampleSection extends BaseSection {
    constructor(root: Locator) {
        super(root)
    }

    // Messages
    authorMs = this.messages([
        'Person One',
        'Person Two',
        'Person Three'
    ])

    // Elements
    author = this.element('id', 'author_name')
}

// Test
test('Sections all have authors', async () => {
  const blogPosts = await Example.blogPosts
  blogPosts.forEach(async (blogPost, idx) => {
    const BlogPost = new ExampleSection(blogPost)
    await expect(await BlogPost.author).toHaveText(BlogPost.authorMs[idx])
  })
})

If you're curious about the implementation details, I've got it all in a repo here: https://github.com/boutchersj/desktop_slinger

If you're going for an exact match, the page.getByTestId() is a nice to have, but for partials like the above template it's better (IMO) to go with .locator([data-testid^="someId"]) or .locator([data-testid*="someId"]) for suffixing and prefixing respectively.

refactoreric

If you want to verify the same things for each instance of the component, maybe you could instead test the component itself in isolation? https://playwright.dev/docs/test-components

Related Discord Threads

TwitterGitHubLinkedIn
AboutQuestionsDiscord ForumBrowser ExtensionTagsQA Jobs

Rayrun is a community for QA engineers. I am constantly looking for new ways to add value to people learning Playwright and other browser automation frameworks. If you have feedback, email [email protected].