Skip to content
Nowel's Blog
Go back

Playwright Portfolio Project, Part 1b: Negative-Path Auth, Sort Validation, and a Broken-Image Check

Edit page

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.

Repo on Github ->


Edit page

Next Post
Playwright Portfolio Project, Part 1a: Login and Locked-Out User