THE REPLACEQA BLOG

Playwright tests for dynamic content without hardcoded text

Test changing page content in Playwright by capturing the selected item and checking its destination without hardcoding titles that change.

To test dynamic content without hardcoded titles, capture the selected item's identity before clicking, then verify that the destination represents that same item. The expectation comes from the user's selection. Reading the destination's current heading and asserting it against itself cannot detect a wrong destination.

This approach suits article lists, product catalogs, and other pages whose data changes while the navigation requirement stays the same. It does not mean removing exact assertions everywhere. If a specific title or price is the requirement, keep an independent expectation for that value.

Decide whether the wording or the relationship matters

Imagine a documentation index whose first article was once “Invoice exports.” A test that always expects that heading after opening the first article will fail when an editor changes the title to “Export invoices as CSV.” The navigation may still be correct.

That is a stale assumption. It is not necessarily a flaky test: a test that fails consistently after an intentional content change has a reproducible mismatch. Intermittent list refreshes and timing races are different problems, even if both initially look like “dynamic content broke the test.”

Write the requirement before editing the assertion:

Selecting an article opens that article's detail page, showing its identifier and selected title.

This allows titles to change between runs, but still rejects navigation to another article. It also explains why merely checking that a heading exists is too weak: the wrong article can have a perfectly good heading.

Give the list and detail page an identity contract

The following example assumes these application conventions:

  • The list container has data-testid="article-list" and contains semantic list items.
  • Each item has a unique data-article-id containing lowercase letters, digits, or hyphens, and one article link.
  • The detail container has data-testid="article-detail" and a data-article-id for the record actually rendered.
  • The page has one level-one heading containing the article title.
  • The list is ready when its items appear, and an article's title stays consistent during this short journey.

These are explicit fixture assumptions, not universal selectors. Adapt them to your application. The detail identifier must describe its loaded record; copying a route parameter into a wrapper while displaying an unrelated record would weaken the contract.

Roles describe the interactive elements, while test IDs identify application regions. Playwright supports both and explains how to scope locators in its locator documentation.

Capture the selected record before navigating

In a Playwright Test project, the check can look like this. Replace the local origin with your fixture or configured application URL.

import { test, expect } from '@playwright/test';

test('opening an article preserves its identity', async ({ page }) => {
  await page.goto('http://127.0.0.1:4173/');

  const list = page.getByTestId('article-list');
  const first = list.getByRole('listitem').first();
  await expect(first).toHaveAttribute(
    'data-article-id', /^[a-z0-9-]+$/,
  );
  const id = await first.getAttribute('data-article-id');
  if (!id || !/^[a-z0-9-]+$/.test(id)) {
    throw new Error('Selected article has no valid identity');
  }

  // Follow the captured record, even if the list order changes.
  const selected = list.locator(`[data-article-id="${id}"]`);
  await expect(selected).toHaveCount(1);
  const link = selected.getByRole('link');
  await expect(link).toHaveText(/\S/);
  const title = (await link.innerText()).trim();

  await link.click();

  const detail = page.getByTestId('article-detail');
  await expect(detail).toHaveAttribute('data-article-id', id);
  await expect(detail.getByRole('heading', { level: 1 })).toHaveText(title);
});

The initial .first() selects the record to follow. After capturing its ID, the test uses a locator for that specific record. Continuing to use .first() for the click would risk opening a different item if the list reordered between reading and clicking. Playwright locators resolve elements when used, which matters on changing pages; see the locator behavior documentation.

The identifier restriction makes the selector interpolation safe for this example. If your identifiers can contain arbitrary characters, use an appropriately escaped selector or a different explicit test-ID convention. Do not silently assume every ID is a CSS-safe string.

The final assertions retry until their conditions hold or their timeout expires. They wait for the rendered result rather than a fixed delay. Playwright's assertion documentation describes these retrying matchers.

Prove that the test still rejects the wrong article

A useful replacement assertion should pass after an acceptable content edit and fail after a real behavior regression. Test both directions in a disposable fixture before relying on it.

For this example, create two records: a17, titled “Invoice exports,” and b29, titled “Team permissions.” Render a17 first in the list. Give the detail pages their corresponding record IDs and headings. Then exercise these changes separately:

Fixture versionSelected list recordDetail recordRequired result
Originala17, Invoice exportsa17, Invoice exportsPass
Title edited in both viewsa17, Export invoices as CSVa17, Export invoices as CSVPass
Link deliberately misdirecteda17, Export invoices as CSVb29, Team permissionsFail

We ran this sequence against a disposable local fixture in Chromium. The original and renamed versions passed. The wrong-destination version failed because the detail ID was b29 while the captured selection was a17.

For the negative case, change the selected link's target to the real detail route for b29; do not merely remove the page. That proves the identity assertion rejects a plausible but incorrect destination. A test that only checked for a visible heading would accept this broken behavior.

Titles alone are weaker identifiers. Two records can share a title, or a title can change independently of a stable ID. Keeping the ID assertion catches a wrong record even when its display text happens to match.

Handle refreshes and content edits deliberately

Capturing an ID does not freeze the application. If the selected record disappears before the click, the test should fail or report that its starting conditions changed. Silently choosing a new record would change the experiment halfway through.

If titles can legitimately update during navigation, decide whether title equality belongs in this test. You might verify the stable ID here and test title rendering separately against controlled data. Keep that decision explicit; do not weaken a failing assertion merely because a retry passes.

For controlled CI tests, seed known records or intercept a data request with a fixture. Playwright's API mocking guide documents request interception and response fulfillment. This gives you repeatable cases for duplicate titles, empty lists, and a missing selected record. A mocked response, however, does not establish that the live upstream service supplies correct data.

For checks against current public content, retaining the observed identity avoids dependence on yesterday's headline. It still cannot prove that the published content itself is correct if both views repeat the same wrong data. That requires an independent source of expected values.

Keep the expected behavior independent

Before replacing a hardcoded assertion, ask what failure the new test must detect. Here the answer is concrete: the selected record and displayed record must agree. Capture the expectation before the action, keep a stable way to follow that record, and deliberately break the destination once to verify the check.

If you are choosing which browser journeys deserve this work, start with a SaaS testing strategy without a QA team. ReplaceQA's closed beta explores public URLs, generates journey tests, and retains validation evidence for review. Request closed-beta access to explore that workflow; review generated expectations against your application's actual requirements.

Keep checking your important customer journeys.

Explore ReplaceQA ↗