You type "Maya" into a search box and check the result:
await driver.findElement(By.css('[data-testid="search-input"]')).sendKeys('Maya');
const cell = await driver.wait(
until.elementLocated(By.xpath("//*[contains(text(),'Maya Chen')]")),
5000
);
assert.ok(await cell.isDisplayed());
Maya is visible. The test passes. Now let me show you why this test is nearly useless.
Imagine the search box is completely broken and ignores what you type. All eight contacts stay on the screen. Is Maya Chen one of those eight? Yes. Is she visible? Yes.
The test passes on a search that does nothing.
The mistake is easy to make, because you tested what you were looking for. But a filter is not defined by what it keeps. A filter is defined by what it removes.
So count the rows. In Selenium you use findElements with an "s", which returns a list:
let rows = await driver.findElements(By.css('[data-testid="contact-row"]'));
assert.strictEqual(rows.length, 8); // before
await driver.findElement(By.css('[data-testid="search-input"]')).sendKeys('Maya');
rows = await driver.wait(async () => {
const found = await driver.findElements(By.css('[data-testid="contact-row"]'));
return found.length === 1 ? found : null;
}, 5000);
assert.strictEqual(rows.length, 1);
assert.ok((await rows[0].getText()).includes('Maya Chen'));
The middle part deserves a closer look. Filtering takes a moment, so you cannot count immediately. This is a custom wait: you give driver.wait your own function, and it keeps calling it until you return something that is not null. Here you return the rows only when there is exactly one.
You will use that pattern often in Selenium, because counting things after a change is very common.
This idea goes beyond search boxes. Any time your test makes a list smaller, count it. For deleting especially: a test that checks "the deleted item is gone" passes when the app deleted everything.
Remember this: when a list changes, check its size, not just one item in it.
Reference: Finding elements.