How to Test a Login Form End-to-End

· TestDriver Team

How to test a login form end-to-end with plain-language steps, covering wrong passwords and logout.

Automate and scale manual testing with AI ->

Above: the test below running against the TestDriver Playground login page, sped up with idle time removed.

You can test a login form by describing it the way a user sees it. TestDriver matches each step against a screenshot of the page, so:

  • your test doesn’t break when an input’s id or class changes
  • you check what the user actually sees, like the error text and the welcome screen
  • the test reads like the login steps you’d give a teammate

TestDriver generates and runs black-box end-to-end tests. It starts your app in a fresh virtual machine, looks at the screen and uses a real mouse and keyboard, so it never needs selectors or access to your code. Each test is a normal Vitest file, and every run is recorded as a replay.

The example covers the three login form test cases most apps need: a wrong password shows an error, the right password logs you in, and logging out brings back an empty form. The full project is on GitHub: testdriverai/example-login-form.

Try it on the login form playground.

Why login forms are awkward with selectors

A selector-based test finds each field by its id, name or test ID. It then checks the result by reading DOM state, such as whether an error element is visible:

// Playwright-style login test (illustrative, not run)
await page.fill("#username", "demo");
await page.fill("#password", "wrong-password");
await page.click("button[type=submit]");
await expect(page.locator("#login-error")).toHaveText("Incorrect username or password.");

That passes as long as the element has the right text, even if a CSS bug hides it or covers the form. TestDriver describes the same steps by what’s on screen:

await testdriver.find("the Password input field below the Username field").click();
await testdriver.type("wrong-password");
await testdriver.pressKeys(["enter"]);
await testdriver.find("the red error message saying Incorrect username or password", { timeout: 10000 });

The check passes only when a user could see the error.

Watch the full replay

The replay shows every step with its screenshot and log.

The test

import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";

describe("Login form (TestDriver Playground)", () => {
  it("rejects a wrong password, logs in and logs out", async (context) => {
    const testdriver = TestDriver(context);

    await testdriver.provision.chrome({ url: "https://testdriver.ai/playground/login/" });

    // Try the demo username with a wrong password.
    await testdriver.find("the Username input field below the demo credentials box", { timeout: 30000 }).click();
    await testdriver.type("demo");
    await testdriver.find("the Password input field below the Username field").click();
    await testdriver.type("wrong-password");
    await testdriver.pressKeys(["enter"]);

    // The form should refuse it and clear the password.
    await testdriver.find("the red error message saying Incorrect username or password", { timeout: 10000 });
    const rejected = await testdriver.assert(
      'the login form shows the error message "Incorrect username or password." and the password field is empty',
    );
    expect(rejected).toBeTruthy();

    // Enter the right password and submit.
    await testdriver.find("the empty Password input field above the red error message").click();
    await testdriver.type("testdriver");
    await testdriver.pressKeys(["enter"]);

    // The welcome screen replaces the form.
    await testdriver.find("the Welcome, demo! heading", { timeout: 10000 });
    const loggedIn = await testdriver.assert(
      'the page shows "Welcome, demo!" with the text "You are logged in." and a Log out button, and the sign-in form is gone',
    );
    expect(loggedIn).toBeTruthy();

    // Log out and check the form comes back empty.
    await testdriver.find("the Log out button below You are logged in").click();
    await testdriver.find("the Sign in heading above the demo credentials box", { timeout: 10000 });
    const loggedOut = await testdriver.assert(
      "the Sign in form is shown again with empty Username and Password fields and no error message",
    );
    expect(loggedOut).toBeTruthy();
  });
});

How it works

Open the page

provision.chrome() starts Chrome in a fresh Linux sandbox and opens the login page. The first find() has a 30-second timeout, so the test waits for the page to load.

Describe elements by what users see

Each find() names the element by its label and position, such as “the Password input field below the Username field”. The test never reads the DOM, so renaming an id or swapping a CSS framework doesn’t break it. See locating elements.

Wait for results, then assert

After each submit, find() with a timeout waits for the error, the welcome heading or the form to appear. assert() then checks a plain-language statement against the screen, including details like an empty password field. See making assertions.

Run it yourself

Clone the example repo, or set up TestDriver in your own project and run the test:

npx testdriverai init
npx vitest run login-form.test.mjs

To test your own login form:

  • Change the url in provision.chrome() to your login page.
  • Use a test account, and pass its password with type(password, { secret: true }) so it’s masked in logs.
  • Rename the error and welcome descriptions to match your app’s wording.

FAQ

Do I need test IDs on my login inputs? No. Steps are matched on screenshots, so there are no selectors or data-testid values to add or keep in sync.

How do I keep real credentials out of the test? Read them from environment variables and type them with { secret: true }. See secrets.

Can I run it in CI? Yes. It’s a normal Vitest file and the browser runs in TestDriver’s cloud sandbox. See CI/CD.

Automate and scale manual testing with AI

TestDriver uses computer-use AI to test any app - write tests in plain English and run them anywhere.