Skip to content
Independent guides for QA & test automationRSSEditorial policy
QA Vibes

Sauce Demo Error Messages: What Each One Means and How to Test It

A reference for the error messages on Sauce Demo, the practice shop testers automate first. Each message with its exact text, the input that triggers it, and where it sits on the page, the checkout errors problem_user and error_user cause, the alerts and page errors a test never sees unless it listens, and a Playwright spec with its real output.

QA Vibes EditorialPublished 12 minTested with Playwright 1.63.0, Chromium 153Revision history ↓

Key takeaways

  • Sauce Demo shows eight different error messages, all in one element: an h3 with role="alert" and data-test="error".
  • The checkout form reports only the first empty field, in form order, so one test per message needs the earlier fields filled.
  • "Last Name is required" with a last name typed in means problem_user: its last name lands in the first name field.
  • error_user's broken sort and cart never show an error on the page. One is a browser alert Playwright dismisses for you, the other a page error a test ignores unless it listens.
  • An error stays on screen after you fix the field. Only the X button clears it, so assert on the message right after the action that caused it.
Contents (12 sections)

Introduction

Sauce Demo is the practice shop most testers automate first, and its error messages are what people end up searching for: "Epic sadface: Username is required", "Error: Postal Code is required". This page lists every one we could trigger, with the exact text, what causes it, and how to check it in a test.

Everything below was run on 7 October 2026 with Playwright 1.63.0 and Chromium 153: 33 scripted cases against the live site, then the spec at the end. If Sauce Demo changes a message, the exact strings here will drift, so the spec checks whole messages on purpose; it will tell you when that happens.

For which login cases to test and why, see test cases for a login page. This page is the reference for what the shop says back.

Every error message, at a glance

Message Page Triggered by
Epic sadface: Username is required Login An empty username, whatever the password
Epic sadface: Password is required Login A username with an empty password
Epic sadface: Username and password do not match any user in this service Login A wrong password, an unknown username, or a username of only spaces
Epic sadface: Sorry, this user has been locked out. Login locked_out_user with the right password
Epic sadface: You can only access '/inventory.html' when you are logged in. Login Opening a shop page without being logged in; the path changes with the page
Error: First Name is required Checkout: your information An empty first name
Error: Last Name is required Checkout: your information A first name with an empty last name, or problem_user
Error: Postal Code is required Checkout: your information First and last name filled, postal code empty

Two more errors never reach the page. On error_user, sorting raises a browser alert, "Sorting is broken! This error has been reported to Backtrace.", and adding some products throws "Failed to add item to the cart." as an uncaught page error. Both are covered below.

Login errors

The login form checks the username first, then the password, then the pair:

  • "Epic sadface: Username is required" appears for an empty username even when the password is also empty. You never see both messages at once.
  • "Epic sadface: Password is required" appears only once a username is filled.
  • "Epic sadface: Username and password do not match any user in this service" covers a wrong password and an unknown username alike, so the page doesn't reveal which accounts exist. A username of three spaces gets this message too, not "Username is required": the login doesn't trim what you type.
  • "Epic sadface: Sorry, this user has been locked out." is the only message tied to one account, locked_out_user. With a wrong password, the same account gets the "do not match" message instead, so the lockout message also tells you the password was right.

The other accounts, standard_user, problem_user, performance_glitch_user, error_user, and visual_user, all log in. performance_glitch_user takes about five seconds, which is a timeout problem rather than an error message; the login article shows how to give that one check a stated limit.

"You can only access … when you are logged in"

Open any shop page directly without logging in, and Sauce Demo sends you back to the login page with the page you asked for named in the message:

  • /inventory.html → "Epic sadface: You can only access '/inventory.html' when you are logged in."
  • /cart.html, /checkout-step-one.html, /checkout-step-two.html, and /checkout-complete.html get the same sentence with their own path.
  • /inventory-item.html?id=4 names /inventory-item.html, without the query string.

Logging out and then opening /inventory.html gives the same message, so this is also how you check that logout really ended the session. In tests, this is the message you'll see when a test forgets to log in or a stored login state has expired.

Checkout information errors

The "Checkout: Your Information" form has three required fields and reports only the first empty one, in form order:

First name Last name Postal code Message
empty empty empty Error: First Name is required
Ada empty empty Error: Last Name is required
Ada Lovelace empty Error: Postal Code is required
empty Lovelace 10115 Error: First Name is required
Ada Lovelace 10115 No error; the overview page opens

So a test for "Postal Code is required" has to fill the first two fields, or it gets the first-name message instead. It also means a case with two empty fields only ever proves the first field's message, which is worth remembering when generated cases, like the ones in the pairwise article, leave more than one field empty.

Sauce Demo checks only that a field isn't empty. A single space in every field, or three spaces as the postal code, passed: the overview page opened. This site's own practice shop trims spaces first and rejects the same input, which is why the black-box techniques article found the opposite there. Neither is wrong in itself; which one is right for your form is a requirement to ask about.

The checkout also works with an empty cart. Checkout, Continue, and Finish all go through with nothing in it: the overview shows "Total: $0.00" and the order completes, with no error at any step.

"Last Name is required", but I filled it in

That's problem_user. When you type a first name and then a last name, the last name is written into the first name field and the last name field stays empty. Typing "Ada" and then "Lovelace" leaves "Lovelace" as the first name, and Continue gives "Error: Last Name is required".

A test that only checks for the error message passes for the wrong reason here. Check the field values after filling them, with toHaveValue, before you decide the validation is at fault.

Errors that never appear on the page

error_user is the account with bugs that don't show an error message at all:

  • Sorting raises a browser alert. Choosing "Price (low to high)" opens an alert saying "Sorting is broken! This error has been reported to Backtrace." and the order stays the same. Playwright dismisses dialogs automatically when nothing listens for them, so a test that never registers a dialog handler sees no alert at all. It only notices if it checks the order afterwards.
  • Some products can't be added to the cart. Clicking "Add to cart" on the Sauce Labs Fleece Jacket throws an uncaught error, "Failed to add item to the cart.", and the cart badge doesn't appear. Uncaught page errors don't fail a Playwright test on their own; you see them only if you listen to the pageerror event.
  • The checkout ignores the last name. Typing into the last name field leaves it empty, and Continue opens the overview anyway, with no "Last Name is required", throwing a page error as it goes. Finish on the overview then does nothing: the page stays on step two, no message appears, and a second page error is thrown.

problem_user has silent bugs of its own. Sorting by price changes nothing and raises no alert, and the Fleece Jacket's "Add to cart" button does nothing. Neither shows a message or throws a page error. For these, an error message is the wrong thing to wait for. Assert the result instead: the first price after sorting, the cart badge after adding.

How the error message is built

Every message above, login and checkout, is the same element:

  • an h3 with data-test="error" and role="alert", the role screen readers announce, so getByRole("alert") also finds it;
  • an X button, data-test="error-button", next to the text;
  • a red X icon in every field of the form, and the error class on every input, not just the empty one. On the login page both fields turn red when only the password is missing.

The message doesn't clear when you fix the field. After "Username is required", typing a username left the same message on screen. Only the X button removed it, along with the icons and the error class. Submitting again replaces it with the next message: with the username filled in, "Password is required". A test that fixes a field and then checks that "the error is gone" will fail unless it clicks the X first.

A spec that checks every message

Sauce Demo marks its elements with data-test, so set testIdAttribute and getByTestId("error") finds the message. Save this as playwright.config.ts:

import { defineConfig } from "@playwright/test";
 
export default defineConfig({
  testDir: "./tests",
  use: { baseURL: "https://www.saucedemo.com", testIdAttribute: "data-test" },
  reporter: "list",
});

And this as tests/errors.spec.ts. The checks use toHaveText with the whole message, not toContainText with part of it, because on a reference page the exact wording is the point. If you only care that some error appeared, toContainText is the less brittle choice.

import { test, expect, type Page } from "@playwright/test";
 
// Sauce Demo publishes this password on its own login page.
const PASSWORD = process.env.SAUCE_PASSWORD ?? "secret_sauce";
 
async function logIn(page: Page, username: string, password = PASSWORD) {
  await page.goto("/");
  await page.getByPlaceholder("Username").fill(username);
  await page.getByPlaceholder("Password").fill(password);
  await page.getByRole("button", { name: "Login" }).click();
}
 
async function openCheckout(page: Page, username = "standard_user") {
  await logIn(page, username);
  await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
  await page.getByTestId("shopping-cart-link").click();
  await page.getByRole("button", { name: "Checkout" }).click();
}
 
async function fillCheckout(page: Page, first: string, last: string, postal: string) {
  await page.getByPlaceholder("First Name").fill(first);
  await page.getByPlaceholder("Last Name").fill(last);
  await page.getByPlaceholder("Zip/Postal Code").fill(postal);
  await page.getByRole("button", { name: "Continue" }).click();
}
 
const loginErrors = [
  { title: "nothing entered", username: "", password: "", error: "Epic sadface: Username is required" },
  { title: "no password", username: "standard_user", password: "", error: "Epic sadface: Password is required" },
  { title: "wrong password", username: "standard_user", password: "wrong_sauce", error: "Epic sadface: Username and password do not match any user in this service" },
  { title: "locked-out account", username: "locked_out_user", password: PASSWORD, error: "Epic sadface: Sorry, this user has been locked out." },
];
 
for (const { title, username, password, error } of loginErrors) {
  test(`login: ${title}`, async ({ page }) => {
    await logIn(page, username, password);
    await expect(page.getByTestId("error")).toHaveText(error);
    await expect(page).toHaveURL("/");
  });
}
 
test("a page opened without logging in names itself in the error", async ({ page }) => {
  await page.goto("/checkout-step-one.html");
  await expect(page).toHaveURL("/");
  await expect(page.getByTestId("error")).toHaveText(
    "Epic sadface: You can only access '/checkout-step-one.html' when you are logged in.",
  );
});
 
test("a login error stays until it is dismissed", async ({ page }) => {
  await logIn(page, "", "");
  const error = page.getByTestId("error");
  await page.getByPlaceholder("Username").fill("standard_user");
  await expect(error).toHaveText("Epic sadface: Username is required");
  await page.getByTestId("error-button").click();
  await expect(error).toHaveCount(0);
  await expect(page.getByPlaceholder("Username")).not.toHaveClass(/\berror\b/);
});
 
const checkoutErrors = [
  { title: "nothing filled", first: "", last: "", postal: "", error: "Error: First Name is required" },
  { title: "first name only", first: "Ada", last: "", postal: "", error: "Error: Last Name is required" },
  { title: "no postal code", first: "Ada", last: "Lovelace", postal: "", error: "Error: Postal Code is required" },
  { title: "only the first name missing", first: "", last: "Lovelace", postal: "10115", error: "Error: First Name is required" },
];
 
for (const { title, first, last, postal, error } of checkoutErrors) {
  test(`checkout: ${title}`, async ({ page }) => {
    await openCheckout(page);
    await fillCheckout(page, first, last, postal);
    await expect(page.getByTestId("error")).toHaveText(error);
    await expect(page).toHaveURL("/checkout-step-one.html");
  });
}
 
// Each of these asserts what the shop should do. test.fail() records that today it doesn't:
// the test passes while the bug is there and fails the day it's fixed.
test("problem_user: the last name stays in the last name field", async ({ page }) => {
  test.fail(true, "problem_user's last name is typed into the first name field");
  await openCheckout(page, "problem_user");
  await fillCheckout(page, "Ada", "Lovelace", "10115");
  await expect(page).toHaveURL("/checkout-step-two.html", { timeout: 2_000 });
});
 
test("error_user: sorting by price puts the cheapest first", async ({ page }) => {
  test.fail(true, "error_user's sort raises an alert and leaves the order unchanged");
  const alerts: string[] = [];
  page.on("dialog", async (dialog) => {
    alerts.push(dialog.message());
    await dialog.dismiss();
  });
  await logIn(page, "error_user");
  await page.getByTestId("product-sort-container").selectOption("lohi");
  expect(alerts).toEqual([]);
  await expect(page.getByTestId("inventory-item-price").first()).toHaveText("$7.99", { timeout: 2_000 });
});
 
test("error_user: adding the fleece jacket updates the cart", async ({ page }) => {
  test.fail(true, "error_user's add-to-cart throws a page error and the cart stays empty");
  const pageErrors: string[] = [];
  page.on("pageerror", (error) => pageErrors.push(error.message));
  await logIn(page, "error_user");
  await page.getByTestId("add-to-cart-sauce-labs-fleece-jacket").click();
  await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1", { timeout: 2_000 });
  expect(pageErrors).toEqual([]);
});

Run it with npx playwright test. Our run, on Playwright 1.63.0:

Running 13 tests using 1 worker
 
  ok  1 tests\errors.spec.ts:35:7 › login: nothing entered (487ms)
  ok  2 tests\errors.spec.ts:35:7 › login: no password (388ms)
  ok  3 tests\errors.spec.ts:35:7 › login: wrong password (401ms)
  ok  4 tests\errors.spec.ts:35:7 › login: locked-out account (405ms)
  ok  5 tests\errors.spec.ts:42:5 › a page opened without logging in names itself in the error (339ms)
  ok  6 tests\errors.spec.ts:50:5 › a login error stays until it is dismissed (469ms)
  ok  7 tests\errors.spec.ts:68:7 › checkout: nothing filled (774ms)
  ok  8 tests\errors.spec.ts:68:7 › checkout: first name only (736ms)
  ok  9 tests\errors.spec.ts:68:7 › checkout: no postal code (742ms)
  ok 10 tests\errors.spec.ts:68:7 › checkout: only the first name missing (761ms)
  x  11 tests\errors.spec.ts:78:5 › problem_user: the last name stays in the last name field (2.7s)
  x  12 tests\errors.spec.ts:85:5 › error_user: sorting by price puts the cheapest first (502ms)
  x  13 tests\errors.spec.ts:98:5 › error_user: adding the fleece jacket updates the cart (2.5s)
 
  13 passed (12.5s)

The three x lines are expected failures: the list reporter marks them, and they count as passed because test.fail() said they would fail. test.fail() accepts any failure, so we checked why each one failed. The problem_user test stayed on /checkout-step-one.html; the sort test failed on expect(alerts).toEqual([]) because the alert arrived; the cart test timed out waiting for the badge. Those are the bugs, not a typo in the test.

Two details came from running it:

  • The dismiss test types first and asserts after. It fills the username, then checks that the old message is still there, then clicks X. Checking the message before typing would pass whether or not the message persists.
  • The dialog handler changes what Playwright does. Once a test registers one, Playwright stops dismissing dialogs for you, so the handler has to call dialog.dismiss() or dialog.accept() itself, or the page waits.

Exercise

Add a checkout case for error_user with every field filled, and predict where it ends up before you run it. Then write the test that would catch its Finish bug: what can you assert on after clicking Finish, given that no error message appears?

To practise on a shop where you don't know the bugs in advance, try the practice lab: it runs your tests against seeded bugs and tells you which ones they catch. When you find one, the bug report grader checks the write-up.

Conclusion

Sauce Demo has eight error messages, all in one data-test="error" element, and they follow simple rules: the login checks the username first, the checkout reports only the first empty field, and a message stays until someone clicks X. The more useful lesson is in the errors that never reach that element. problem_user gives a correct message for the wrong reason, and error_user hides its failures in an alert Playwright dismisses and page errors it doesn't report. Assert the outcome, not just the absence of a message, and listen for dialogs and page errors when a page can fail quietly.

Sources and further reading

Tools mentioned

PlaywrightUI AutomationOpen source

Links go to each tool’s official site. How we choose and link tools

Revision history

First published.

Spotted a mistake? Report it — corrections land here.

Written and reviewed by

QA Vibes Editorial

Articles are written and reviewed by practicing QA and automation engineers. Every article lists its sources and shows when it was last updated.

Check your own tests

Playwright test reviewer

Paste a test to find fixed waits, missing awaits, and assertions that can never fail.