Part 1a covered valid login and the locked-out user. Since then I’ve added three things to the suite: negative-path tests for auth, a sort dropdown check on the inventory page, and a broken-image check on problem_user. Along the way I also ran into something in a real Chrome window that had nothing to do with any of it, which I will talk about briefly at the end.
Empty fields and invalid password
Four negative-path cases went into auth.spec.ts — empty username, empty password, both empty, and a wrong password. Same assertion pattern for all four: check the error text, confirm the URL didn’t change. Wrong password gets “Username and password do not match any user in this service”; empty fields get “Username is required” or “Password is required.”
Nothing went wrong writing these — the one thing worth noting is that having the POM already in place from Part 1a made them straightforward to add.
Sort dropdown
I started on problem_user’s test case — the account name alone tells you it’s the one with known bugs. Then I logged in with this account and ran through a few actions — nothing looked obviously broken. I didn’t know what test case to add until I compared it against standard_user’s inventory page, which got me to build the test case on the normal flow first, as a baseline.
That baseline test had two bugs, caught when I was reviewing my code:
// Buggy — parseFloat chokes on the "$" prefix
const sortedPrices = [...prices].sort(
(a, b) => parseFloat(a) - parseFloat(b) // parseFloat("$7.99") === NaN
);
// NaN - NaN === NaN for every pair -> .sort() no-ops -> sortedPrices is just prices again
// Fixed — strip the currency symbol first
const sortedPrices = [...prices].sort(
(a, b) => parseFloat(a.replace("$", "")) - parseFloat(b.replace("$", ""))
);
Caught this one in review, before it ever ran. parseFloat won’t parse $, so parseFloat("$7.99") returns NaN — and a comparator that always returns NaN makes .sort() leave the array exactly as it started.
Stripping the $ fixed the comparator, but the underlying approach — sorting an array of price strings — turned out to have a second problem:
// Before — compares string[] to string[], sensitive to formatting, not value
const prices = await inventoryPage.getItemPrices(); // ["$7.99", "$9.99", ...]
const sortedPrices = [...prices].sort(
(a, b) => parseFloat(a.replace("$", "")) - parseFloat(b.replace("$", ""))
);
expect(prices).toEqual(sortedPrices); // "$7.90" !== "$7.9" even though same value
"$7.90" and "$7.9" are the same price but not the same string. So my fix went from “clean up the string comparator” to “stop comparing strings at all”: map every price to a number → sort the number array → then compare the numbers:
test.describe("Inventory", () => {
test("sort products by price (low to high)", async ({
loginPage,
inventoryPage,
}) => {
await loginPage.login(usernames.standard_user, PASSWORD);
await inventoryPage.sortBy("lohi");
await expect(inventoryPage.sortDropdown).toHaveValue("lohi");
const prices = await inventoryPage.getItemPrices();
const priceValues = prices.map((price) => parseFloat(price.replace("$", "")));
const sortedValues = [...priceValues].sort((a, b) => a - b);
expect(priceValues).toEqual(sortedValues);
});
});
Two bugs, same root cause: comparing prices as text instead of as numbers.
Broken images on problem_user
Writing getBrokenImages() meant using locator.evaluateAll(). The Playwright docs are vague about the callback’s argument type — couldn’t tell if it was a Locator or real DOM elements just from reading them, so I logged the value instead:
await locator.evaluateAll((els) => {
console.log(els); // <-- see what it actually is
return els.length;
});
Real DOM elements, confirmed.
Next problem: I’d typed the function to return Promise<Element[]>. It compiled, and the test passed, since the test only checked .length.
// Before — type-checks, test passes, but silently wrong at runtime
async getBrokenImages(): Promise<Element[]> {
return await this.inventoryItemImages.evaluateAll((imgs) =>
imgs.filter((img) => img.getAttribute("src")?.includes("404"))
);
// each item in the resolved array logs as "ref: <Node>", not a usable Element
}
But evaluateAll’s callback runs in the browser, and the result has to cross back to Node as JSON. Real DOM elements can’t survive that, so Playwright swaps them for a placeholder instead of throwing an error. What came back wasn’t Element[], just "ref: <Node>" stand-ins. TypeScript couldn’t catch this, since Element type-checks fine inside the callback.
So I fixed it by extracting the src string inside the callback, before it has to cross into Node:
// After — map to a JSON-serializable value inside the callback
async getBrokenImages(): Promise<string[]> {
return await this.inventoryItemImages.evaluateAll((imgs) => {
const brokenImgs = imgs.filter((img) =>
img.getAttribute("src")?.includes("404")
);
return brokenImgs.map((img) => img.getAttribute("src") || "");
});
}
Strings survive the trip. Elements don’t.
Aside: Chrome’s password breach warning
I was testing login manually in a real Chrome window, not Playwright’s isolated context. Chrome popped up a native warning: password found in a data breach.
It hadn’t. secret_sauce is the publicly documented password for every SauceDemo account, since the site is built for people to practice test automation on. Chrome doesn’t know that — it just sees the same password reused across many logins and flags it.
Playwright can’t act on it either way, since it’s a browser-level popup, not page DOM. Sticking to Playwright’s default isolated context avoids the warning entirely, since that context has no synced password manager to trigger the check.