In Playwright, how do you check that a deleted transaction row is really gone and not merely never rendered?
answer
- Absence alone is weak evidence
- Numbers make positive statements
- Removed is not the same as hidden
- Record the count before acting
- Detachment versus invisibility
basics
~20 sBracket the delete with positive assertions. Assert the table and the target row are visible and the row count is known, delete, then assert the count dropped by one and the row is no longer attached.
solid answer
~40 sNever let the absence check carry the test on its own. Anchor first: `await expect(rows).toHaveCount(13)` and `await expect(target).toBeVisible()` both fail on a page that never rendered the transactions table. Then delete, and assert the effect as a number — `await expect(rows).toHaveCount(12)` — because a count is false on an empty page in a way that `not.toBeVisible()` never is. Finish with `await expect(target).not.toBeAttached()`, which is stricter than a visibility negation: it keeps failing while the node lingers in the DOM, hidden by CSS. The two assertions catch different bugs. The count catches a delete that removed nothing or removed everything; the detachment check catches a row that was hidden rather than removed.
code
typescript · 16 linesimport { expect, test } from '@playwright/test';
test('deleting a transaction removes exactly that row', async ({ page }) => {
await page.goto('/statements/2026-08');
const rows = page.getByRole('table', { name: 'Transactions' }).getByRole('row');
const target = rows.filter({ hasText: 'Coffee Roasters' });
await expect(rows).toHaveCount(13);
await expect(target).toBeVisible();
await target.getByRole('button', { name: 'Delete' }).click();
await expect(rows).toHaveCount(12);
await expect(target).not.toBeAttached();
});go deeper
Learn the bracket: assert the row is there, delete it, assert the row count went down by one. Do not write a test whose only assertion is that something cannot be seen.
Explain why a count is stronger evidence than a negation. A number is false on an unrendered page, whereas every absence check is satisfied by one, so the count is what makes the test falsifiable.
Show the difference between removed and hidden, pick the assertion that matches the requirement the application actually made, and say which real bug each line of the test would catch.
Set the standard for what counts as evidence in a suite, and weigh strictness against coupling: detachment assertions catch real leaks but bind tests to rendering internals that a virtualised table may legitimately change.
## What "gone" has to mean "The row is gone" is three different claims, and the matcher you choose decides which one the test makes: - **not rendered to the user** — the node may still be mounted; `toBeHidden()` covers it - **not in the DOM** — the node was removed; `not.toBeAttached()` or `toHaveCount(0)` covers it - **one fewer than before** — a claim about the collection, covered by a count assertion Only the third is immune to the failure that ruins the other two. `toBeHidden()` and `not.toBeAttached()` are both satisfied by a page that rendered no transactions table at all, so on their own they prove that the row is absent — from a page you have not verified exists. ## Counting turns a negation into a positive claim A count assertion states a number, and a number is false on an empty page. That single property is what makes it the backbone of a removal check: - `await expect(rows).toHaveCount(13)` before the delete fails if the table never rendered. - `await expect(rows).toHaveCount(12)` after it fails if the delete did nothing, and also if the page fell over between the two assertions. - Together they prove that exactly one row left, which a per-row negation never establishes — deleting the wrong row, or all of them, still satisfies an absence check on the target. `toHaveCount(0)` is the exception that proves the rule. It is a precise claim about DOM absence, but zero is also what an unrendered page produces, so it needs an anchor exactly as much as `not.toBeVisible()` does. ## Removed, hidden, or never there | Application behaviour | `not.toBeVisible()` | `not.toBeAttached()` | `toHaveCount(n-1)` | |---|---|---|---| | Row removed from the DOM | passes | passes | passes | | Row kept in the DOM, hidden by CSS | passes | fails | fails | | Table never rendered | passes | passes | fails | | Wrong row deleted | fails | fails | passes | No single row of that table is the answer; the useful reading is that the checks fail in **different** situations, so a removal test worth trusting uses more than one of them. The last column is the only one that survives an unrendered page, and the last row is the only case a count misses — which is why the target row still deserves its own assertion. ## A worked sequence on the statement page 1. Navigate, then anchor: assert the transactions table is visible and that the row locator has the count the fixture guarantees. 2. Locate the specific row by its content, and assert it is visible — this is the anchor that proves the thing you are about to delete was there. 3. Perform the delete through the row's own control, not a global one, so the test exercises the path a user takes. 4. Assert the count dropped by one. This is the assertion that fails when the page dies mid-test. 5. Assert `not.toBeAttached()` on the target row, which fails while the node lingers in the DOM. 6. Optionally assert a neighbouring row is still present, which catches a delete that took too much with it. Steps 4 and 5 are complementary rather than redundant: the count catches "too many rows left or too few", the detachment check catches "the right number of rows, but this one is still mounted and merely hidden". ## Where each check can still lie to you - A hidden-but-attached row can still receive clicks or be read by assistive technology, so `not.toBeVisible()` alone is a weak requirement for anything security- or accessibility-adjacent. - A count is only as good as the fixture behind it; a shared, mutable seed database turns `toHaveCount(13)` into the flakiest line in the file. - `not.toBeAttached()` asserts an implementation detail. If the application legitimately keeps nodes mounted — a virtualised table, an animated exit, a collapsed group — the check tests a promise the app never made. ## Review heuristics - Ask whether the test would pass against a blank page. If yes, it needs a positive anchor. - Ask whether it would pass if the wrong row were deleted. If yes, it needs a count or a check on a survivor. - Prefer asserting the delta over asserting the absence: `toHaveCount(12)` says more than `toHaveCount(0)` on a filtered locator, and it says it in a form that cannot be satisfied by an empty page.
- What if the starting row count is not fixed by the test data?Read it and reuse it: `const before = await rows.count()`, act, then `await expect(rows).toHaveCount(before - 1)`. Because `count()` is a one-shot read with no retry, take it only after a positive assertion has established that the table rendered — otherwise you anchor the whole test on zero.
- When is not.toBeVisible() the better check for a removed element than not.toBeAttached()?When the application legitimately keeps the node mounted: a collapsed group, a virtualised list, an animated exit that leaves the element in place. Asserting detachment there tests an implementation detail the app never promised, so assert invisibility instead and anchor it on something positive.
- How would you catch a delete that removes the wrong row?A count alone cannot: twelve rows remain either way. Assert on a survivor as well as on the target — check that a named neighbouring transaction is still visible, or assert the remaining row labels as a set — so the test pins which row left rather than only how many.
saying these in an interview costs you the question
- Asserts the row is absent without recording the count beforehand.
- Treats not.toBeVisible as proof the node was removed from the DOM.
- Checks toHaveCount(0) on a page that may never have rendered.
- Assumes a hidden but attached row cannot still receive clicks.
- Confirms the target row left but never that the others survived.