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

Test Cases for a Login Page: 22 Cases, Run Against a Real Login

A login page test case list where every case was run. 22 cases against Sauce Demo, grouped by what they protect, with the error text the site actually showed, the cases a practice site can't answer, and a data-driven Playwright spec with its real output.

QA Vibes EditorialPublished Updated 11 minTested with Playwright 1.63.0, Chromium (Playwright 1.63.0)Revision history ↓

Key takeaways

  • A login page needs cases for four things: who gets in, who doesn't, what the page does, and what happens to the session afterwards.
  • Wrong password and unknown username should get the same message. On Sauce Demo they do, so the page doesn't reveal which accounts exist.
  • Our first logout test failed on our own locator, not the site: Sauce Demo's Logout is a link marked role="button", so getByRole finds a button, not a link.
  • Some cases can't be answered on a practice site: Sauce Demo let us in after 10 wrong passwords, and keeps the username in a plain cookie.
  • Put the rejected logins in one data table, so a new case is one line, and give a slow login a stated time limit instead of raising every timeout.
Contents (11 sections)

Introduction

Search for "test cases for login page" and you'll find long lists that nobody ran. This one is shorter, and every case in it was run against a real login page: Sauce Demo, a practice shop built for testers, which publishes its test accounts and password on its own login screen.

For each case you get the input, what a login should do, and what Sauce Demo actually did when we tried it on 21 September 2026. Then 14 of the cases become one Playwright spec, with its real output.

A few cases can't be answered on a practice site at all, because they depend on how your own system handles sessions and repeated attempts. Those are listed separately, with what Sauce Demo did, so you know what to check on yours.

The login page under test

Sauce Demo's login has a username field, a password field, a Login button, and an error area. It accepts six accounts with the password secret_sauce: standard_user, locked_out_user, problem_user, performance_glitch_user, error_user, and visual_user. All but locked_out_user log in; the others' bugs are on later pages of the shop.

Who gets in

# Case Input A login should Sauce Demo did
1 Valid account standard_user / secret_sauce Open the products page Opened /inventory.html with 6 products
2 Enter key Valid account, Enter in the password field Submit the form Opened /inventory.html
3 Other valid accounts problem_user, error_user, visual_user Log in Logged in
4 Slow login performance_glitch_user Log in within your stated limit Logged in after about 5.1 seconds

Case 4 matters more for automation than for users: Playwright's default assertion timeout is 5 seconds, so a plain toHaveURL check fails on a login that takes 5.1. The fix isn't a bigger global timeout; it's a stated limit for this one check (see the spec below).

Who doesn't get in, and what they're told

# Case Input A login should Sauce Demo did
5 Both fields empty nothing Refuse, and name the missing field "Epic sadface: Username is required"
6 Password empty standard_user / nothing Refuse, and name the missing field "Epic sadface: Password is required"
7 Username empty nothing / secret_sauce Refuse, and name the missing field "Epic sadface: Username is required"
8 Wrong password standard_user / wrong Refuse without saying which part was wrong "Epic sadface: Username and password do not match any user in this service"
9 Unknown username nobody / secret_sauce Give the same message as case 8 The same message as case 8
10 Username in capitals STANDARD_USER Follow your rule; usernames are often case-insensitive Refused, with the case 8 message
11 Password in capitals SECRET_SAUCE Refuse: passwords are case-sensitive Refused, with the case 8 message
12 Space before or after the username " standard_user", "standard_user " Follow your rule; many sites trim spaces Refused, with the case 8 message
13 Space after the password "secret_sauce " Refuse: a space is part of a password Refused, with the case 8 message
14 Locked account locked_out_user Refuse, and say the account is locked "Epic sadface: Sorry, this user has been locked out."
15 Very long username 5,000 characters Refuse normally, with no crash or hang Refused with the case 8 message, in under 0.1 seconds
16 Input shaped like SQL injection ' OR '1'='1 in both fields Refuse normally Refused with the case 8 message

Cases 8 and 9 are the important pair. When a wrong password and an unknown username get different messages, anyone can find out which accounts exist by trying names. Sauce Demo gives both the same message, which is what you want. Case 14 is a deliberate exception: telling someone their account is locked is a product decision, and on your system it's worth asking whether that message also tells a stranger the account exists.

Three conditions decide what this page says: whether the account exists, whether the password is right, and whether the account is locked. Our free decision table generator lists all eight combinations so that none is skipped, and has this login as an example. Two of the eight can't happen, because an account that doesn't exist can't be locked. The one that's easiest to forget is case 14: a locked account given the right password.

Cases 10 and 12 don't have one right answer. Decide the rule (are usernames case-sensitive? are spaces trimmed?), write it down, and test that rule. Sauce Demo's rule is "exact match".

Case 16 only shows that one input didn't get through. It is not a security test; a real injection check needs a security tool and permission to attack the system.

The page itself

# Case A login should Sauce Demo did
17 Password field Hide what's typed type="password": masked
18 Dismissing the error Let the user clear it A "Dismiss error" button removes it
19 Announcing the error Tell screen reader users about it The error has role="alert", so screen readers announce it
20 Field names Name every field for assistive technology Named "Username" and "Password" through aria-label, so getByLabel finds them; the only visible text is the placeholder

The session after login and logout

# Case A login should Sauce Demo did
21 Protected page without logging in Send the visitor to the login page Redirected to / with "Epic sadface: You can only access '/inventory.html' when you are logged in."
22 Back after logging out Not show the protected page again Back returned to / with the same message and no products, in 3 of 3 runs; the session cookie was gone after logout

Case 22 is where it's easy to fool yourself. Our first script for this check opened the products page a second time before logging out, and Back then landed on /inventory.html, which looked like a bug. Run in the order a user would (log in, log out, press Back), it behaved correctly every time. If a result surprises you, repeat it in the plainest order before you file anything.

Checks a practice site can't answer

These belong on your login's list, but Sauce Demo is a demo, so what it does here isn't a finding. We ran them to show why you need to check them on your own system:

  • Repeated wrong passwords. After 10 wrong passwords in a row, the right one still logged us straight in. A real system should slow down or lock out repeated failures according to its policy. Test that the limit exists, that the right password is refused while the account is locked, and that the lock ends when the policy says.
  • The session cookie. Sauce Demo keeps the username itself in a cookie called session-username, which scripts can read (it isn't HttpOnly), isn't marked Secure, and expires after about 10 minutes. On a real system, check that the session is an opaque token, and that the cookie is HttpOnly, Secure, has a SameSite setting, and expires when your policy says.
  • HTTPS. http://www.saucedemo.com/ redirected to https://. Check the same on yours.
  • Password reset, "remember me", and multi-factor login. Sauce Demo has none of these. If yours does, each is a flow with its own cases.

Automate the cases that repeat

Fourteen of the cases above make one spec. The rejected logins share one shape, so they go in a table, and adding a case is one line.

Save this config as playwright.config.ts. Sauce Demo marks its elements with data-test, so testIdAttribute lets getByTestId use them:

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

And this spec as tests/login.spec.ts:

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: string) {
  await page.goto("/");
  await page.getByPlaceholder("Username").fill(username);
  await page.getByPlaceholder("Password").fill(password);
  await page.getByRole("button", { name: "Login" }).click();
}
 
const rejected = [
  { title: "both fields empty", username: "", password: "", error: "Username is required" },
  { title: "password empty", username: "standard_user", password: "", error: "Password is required" },
  { title: "username empty", username: "", password: PASSWORD, error: "Username is required" },
  { title: "wrong password", username: "standard_user", password: "wrong_sauce", error: "Username and password do not match" },
  { title: "unknown username", username: "nobody_user", password: PASSWORD, error: "Username and password do not match" },
  { title: "username in capitals", username: "STANDARD_USER", password: PASSWORD, error: "Username and password do not match" },
  { title: "space after the username", username: "standard_user ", password: PASSWORD, error: "Username and password do not match" },
  { title: "locked-out account", username: "locked_out_user", password: PASSWORD, error: "Sorry, this user has been locked out." },
];
 
for (const { title, username, password, error } of rejected) {
  test(`rejects: ${title}`, async ({ page }) => {
    await logIn(page, username, password);
    await expect(page.getByTestId("error")).toContainText(error);
    await expect(page).toHaveURL("/");
  });
}
 
test("accepts a valid user and shows the products", async ({ page }) => {
  await logIn(page, "standard_user", PASSWORD);
  await expect(page).toHaveURL("/inventory.html");
  await expect(page.getByTestId("inventory-item")).toHaveCount(6);
});
 
test("Enter in the password field submits the form", async ({ page }) => {
  await page.goto("/");
  await page.getByPlaceholder("Username").fill("standard_user");
  await page.getByPlaceholder("Password").fill(PASSWORD);
  await page.getByPlaceholder("Password").press("Enter");
  await expect(page).toHaveURL("/inventory.html");
});
 
test("the password is masked", async ({ page }) => {
  await page.goto("/");
  await expect(page.getByPlaceholder("Password")).toHaveAttribute("type", "password");
});
 
test("a protected page sends a visitor who isn't logged in back to login", async ({ page }) => {
  await page.goto("/inventory.html");
  await expect(page).toHaveURL("/");
  await expect(page.getByTestId("error")).toContainText("You can only access '/inventory.html' when you are logged in.");
});
 
test("Back after logging out doesn't show the products again", async ({ page }) => {
  await logIn(page, "standard_user", PASSWORD);
  await expect(page).toHaveURL("/inventory.html");
  await page.getByRole("button", { name: "Open Menu" }).click();
  await page.getByRole("button", { name: "Logout" }).click();
  await expect(page).toHaveURL("/");
  await page.goBack();
  await expect(page).toHaveURL("/");
  await expect(page.getByTestId("inventory-item")).toHaveCount(0);
});
 
test("a slow login still arrives, within a stated limit", async ({ page }) => {
  // performance_glitch_user is slow on purpose. Say how slow is acceptable instead of raising every timeout.
  await logIn(page, "performance_glitch_user", PASSWORD);
  await expect(page).toHaveURL("/inventory.html", { timeout: 10_000 });
});

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

Running 14 tests using 1 worker
 
  ok  1 tests\login.spec.ts:25:7 › rejects: both fields empty (825ms)
  ok  2 tests\login.spec.ts:25:7 › rejects: password empty (568ms)
  ok  3 tests\login.spec.ts:25:7 › rejects: username empty (591ms)
  ok  4 tests\login.spec.ts:25:7 › rejects: wrong password (593ms)
  ok  5 tests\login.spec.ts:25:7 › rejects: unknown username (589ms)
  ok  6 tests\login.spec.ts:25:7 › rejects: username in capitals (614ms)
  ok  7 tests\login.spec.ts:25:7 › rejects: space after the username (602ms)
  ok  8 tests\login.spec.ts:25:7 › rejects: locked-out account (641ms)
  ok  9 tests\login.spec.ts:32:5 › accepts a valid user and shows the products (713ms)
  ok 10 tests\login.spec.ts:38:5 › Enter in the password field submits the form (659ms)
  ok 11 tests\login.spec.ts:46:5 › the password is masked (500ms)
  ok 12 tests\login.spec.ts:51:5 › a protected page sends a visitor who isn't logged in back to login (548ms)
  ok 13 tests\login.spec.ts:57:5 › Back after logging out doesn't show the products again (1.7s)
  ok 14 tests\login.spec.ts:68:5 › a slow login still arrives, within a stated limit (5.8s)
 
  14 passed (17.3s)

Three things in this spec came from running it, not from planning it:

  • The Logout locator. The first version used getByRole("link", { name: "Logout" }) and timed out. Logout is an <a href="#"> element, but Sauce Demo gives it role="button", so the accessibility tree lists it as button "Logout". getByRole matches the role assistive technology sees, not the tag name.
  • The slow login. performance_glitch_user takes just over 5 seconds, a little past Playwright's default 5-second assertion timeout. The spec gives that one check a 10-second limit and a comment saying why, so a login slower than 10 seconds still fails.
  • The URL after a rejection. Each rejected case also checks that the page stayed on /. An error message alone doesn't prove the user wasn't let in.

Exercise

Add two cases to the rejected table: a password with a space before it, and a username with a tab after it. Predict what Sauce Demo does before you run them. Then add a case proving that problem_user logs in, and decide whether it belongs in the table or in a test of its own.

To practise on a login and shop where you don't know the bugs in advance, try the practice lab: it runs your tests against ten seeded bugs and tells you which ones they catch.

Conclusion

A useful login test list covers who gets in, who doesn't and what they're told, the page itself, and the session afterwards. Run each case before you trust it: on Sauce Demo that turned one apparent bug into a mistake in our own script, and one failing locator into a lesson about roles. Then automate the cases that repeat, and keep the session and lockout checks for the system you actually ship.

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

Linked the decision table generator.
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.

Design your own cases

Decision table generator

List the conditions and actions, decide each combination, and see which ones nobody decided.