Tables, date pickers and dynamic elements are the widgets that make UI tests brittle. This guide shows Playwright patterns for each: finding a row by its content instead of its index, checking sort order, filling native and custom date pickers, and locating elements whose IDs, text or timing change.
Tables
Level: L3.
What is it?
Working with HTML tables: reading cells, finding rows by content, acting on a specific row, and validating sorting/filtering/pagination.
Why do we need it?
Admin panels, dashboards, and reports are table-heavy. You must locate the right row and verify tabular data reliably.
How does it work?
Use role-based locators (row, cell) or scoped CSS, find a row by unique text, then act/assert within it. Read whole columns with allTextContents.
Syntax
const row = page.getByRole('row', { name: 'Asha' });await row.getByRole('button', { name: 'Edit' }).click();const emails = await page.locator('table tbody tr td:nth-child(2)').allTextContents();
Basic Example
await expect(page.getByRole('row', { name: 'Order #1001' })).toBeVisible();
Practical Example — act on a row, verify a cell
test('deactivate a user from the table', async ({ page }) => { await page.goto('/admin/users'); const row = page.getByRole('row').filter({ hasText: 'naveed@example.com' }); await expect(row.getByRole('cell', { name: 'Active' })).toBeVisible(); await row.getByRole('button', { name: 'Deactivate' }).click(); await expect(row.getByRole('cell', { name: 'Inactive' })).toBeVisible();});
Line-by-Line Explanation
- Find the row by the unique email (not by index — robust to reordering).
- Assert its status cell shows “Active”.
- Click Deactivate within that row, then assert the same row now shows “Inactive”.
Common Mistakes
- Selecting rows by .nth() when order/pagination changes.
- Reading a cell by absolute position that shifts when columns change.
- Not scoping actions to the target row (clicking the wrong row’s button).
Best Practices
- Locate rows by unique content, then scope actions/assertions inside them.
- Prefer getByRole('row'/'cell') for semantics.
- For column-wide checks, use allTextContents and assert on the array (sorted/contains).
Interview Questions
- Q: How to act on a specific table row? A: Find the row by unique text (filter/getByRole('row',{name})), then scope the action inside it.
- Q: How to verify sorting? A: Read the column with allTextContents, compare to a sorted copy.
- Q: Why avoid .nth() for rows? A: Row order changes with sorting/pagination, making tests brittle.
Practice Exercise
On a demo admin table: find a row by name, edit it, assert an updated cell; then read a column and assert it’s sorted ascending.
Real-World Scenario
An admin grid’s “delete” once targeted the wrong row after re-sort. A row-scoped test (locate by unique id, act within that row) prevented a repeat of the incident where an admin deleted the wrong account.
Date Pickers
Level: L3.
What is it?
Automating date selection: native <input type="date"> and custom calendar widgets.
Why do we need it?
Bookings, filters, DOBs — date inputs are everywhere and calendar widgets are a classic automation pain point.
How does it work?
- Native date input: fill() with an ISO string (YYYY-MM-DD).
- Custom widget: open it, navigate months, click the day cell (often by aria-label).
Syntax
// nativeawait page.getByLabel('Date').fill('2026-01-15');// customawait page.getByLabel('Check-in').click();await page.getByRole('button', { name: 'Next month' }).click();await page.getByRole('gridcell', { name: '15' }).click();
Basic Example
await page.locator('input[type="date"]').fill('2026-03-01');
Practical Example — dynamic future date on a custom picker
test('pick a date 7 days out', async ({ page }) => { await page.goto('/booking'); const target = new Date(); target.setDate(target.getDate() + 7); const day = String(target.getDate()); const monthLabel = target.toLocaleString('en-US', { month: 'long', year: 'numeric' }); await page.getByLabel('Travel date').click(); // navigate until the calendar header shows the target month while (!(await page.getByRole('heading', { name: monthLabel }).isVisible())) { await page.getByRole('button', { name: 'Next month' }).click(); } await page.getByRole('gridcell', { name: day, exact: true }).click(); await expect(page.getByLabel('Travel date')).toHaveValue(new RegExp(day));});
Line-by-Line Explanation
- Compute the target date relative to today (tests don’t rot as time passes).
- Loop “Next month” until the header matches the target month.
- Click the day cell by exact number, then assert the input reflects it.
Common Mistakes
- Hard-coding a fixed date that becomes past/invalid over time.
- Using fill() on a custom (non-native) widget.
- Clicking a day number that also appears in the prev/next month’s greyed cells (use exact/scope to current month).
Best Practices
- Compute dates relative to new Date().
- Detect native vs custom and handle accordingly.
- Scope day cells to the active month to avoid ambiguous matches.
Interview Questions
- Q: How to set a native date input? A: fill('YYYY-MM-DD').
- Q: How to handle a custom calendar? A: Open it, navigate months, click the day cell (often by aria-label/role).
- Q: Why compute dates dynamically? A: Hard-coded dates expire and cause future failures.
Practice Exercise
Automate both a native date input and a custom calendar to select “today + 10 days”, computed dynamically; assert the chosen value.
Real-World Scenario
A hotel booking test hard-coded a date that eventually fell in the past, failing nightly. Switching to today + N logic made it evergreen and revealed a real bug where the picker disabled valid future dates.
Dynamic Web Elements
Level: L3–L4.
What is it?
Handling elements whose ids/classes/positions change per render (auto-generated ids, lazy-loaded content, elements that appear/disappear).
Why do we need it?
SPAs and component frameworks generate volatile attributes. Locators tied to them break constantly. You need stable strategies.
How does it work?
Locate by stable, user-facing properties (role, text, label, data-testid) and relative relationships, not by volatile ids or absolute paths. Rely on auto-waiting for appear/disappear.
Syntax / strategies
// ✅ stable, semanticpage.getByRole('button', { name: 'Save' });page.getByTestId('save-btn');page.getByText('Order total').locator('..'); // relative to text// ⚠️ volatilepage.locator('#ember2381'); // auto-generated id
Basic Example
await expect(page.getByText('Loading...')).toBeHidden(); // wait it outawait expect(page.getByRole('table')).toBeVisible();
Practical Example — lazy list + spinner + relative locator
test('handle lazy-loaded results', async ({ page }) => { await page.goto('/search'); await page.getByPlaceholder('Search').fill('laptop'); await page.getByRole('button', { name: 'Search' }).click(); await expect(page.getByRole('progressbar')).toBeHidden(); // spinner gone const firstResult = page.getByRole('listitem').first(); await expect(firstResult).toBeVisible(); // find the price relative to the product name (structure-independent-ish) const price = firstResult.getByText(/^\$/); await expect(price).toBeVisible();});
Line-by-Line Explanation
- Wait for the spinner to disappear (auto-retrying toBeHidden) instead of a fixed sleep.
- Use role-based, position-tolerant locators.
- Find the price by a pattern within the item — resilient to changing class names.
Common Mistakes
- Locating by auto-generated ids/hashed classes.
- Absolute XPath through volatile DOM.
- Hard waits instead of waiting on appear/disappear states.
Best Practices
- Prefer role/text/label/data-testid.
- Use relative locators (filter, chaining, locator('..')).
- Ask devs for stable data-testid hooks on dynamic components.
- Let auto-waiting handle timing.
Interview Questions
- Q: How to handle changing ids? A: Locate by stable semantics (role/text/testid), not the volatile id.
- Q: How to wait for a spinner? A: expect(spinner).toBeHidden() (auto-retries).
- Q: How to find an element relative to a label? A: Locate the label text, then traverse/scope to the target.
Practice Exercise
On a page with auto-generated ids and a loading spinner, write locators that survive a reload (role/text/testid) and wait correctly for the spinner.
Real-World Scenario
A React app’s hashed class names changed every build, breaking CSS-based tests daily. Migrating to data-testid + role locators stopped the churn — locators no longer depended on styling internals.
FAQs
How do you find a table row by text in Playwright?
Filter the rows: page.getByRole('row').filter({ hasText: 'Order #1042' }), then locate the cell or button inside that row.
How do you handle a date picker in Playwright?
For native <input type="date">, call fill('2026-12-25'); for custom widgets, open the picker and click the month navigation and day with role or text locators, or type into the input if it accepts text.