← Back to microsoft/playwright
microsoft / playwright · Issue No. 42842
We follow the playwright guidance https://playwright.dev/docs/other-locators#css-locator about preferring user-visible locators (further enforced by the no-raw-locators lint rule), however we've hit some performance issues with getBy()...innerText() calls in loops that folks have written because playwright's api lacks an idiomatic nthMatch/nthChild locator.
For us, this often happens on tables (100+ rows), where we're trying to collect all of the values of a certain column for testing.
const colIdx = 5;
const cellValuesList: string[] = [];
const rows = page.getByRole("table").getByRole("rowgroup").getByRole("row");
for (const row of await rows.all()) {
const cellValue = await row.getByRole("cell").nth(colIdx).innerText();
cellValuesList.push(cellValue);
}
expect(expectedColsText).toEqual(cellValuesList)
The inner innerText() call takes about 50ms, and for 100 rows, we burn about 5s just collecting the cell values. Worse, since we're being forced to collect each cell one by one, we have to wrap this whole loop in an expect().toPass() when we're doing assertions (like asserting the column is sorted correctly after a button click). That adds further slowdowns since expect().toPass() retries the entire loop.
I have seen failed proposals for table helpers (https://github.com/microsoft/playwright/issues/4646, https://github.com/microsoft/playwright/issues/33111), but I think the problem is more generalizable. My solution this far has been to swap to a css locator for this usecase:
page.getByRole("table").locator(`[role='row'] [role='cell']:nth-child(${cssColIdx})`).allInnerTexts()
However it's still kind of clunky and might be an argument for an official nthChild (or nthMatch) locator in the playwright api.
I imagine it could work sort of like the existing filter api. (I am hand waving a little here as this might be more complicated than it sounds given how chaining locators behaves)
// get the 5th cell, from each row
page.getByRole("table").getByRole("row").nthMatch(5, page.getByRole("cell"))
This would be more performant the calling getBy()...innerText() in a loop, and also be more ergonomic, as it means we could "oneshot" the table column assertion by relying on playwright's standard auto-retry behavior rather than needing to fall back to an expect().toPass().
Contrast
// new
await expect(
page.getByRole("table").getByRole("row").nthMatch(5, page.getByRole("cell"))
).toHaveText(expectedColsText)
// old
await expect(
const cellValuesList: string[] = [];
const rows = page.getByRole("table").getByRole("rowgroup").getByRole("row");
for (const row of await rows.all()) {
const cellValue = await row.getByRole("cell").nth(5).innerText();
cellValuesList.push(cellValue);
}
expect(expectedColsText).toEqual(cellValuesList)
).toPass()
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.