Skip to main content

Test automation cheatsheet

Learn to automate tests step by step, from your first script to a framework that runs in CI. Examples use Playwright with TypeScript on the practice app; the ideas carry over to Selenium, Cypress and WebdriverIO. Each section has three parts:

  • In short — the idea in one sentence.
  • Example — code with a comment saying what each line does or checks.
  • Try it — a small exercise.

Every example on this page was run and passes. Want the longer story? The framework implementation guide builds a complete framework, and the Playwright cheat sheet covers the tool's API.

📖 Full guide: Framework implementation → Beginner
How to use this page

Go through Part 1 in order — it gets you writing real tests. Part 2 turns scripts into a framework. Part 3 is for making a suite fast and trustworthy at scale. Type the examples yourself and run them — a test you've seen fail is a test you understand. You need Node.js 20 or newer. New to deciding what to test? Start with the manual testing cheat sheet.

Contents​

Part 1 — Beginner: What automation is · Set-up · Your first test · Finding elements · Actions · Assertions · Running & debugging · Grouping & hooks

Part 2 — Core: Page objects · Fixtures · Test data · Config & environments · API calls in tests · Tags & tiers · Parallel & independent · Reports & traces · Running in CI · Common mistakes

Part 3 — Advanced: Saved logins · Network mocking · Visual testing · Flaky tests · Known bugs · Sharding · Other tools · Words you'll meet

Part 1 — Beginner​

1. What automation is​

In short: test automation is code that drives the product and checks the results, so the same checks run in minutes on every change.

AutomateKeep manual
Regression checks that repeat every releaseNew features still changing
Smoke checks on every buildExploratory testing
Many data combinations"Does this look and feel right?"
API and contract checksOne-off checks
Cross-browser runs of the same flowHardware, real payments, captchas

Automation doesn't think — it only checks what you told it to. Deciding what to check is still testing work.

Try it: take your regression pack from the manual testing milestones (or any checklist) and mark each line "automate" or "keep manual".

2. Set-up​

In short: one command creates a Playwright project with a config file, an example test and the browsers.

mkdir my-tests && cd my-tests
npm init playwright@latest # answer: TypeScript, tests folder "tests", add GitHub Actions: yes
npx playwright test # runs the example test in 3 browsers
npx playwright show-report # opens the HTML report
my-tests/
├── playwright.config.ts # runner settings: browsers, retries, reports
├── tests/example.spec.ts # a sample test
├── .github/workflows/playwright.yml
└── package.json

Playwright is both the runner (finds and runs tests) and the browser driver (controls Chromium, Firefox and WebKit — the engine behind Safari).

Try it: run the set-up, then run npx playwright test --project=chromium to use only one browser. How much faster is it?

3. Your first test​

In short: a test is an async function that gets a page (a browser tab), does something, and checks the result with expect.

tests/first.spec.ts
// tests/first.spec.ts
import { test, expect } from '@playwright/test';

test('login page has the right title', async ({ page }) => {
await page.goto('https://www.saucedemo.com/'); // open the page
await expect(page).toHaveTitle('Swag Labs'); // passes: the tab title is "Swag Labs"
});
npx playwright test tests/first.spec.ts
# ✓ login page has the right title

await means "wait for this step to finish before the next line". Forget it and steps run out of order — the most common beginner bug.

Try it: change 'Swag Labs' to 'Swag Lab' and run again. Read the failure message: it shows expected vs received.

4. Finding elements​

In short: a locator describes how to find an element; prefer what a user sees (role and name, label, text) over page structure.

tests/locators.spec.ts
// tests/locators.spec.ts
import { test, expect } from '@playwright/test';

test('ways to find elements', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');

page.getByRole('button', { name: 'Login' }); // 1st choice: role + visible name
page.getByPlaceholder('Username'); // placeholder text of an input
page.getByText('Accepted usernames are:'); // visible text
page.getByTestId('username'); // data-testid="username" (see note below)
page.locator('[data-test="password"]'); // any CSS selector — last resort

await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); // passes
});
PriorityLocatorWhy
1getByRoleHow users and screen readers find things; survives restyling
2getByLabel, getByPlaceholder, getByTextVisible to users
3getByTestIdStable ids added for tests
4locator('css'), XPathTied to page structure — breaks when layout changes

Note: getByTestId looks for data-testid by default. Sauce Demo uses data-test, so set use: { testIdAttribute: 'data-test' } in the config, or use locator('[data-test="…"]') as this page does.

Try it: run npx playwright codegen https://www.saucedemo.com — click around and watch Playwright suggest locators for you.

5. Actions​

In short: locators have action methods — click, fill, press, selectOption, check — and each one waits until the element is ready.

tests/actions.spec.ts
// tests/actions.spec.ts
import { test, expect } from '@playwright/test';

test('common actions', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user'); // type into a field (clears it first)
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByPlaceholder('Password').press('Enter'); // press a key

await page.locator('[data-test="product-sort-container"]').selectOption('hilo'); // pick from a <select>
await page.getByRole('button', { name: 'Add to cart' }).first().click(); // click the first match
await page.getByRole('button', { name: 'Open Menu' }).click();
await page.getByText('Logout').click(); // no role here: this <a> has no href

await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); // back on the login page
});

.first() picks the first match when several elements match — without it, Playwright stops with a strict mode violation rather than guess.

Try it: add a step that sorts by 'az' and checks the first product is Sauce Labs Backpack.

6. Assertions​

In short: await expect(locator) assertions retry until they pass or time out (5 s by default); plain expect(value) checks once.

tests/assertions.spec.ts
// tests/assertions.spec.ts
import { test, expect } from '@playwright/test';

test('assertions that wait', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();

await expect(page).toHaveURL(/inventory\.html/); // URL matches
await expect(page.locator('[data-test="inventory-item"]')).toHaveCount(6); // exactly 6 products
await expect(page.locator('[data-test="title"]')).toHaveText('Products'); // exact text
await expect(page.getByText('Sauce Labs Backpack')).toBeVisible(); // on screen
await expect(page.locator('[data-test="shopping-cart-badge"]')).toBeHidden(); // empty cart: no badge

const count = await page.locator('[data-test="inventory-item"]').count(); // a plain number...
expect(count).toBe(6); // ...checked once, no waiting
});
AssertionChecks
toBeVisible() / toBeHidden()Shown / not shown
toHaveText('…') / toContainText('…')Exact / partial text
toHaveCount(n)Number of matches
toHaveValue('…')Value of an input
toHaveURL(/…/) / toHaveTitle('…')Page URL / tab title
toBeEnabled() / toBeChecked()State

Because of the retrying, you never need waitForTimeout(3000) sleeps. Use web-first assertions and the test waits exactly as long as needed.

Try it: add an assertion that the cart badge shows 1 after adding one item.

7. Running & debugging​

In short: run headless and fast by default; switch to UI mode, headed mode or the inspector when a test fails.

npx playwright test                         # all tests, all projects, headless
npx playwright test tests/cart.spec.ts # one file
npx playwright test -g "counts one item" # tests whose title matches
npx playwright test --headed # watch the browser
npx playwright test --ui # UI mode: time-travel through every step
npx playwright test --debug # step line by line with the inspector
npx playwright show-report # HTML report of the last run

When a test fails, read the error first: it says which locator, what it expected, and what it received. Then open the trace (section 16).

Try it: break a locator on purpose ('Login' → 'Log in') and run with --ui. Find the failing step and the page snapshot at that moment.

8. Grouping & hooks​

In short: test.describe groups tests; beforeEach runs set-up before every test in the group, so each test starts from the same state.

tests/hooks.spec.ts
// tests/hooks.spec.ts
import { test, expect } from '@playwright/test';

test.describe('cart', () => { // group related tests
test.beforeEach(async ({ page }) => { // runs before EACH test in this group
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
});

test('starts empty', async ({ page }) => {
await expect(page.locator('[data-test="shopping-cart-badge"]')).toBeHidden();
});

test('counts one item', async ({ page }) => {
await page.getByRole('button', { name: 'Add to cart' }).first().click();
await expect(page.locator('[data-test="shopping-cart-badge"]')).toHaveText('1');
});
});

Each test gets a fresh browser context (like a new private window) — no cookies or storage leak between tests. That's why both tests log in.

Try it: add a third test to the group, "removes the item", that adds then removes one product and checks the badge is hidden.

Part 2 — Core​

9. Page objects​

In short: a page object is a class for one screen; it keeps that screen's locators and actions in one place, so tests read like user steps.

src/pages/LoginPage.ts
// src/pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
import type { User } from '../data/users';

export class LoginPage {
readonly username: Locator;
readonly password: Locator;
readonly loginButton: Locator;
readonly error: Locator;

constructor(private readonly page: Page) {
this.username = page.getByPlaceholder('Username');
this.password = page.getByPlaceholder('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
this.error = page.locator('[data-test="error"]');
}

async open() {
await this.page.goto('/');
}

async loginAs(user: User) {
await this.username.fill(user.username);
await this.password.fill(user.password);
await this.loginButton.click();
}

async expectError(text: string) {
await expect(this.error).toContainText(text);
}
}

In a test: await loginPage.loginAs(users.standard). When the login screen changes, you fix LoginPage.ts once instead of every test.

Try it: write an InventoryPage with addToCart(name) and cartBadge(). Compare with the one in the framework guide.

10. Fixtures​

In short: fixtures create what a test needs and pass it in as a parameter — page and request are built-in fixtures; you can add your own.

src/fixtures/test.ts
// src/fixtures/test.ts — tests import `test` from here, so pages and clients arrive ready-made
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';
import { CheckoutPage } from '../pages/CheckoutPage';
import { BookingClient } from '../api/BookingClient';

type Fixtures = {
loginPage: LoginPage;
inventoryPage: InventoryPage;
checkoutPage: CheckoutPage;
bookingClient: BookingClient;
};

export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => use(new LoginPage(page)),
inventoryPage: async ({ page }, use) => use(new InventoryPage(page)),
checkoutPage: async ({ page }, use) => use(new CheckoutPage(page)),
bookingClient: async ({ request }, use) => use(new BookingClient(request)),
});

export { expect } from '@playwright/test';

Tests then import test from this file and ask for what they need: async ({ loginPage, inventoryPage }) => { … }. Code before use() is set-up, code after it is clean-up — and it runs even if the test fails.

Try it: add a checkoutPage fixture for your own CheckoutPage class.

11. Test data​

In short: keep test data in one place, and use builders that return valid data with defaults so each test overrides only what it's about.

src/data/bookingBuilder.ts
// src/data/bookingBuilder.ts — builds valid test data; override only what a test cares about
export type Booking = {
firstname: string;
lastname: string;
totalprice: number;
depositpaid: boolean;
bookingdates: { checkin: string; checkout: string };
additionalneeds?: string;
};

export function aBooking(overrides: Partial<Booking> = {}): Booking {
const unique = Date.now().toString(36); // unique name, so parallel runs don't clash
return {
firstname: `Test-${unique}`,
lastname: 'Automation',
totalprice: 150,
depositpaid: true,
bookingdates: { checkin: '2026-10-01', checkout: '2026-10-05' },
additionalneeds: 'Breakfast',
...overrides,
};
}

aBooking({ totalprice: 0 }) — the reader sees immediately that price is what this test is about. The unique name stops tests running in parallel from reading each other's data.

Try it: add a builder aCustomer() that returns first name, last name and postal code for the checkout form.

12. Config & environments​

In short: playwright.config.ts holds runner settings; environment values (URLs, users) come from environment variables through one file.

src/config/env.ts
// src/config/env.ts — one place for every environment-specific value
const required = (name: string, fallback?: string): string => {
const value = process.env[name] ?? fallback;
if (!value) throw new Error(`Missing environment variable ${name}`);
return value;
};

export const env = {
uiBaseUrl: required('UI_BASE_URL', 'https://www.saucedemo.com'),
apiBaseUrl: required('API_BASE_URL', 'https://restful-booker.herokuapp.com'),
apiUser: required('API_USER', 'admin'), // demo credentials, public
apiPassword: required('API_PASSWORD', 'password123'),
};
UI_BASE_URL=https://staging.example.com npx playwright test   # same tests, another environment

Set baseURL in the config and tests use short paths: page.goto('/'). Never commit real passwords — in CI they come from the secret store.

Try it: set UI_BASE_URL to a wrong address and run. How quickly does it fail, and is the message clear?

13. API calls in tests​

In short: the built-in request fixture sends HTTP requests — use it for API tests, and to create test data fast in UI tests.

tests/request.spec.ts
// tests/request.spec.ts
import { test, expect } from '@playwright/test';

test('API call inside a test', async ({ request }) => {
const res = await request.get('https://restful-booker.herokuapp.com/ping'); // no browser needed
expect(res.status()).toBe(201); // this API answers ping with 201
});

Creating data through the API and testing only the part you care about through the UI makes tests many times faster. More in the API testing cheat sheet.

Try it: send GET https://restful-booker.herokuapp.com/booking and check the response is a list with at least one item.

14. Tags & tiers​

In short: tag tests (@smoke, @regression) and run a different tier at different times — smoke on every change, regression nightly.

tests/tags.spec.ts
// tests/tags.spec.ts
import { test, expect } from '@playwright/test';

test('login page loads', { tag: '@smoke' }, async ({ page }) => { // tag option
await page.goto('https://www.saucedemo.com/');
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
});

test('title is Swag Labs @regression', async ({ page }) => { // tag in the title works too
await page.goto('https://www.saucedemo.com/');
await expect(page).toHaveTitle('Swag Labs');
});
npx playwright test --grep @smoke                 # only smoke
npx playwright test --grep-invert @quarantine # everything except quarantined tests

Try it: tag three of your tests @smoke and run only them.

15. Parallel & independent​

In short: Playwright runs test files in parallel workers; tests must never depend on each other or on shared data.

// playwright.config.ts (excerpt)
export default defineConfig({
fullyParallel: true, // tests inside one file run in parallel too
workers: process.env.CI ? 4 : undefined, // undefined = half your CPU cores
});
Makes tests depend on each otherFix
Test B uses data test A createdEach test creates its own data (builders + API)
Tests share one user account that they changeOne account per worker, or read-only shared accounts
Order of tests mattersSet-up in hooks/fixtures, never in "the previous test"

Try it: run your suite with --workers=1 and then with --workers=4. If results differ, you have a dependency to find.

16. Reports & traces​

In short: the HTML report lists every test; a trace records every step, the page, the network and the console, so you can replay a failure.

// playwright.config.ts (excerpt)
reporter: [['list'], ['html', { open: 'never' }], ['junit', { outputFile: 'results/junit.xml' }]],
use: {
trace: 'on-first-retry', // record a trace when a failed test is retried
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
npx playwright show-report                               # open the HTML report
npx playwright show-trace test-results/<test>/trace.zip # replay one test step by step

The JUnit XML file is for other tools: CI dashboards and test management tools can import it.

Try it: run one test with --trace on, open its trace and find the network request made when the page loaded.

17. Running in CI​

In short: CI (Continuous Integration) runs the tests on every pull request, so a change that breaks something is caught before it's merged.

# .github/workflows/tests.yml (the essential steps)
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- run: npm ci # install exact versions from package-lock.json
- run: npx playwright install --with-deps # browsers + the Linux libraries they need
- run: npx playwright test --grep @smoke
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # keep the report even when tests fail
with:
name: playwright-report
path: playwright-report/

The full, tested workflow (with nightly regression and sharding) is in the framework guide.

Try it: push a project with this workflow to GitHub and open a pull request. Where do you download the report from?

18. Common mistakes​

In short: most broken suites fail for the same few reasons.

MistakeSymptomFix
Missing awaitSteps out of order, random failuresawait every action and web-first assertion
waitForTimeout(3000)Slow and still flakyWeb-first assertions wait for the real condition
CSS/XPath tied to layoutBreaks on every redesignRole, label, text or test-id locators
Tests share dataPass alone, fail togetherUnique data per test
Huge end-to-end testsOne failure hides ten checksShort tests with one purpose
Assertions inside page objects for everythingTests hide what they checkKeep main assertions in the test
Importing one test file from another"test file should not import test file"Move shared code to src/

Part 3 — Advanced​

19. Saved logins​

In short: log in once in a set-up project, save the session to a file, and start every other test already logged in.

tests/auth.setup.ts
// tests/auth.setup.ts — Milestone 3: log in once, save the session for every UI test
import { test as setup, expect } from '@playwright/test';
import { users } from '../src/data/users';
import { STANDARD_USER_STATE } from '../src/data/authState';

setup('log in as standard_user', async ({ page }) => {
await page.goto('/');
await page.getByPlaceholder('Username').fill(users.standard.username);
await page.getByPlaceholder('Password').fill(users.standard.password);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL(/inventory\.html/);

await page.context().storageState({ path: STANDARD_USER_STATE }); // cookies + localStorage
});
tests/ui/cart.spec.ts
// tests/ui/cart.spec.ts — Milestone 3: starts already logged in, thanks to the saved session
import { test, expect } from '../../src/fixtures/test';
import { STANDARD_USER_STATE } from '../../src/data/authState';

test.use({ storageState: STANDARD_USER_STATE });

test('adding and removing updates the cart badge @regression', async ({ page, inventoryPage }) => {
await page.goto('/inventory.html'); // no login steps needed

await inventoryPage.addToCart('Sauce Labs Backpack');
await inventoryPage.addToCart('Sauce Labs Bike Light');
await expect(inventoryPage.cartBadge()).toHaveText('2');

await inventoryPage.removeFromCart('Sauce Labs Backpack');
await expect(inventoryPage.cartBadge()).toHaveText('1');

await inventoryPage.removeFromCart('Sauce Labs Bike Light');
await expect(inventoryPage.cartBadge()).toBeHidden(); // badge disappears at zero
});
// playwright.config.ts (excerpt) — run "setup" before the browser projects
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{ name: 'chromium', dependencies: ['setup'], use: { ...devices['Desktop Chrome'] } },
],

The file path lives in src/data/authState.ts, because a test file may not import another test file. Add .auth/ to .gitignore — it contains live sessions.

Try it: time your cart tests before and after using the saved login.

20. Network mocking​

In short: page.route intercepts requests the page makes, so a test can return fake data, change real data, or simulate a server error.

tests/mock/fruits.spec.ts
// tests/mock/fruits.spec.ts — Milestone 6: control what the back end returns
import { test, expect } from '@playwright/test';

const APP = 'https://demo.playwright.dev/api-mocking/';

test('shows exactly the fruits the API returns @regression', async ({ page }) => {
await page.route('**/api/v1/fruits', route =>
route.fulfill({ json: [{ name: 'Mango', id: 1 }, { name: 'Jackfruit', id: 2 }] }),
);

await page.goto(APP);

await expect(page.getByText('Mango')).toBeVisible();
await expect(page.getByText('Jackfruit')).toBeVisible();
await expect(page.getByText('Strawberry')).toBeHidden(); // a real fruit, not in our mock
});

test('keeps working when we add to the real response @regression', async ({ page }) => {
await page.route('**/api/v1/fruits', async route => {
const response = await route.fetch(); // call the real API
const fruits = await response.json();
fruits.push({ name: 'Tamarind', id: 999 });
await route.fulfill({ response, json: fruits });
});

await page.goto(APP);

await expect(page.getByText('Tamarind')).toBeVisible();
await expect(page.getByText('Strawberry')).toBeVisible();
});

test('shows no fruits when the API fails @regression', async ({ page }) => {
await page.route('**/api/v1/fruits', route => route.fulfill({ status: 500, body: 'boom' }));

await page.goto(APP);

await expect(page.getByText('Strawberry')).toBeHidden();
});

Use mocking to test states that are hard to create for real (errors, empty lists, huge lists) and to cut out slow or flaky third parties. Keep some tests un-mocked — mocks can hide real integration bugs.

Try it: add a test where the API returns an empty list []. What should the page show?

21. Visual testing​

In short: toHaveScreenshot compares the page with an approved baseline image and fails if too many pixels differ.

tests/visual/login.visual.spec.ts
// tests/visual/login.visual.spec.ts — Milestone 6: compare against an approved screenshot
import { test, expect } from '@playwright/test';

test('login page looks the same as the approved baseline @visual', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('login.png', { maxDiffPixelRatio: 0.01 });
});
npx playwright test tests/visual --update-snapshots   # create or approve the baseline
npx playwright test tests/visual # compare against it

Baselines are saved per browser and operating system (login-chromium-darwin.png on a Mac). Fonts render differently on Linux, so create CI baselines on the CI OS — for example inside the official Playwright Docker image.

Try it: log in as visual_user and standard_user and screenshot the product list for each. Does the visual test catch the layout bugs?

22. Flaky tests​

In short: a flaky test passes and fails on the same code; it teaches the team to ignore red builds, so treat it as a bug with an owner.

CauseFix
Racing the pageWeb-first assertions, never sleeps
Shared or leftover dataUnique data per test; clean up in fixtures
Slow environmentLonger timeout for that check only: toBeVisible({ timeout: 15_000 })
Animationspage.emulateMedia({ reducedMotion: 'reduce' }) or disable in test mode
Third-party widgetsMock them (section 20)
// playwright.config.ts (excerpt)
retries: process.env.CI ? 2 : 0, // a pass on retry is reported as "flaky"
grepInvert: process.env.CI ? /@quarantine/ : undefined, // quarantined tests don't block CI

Quarantine a flaky test with a @quarantine tag and a ticket with an owner and a deadline — don't just delete or ignore it.

23. Known bugs​

In short: test.fail() marks a test that is expected to fail because of a known bug — the run stays green, and you're told when the bug is fixed.

tests/ui/known-bugs.spec.ts
// tests/ui/known-bugs.spec.ts — Milestone 6: document known bugs without breaking the build
import { test, expect } from '../../src/fixtures/test';
import { users } from '../../src/data/users';

test('problem_user can sort by price @known-bug', async ({ loginPage, inventoryPage }) => {
test.fail(true, 'BUG-101: sorting does nothing for problem_user'); // expected to fail for now

await loginPage.open();
await loginPage.loginAs(users.problem);
await inventoryPage.sortBy('lohi');

expect((await inventoryPage.prices())[0]).toBe(7.99);
});

test('performance_glitch_user still reaches products @regression', async ({ loginPage, page }) => {
await loginPage.open();
await loginPage.loginAs(users.glitch);

// this login takes about 5 s; give this one check more time instead of adding a sleep
await expect(page.getByText('Products')).toBeVisible({ timeout: 15_000 });
});

When the bug is fixed, the "expected to fail" test suddenly passes — and Playwright reports that as a failure, reminding you to remove test.fail. The second test shows the fix for a slow page: more time for one check, not a sleep.

Try it: write a test.fail test for problem_user's last-name bug in checkout.

24. Sharding​

In short: sharding splits the suite across several machines that run at the same time, cutting wall-clock time.

npx playwright test --shard=1/3     # machine 1 runs the first third
npx playwright test --shard=2/3 # machine 2
npx playwright test --shard=3/3 # machine 3

In GitHub Actions, a matrix: { shard: [1, 2, 3] } runs the three commands on three machines. Merge the reports afterwards with npx playwright merge-reports using the blob reporter.

25. Other tools​

In short: the ideas on this page are the same in every tool; only the names change.

IdeaPlaywrightSelenium (Java)CypressWebdriverIO
Find elementpage.getByRole(…)driver.findElement(By.…)cy.get(…) / cy.contains(…)$('…')
Click / type.click() / .fill().click() / .sendKeys().click() / .type().click() / .setValue()
WaitBuilt inWebDriverWait + ExpectedConditionsBuilt inBuilt in on $()
Assertexpect(locator)JUnit/TestNG + AssertJ.should(…)expect(el)
RunnerPlaywright TestJUnit / TestNGCypressMocha / Jasmine
BrowsersChromium, Firefox, WebKitAll major (via drivers)Chromium family, Firefox, WebKit (experimental)All major
Mobile apps✗Via Appium✗Via Appium

Tool guides: Selenium · Playwright · Appium · Robot Framework.

26. Words you'll meet​

In short: the jargon, in one line each.

WordMeaning
RunnerThe tool that finds, runs and reports tests (Playwright Test, JUnit, pytest)
LocatorA description of how to find an element
Web-first assertionAn assertion that retries until true or timeout
Page objectA class for one screen that hides its locators
FixtureSomething a test needs, created and cleaned up for it
HeadlessThe browser runs without a visible window
Browser contextAn isolated browser session, like a private window
WorkerA separate process that runs tests in parallel
TraceA recording of a run you can replay step by step
BaselineThe approved screenshot a visual test compares against
FlakyPasses and fails on the same code
QuarantineTaking a flaky test out of blocking runs while it's fixed
ShardOne slice of the suite, run on its own machine
CIContinuous Integration — builds and tests every change automatically

For the full framework, see the framework implementation guide.