Part 2: Test design techniques (9-15)
9. What is equivalence partitioning?
Split the input space into classes that the system should treat the same way, then test one representative from each class. For an age field that accepts 18-65: partitions are below 18 (invalid), 18-65 (valid), above 65 (invalid), plus non-numeric and empty (invalid). Five tests instead of hundreds.
The insight interviewers want: partitions come from the spec and the code's branches, not just the obvious numeric ranges. If the code treats 18-25 differently for insurance pricing, that is a separate partition.
10. What is boundary value analysis?
Bugs cluster at the edges of partitions because that is where < and <= get confused. For 18-65 inclusive, test 17, 18, 19 and 64, 65, 66. Two-value BVA tests each boundary and its neighbour outside (17, 18, 65, 66); three-value adds the neighbour inside.
const cases = [
{ age: 17, valid: false },
{ age: 18, valid: true },
{ age: 65, valid: true },
{ age: 66, valid: false },
];
for (const { age, valid } of cases) {
test(`age ${age} is ${valid ? 'accepted' : 'rejected'}`, () => {
expect(isEligible(age)).toBe(valid);
});
}
Follow-up: "what about strings?" Boundaries exist there too - empty string, max length, max length plus one, multibyte characters that are one character but several bytes.
11. What is pairwise (all-pairs) testing?
When a feature has many parameters, testing every combination explodes. Five settings with three values each is 3^5 = 243 combinations. Pairwise testing picks a much smaller set in which every pair of values appears together at least once, on the observation that most interaction bugs involve two factors. Tools like Microsoft's PICT generate the set from a model file.
The gotcha: pairwise does not catch bugs that need three specific values together, and it ignores which combinations matter most to the business. Combine it with risk - always include the configuration most of your users actually run.
12. What is a decision table, and when do you use one?
A table of conditions (rows) against rules (columns), with the expected action for each rule. Use it when behaviour depends on combinations of booleans - discount rules, permission checks, shipping eligibility.
| Condition | R1 | R2 | R3 | R4 |
|---|
| Member? | Y | Y | N | N |
| Order over $50? | Y | N | Y | N |
| Free shipping | Yes | No | Yes | No |
| Discount | 10% | 10% | 0% | 0% |
Each column becomes a test. The table also exposes gaps in the spec - if product cannot tell you what R4 should do, you found a bug before writing code.
13. What is state transition testing?
Model the system as states and the events that move between them, then test valid transitions, invalid transitions and sequences. An order goes pending -> paid -> shipped -> delivered, with cancelled reachable only from pending and paid. Tests: every valid edge, plus attempts at invalid ones ("cancel a delivered order" must fail), plus loops ("pay twice").
14. What is property-based testing?
Instead of hand-picked examples, you state a property that must hold for all inputs and let the framework generate hundreds of random inputs, shrinking any failure to a minimal case. In TypeScript, fast-check is the common library.
import fc from 'fast-check';
test('formatting then parsing a price round-trips', () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 10_000_000 }), (cents) => {
expect(parsePrice(formatPrice(cents))).toBe(cents);
}),
);
});
Good for parsers, serialisers, sorting, maths. Follow-up: "how do you reproduce a failure?" The framework prints the seed and the shrunk counterexample - pin both into a regular example test.
15. How do you test something with no written requirements?
Interviewers ask this because it is the normal case. Answer with a process: talk to the people who own it (product, the developer, support) to learn intent; read the code and existing tests for implied rules; compare with similar features and competitors; run time-boxed exploratory sessions with charters ("explore checkout with expired cards to discover how errors are reported"); write down the assumptions you are testing against and get them confirmed. The deliverable includes the missing requirements you surfaced.
Part 3: Playwright, Selenium and Cypress (16-24)
16. How do Playwright, Selenium and Cypress compare?
| Playwright | Selenium | Cypress |
|---|
| Architecture | Drives browsers over their automation protocols from an out-of-process test runner | W3C WebDriver protocol via browser drivers; WebDriver BiDi adds event-driven features | Runs test code inside the browser alongside the app |
| Languages | TypeScript/JavaScript, Python, Java, .NET | Java, Python, C#, JavaScript, Ruby and more | JavaScript/TypeScript |
| Browsers | Chromium, Firefox, WebKit | Every browser with a WebDriver implementation | Chrome-family and Firefox; WebKit experimental |
| Waiting | Auto-waits on actions; web-first assertions retry | Explicit waits you write yourself | Retries queries and assertions automatically |
| Multiple tabs / origins | Native (pages, contexts) | Native (window handles) | Limited; cy.origin for cross-origin |
| Parallelism | Built-in workers and --shard | Selenium Grid or a cloud grid | Parallel across CI machines coordinated by Cypress Cloud |
How to answer "which would you pick?": Playwright is the default for new TypeScript web projects; Selenium wins where you need a language Playwright does not support, a huge existing suite, or a browser/device grid vendor that speaks WebDriver; Cypress has an excellent interactive developer experience for single-origin front-end apps. Then name what would change your mind.
17. What is auto-waiting in Playwright, and what does it not cover?
Before an action like click(), Playwright waits for actionability checks: the element is attached, visible, stable (not animating), enabled, and receives events (not covered by another element). Web-first assertions such as await expect(locator).toHaveText('Saved') retry until they pass or the expect timeout (5 seconds by default) runs out.
What it does not cover: application-level readiness. If a button is visible and enabled but its click handler is not wired until a script finishes hydrating, Playwright will happily click it and nothing happens. It also does not know your data is eventually consistent. The fix is to assert on an outcome the user would see, not to add waitForTimeout.
await page.waitForTimeout(3000);
expect(await page.locator('.status').textContent()).toBe('Saved');
await expect(page.getByRole('status')).toHaveText('Saved');
The gotcha in the bad version: textContent() reads once. Only expect(locator) assertions retry.
18. Which locator strategy do you prefer, and why?
Priority order in Playwright: getByRole (with an accessible name), getByLabel, getByPlaceholder, getByText, then getByTestId, and CSS or XPath only as a last resort.
await page.getByRole('button', { name: 'Place order' }).click();
await page.getByLabel('Email').fill('ada@example.com');
await page.getByTestId('cart-total');
Role and label locators mirror how users and assistive technology find elements, so they survive refactors of classes and DOM structure, and they double as a light accessibility check - if you cannot locate a button by role and name, a screen reader user probably cannot either. Test IDs are stable but invisible to users. Long XPath like //div[3]/span[2] breaks on any layout change.
Follow-up: Playwright locators are strict - an action on a locator that matches several elements throws, instead of silently using the first. Narrow it with .filter() or chaining rather than reaching for .first().
19. How do you handle waits in Selenium?
Selenium does not auto-wait on actions. Options:
- Implicit wait: a global timeout for element lookup. Simple, but it only covers finding elements, not their state.
- Explicit wait:
WebDriverWait with a condition (elementToBeClickable, visibilityOfElementLocated). The standard choice.
- Fluent wait: explicit wait with custom polling interval and ignored exceptions.
The classic gotcha: mixing implicit and explicit waits. The Selenium documentation warns against it because the combined timeouts become unpredictable. Pick explicit waits and set the implicit wait to zero.
20. What is Playwright's browser context, and why does it matter?
A browser context is an isolated, incognito-like session inside one browser process - its own cookies, storage and cache. Playwright Test gives every test a fresh context by default, which is cheap (milliseconds, not a new browser launch) and is the main reason tests can run in parallel without leaking state into each other.
Two contexts in one test simulate two users (a buyer and a seller chatting), and storageState starts a context already logged in.
21. How do you avoid logging in through the UI in every test?
Log in once in a setup project, save the authenticated state, and reuse it.
import { test as setup, expect } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_USER!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'chromium',
use: { storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
Keep one real UI login test so the login page itself stays covered. Gotchas: add the .auth folder to .gitignore, and if tests mutate account state, give each parallel worker its own account.
22. How do you handle iframes, new tabs and file downloads in Playwright?
const card = page.frameLocator('#payment-frame');
await card.getByLabel('Card number').fill('4242424242424242');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open invoice' }).click();
const popup = await popupPromise;
await expect(popup).toHaveURL(/invoice/);
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.csv$/);
The pattern interviewers look for: start waiting for the event before the action that triggers it, then await it. Awaiting after the click races the event.
23. What does Playwright's trace viewer give you?
A trace is a recording of a test: every action with before/after DOM snapshots, screenshots, console logs, network requests and the source line. The common config is trace: 'on-first-retry', so a trace is only kept when a test failed and was retried - cheap in normal runs, invaluable when CI fails and you cannot reproduce locally. Open it with npx playwright show-trace trace.zip or upload it as a CI artifact. Mentioning traces as your first debugging step for a CI-only failure is a strong signal.
24. When would you still choose Selenium in 2026?
When the team writes Java, C# or Ruby and wants tests in the same language and build as the product; when there is a large, healthy existing suite and a rewrite would not pay back; when you run on a device or browser cloud whose primary interface is WebDriver; or when you need a browser Playwright does not drive. Selenium also continues to evolve - Selenium Manager handles driver downloads automatically, and the project is adopting the W3C WebDriver BiDi standard for event-driven features such as console and network listening. "Selenium is dead" is a red flag answer.
Part 4: Framework design and test data (25-32)
25. What is the Page Object Model?
A pattern where each page or component gets a class that owns its locators and exposes intent-level methods, so tests read like user stories and a UI change is fixed in one place.
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly email: Locator;
readonly password: Locator;
readonly submit: Locator;
readonly error: Locator;
constructor(private readonly page: Page) {
this.email = page.getByLabel('Email');
this.password = page.getByLabel('Password');
this.submit = page.getByRole('button', { name: 'Sign in' });
this.error = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submit.click();
}
}
Keep assertions in tests, not in page objects (a page object that asserts hides what the test checks), and return locators so tests can use web-first assertions on them.
26. What are the criticisms of the Page Object Model?
- God objects: one
AppPage with 200 methods. Fix: model components (header, cart drawer, date picker), not just pages.
- Inheritance trees:
BasePage -> AuthenticatedPage -> AdminPage couples everything. Prefer composition.
- Abstraction for its own sake: wrapping
click() in clickLoginButton() adds a layer without adding meaning. With good role-based locators, a short test can be clearer inline.
- Hidden waits and assertions inside methods make failures hard to read.
Alternatives people mention: the Screenplay pattern (actors performing tasks, popular in Serenity), and Playwright fixtures, which inject ready-to-use page objects or logged-in state into tests. The balanced answer: POM is still useful, but keep page objects thin and favour fixtures and composition.
27. What are Playwright fixtures and why are they better than beforeEach?
Fixtures are typed, composable setup and teardown that a test requests by name. They run only when used, can be scoped per test or per worker, and the teardown runs after use() even when the test fails.
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login-page';
type Fixtures = { loginPage: LoginPage; seededUser: { email: string; password: string } };
export const test = base.extend<Fixtures>({
seededUser: async ({ request }, use) => {
const user = { email: `qa+${Date.now()}@example.com`, password: 'S3cure!pass' };
await request.post('/api/test/users', { data: user });
await use(user);
await request.delete(`/api/test/users/${encodeURIComponent(user.email)}`);
},
loginPage: async ({ page }, use) => {
const lp = new LoginPage(page);
await lp.goto();
await use(lp);
},
});
Compared to beforeEach: no shared mutable variables, setup only for tests that need it, and type safety on what the test receives.
28. How do you manage test data?
Principles: each test creates the data it needs, owns it, and does not depend on data another test made. Options from most to least isolated:
- Create via API in a fixture (fast, isolated, cleans up after itself) - usually the best choice for e2e.
- Factories / builders in unit and integration tests (
buildUser({ role: 'admin' })) with sensible defaults so tests only state what matters.
- Seeded database snapshots or containers per run (Testcontainers spins up a real Postgres for the test session).
- Shared static fixtures in a long-lived environment - cheapest to start, worst for parallel runs and flakiness.
Gotchas: unique identifiers per test (Date.now() alone can collide across parallel workers - add the worker index or a UUID), never use real customer data (privacy and compliance), and plan cleanup for when a run is killed mid-way (tag test data so a sweeper can remove it).
29. What is the difference between a mock, a stub, a spy and a fake?
| Double | What it does | Example |
|---|
| Dummy | Passed but never used | A logger argument a function ignores |
| Stub | Returns canned answers | getRate() always returns 1.2 |
| Spy | Records how it was called (and may call through) | Assert sendEmail was called once |
| Mock | Pre-programmed with expectations, fails if they are not met | Expects charge(100) exactly once |
| Fake | A working lightweight implementation | In-memory repository instead of Postgres |
The follow-up: "what is the risk of mocking?" Over-mocked tests pass while the real integration is broken, and they lock in implementation details so every refactor breaks them. Mock at boundaries you do not own (payment provider, email), not your own internal classes.
30. How do you mock network calls in a Playwright test?
page.route() intercepts requests matching a pattern and lets you fulfil, modify or abort them.
test('shows an error banner when the API fails', async ({ page }) => {
await page.route('**/api/orders', (route) =>
route.fulfill({ status: 500, json: { message: 'Internal error' } }),
);
await page.goto('/orders');
await expect(page.getByRole('alert')).toContainText('could not load your orders');
});
Use it to test states that are hard to produce for real - errors, empty lists, slow responses, a third-party widget being down. For unit and component tests, Mock Service Worker (MSW) does the same at the network layer and can be shared between the test runner and local development. The trap: a suite that mocks every call is no longer an e2e suite. Keep at least the critical journeys running against a real backend.
31. How do you test an API, and what do you check beyond the status code?
Check: status code, response schema (types, required fields), values, headers (content type, caching, security headers), error format for bad input, auth (401 without a token, 403 with the wrong role), idempotency of retries, pagination edges, and response time.
import { test, expect } from '@playwright/test';
test('POST /api/orders creates an order', async ({ request }) => {
const res = await request.post('/api/orders', {
data: { sku: 'BOOK-1', quantity: 2 },
});
expect(res.status()).toBe(201);
const body = await res.json();
expect(body).toMatchObject({ sku: 'BOOK-1', quantity: 2, status: 'pending' });
expect(body.id).toEqual(expect.any(String));
const fetched = await request.get(`/api/orders/${body.id}`);
expect(fetched.ok()).toBeTruthy();
});
test('rejects a negative quantity', async ({ request }) => {
const res = await request.post('/api/orders', { data: { sku: 'BOOK-1', quantity: -1 } });
expect(res.status()).toBe(400);
});
Follow-up: validate against the OpenAPI schema rather than hand-listing fields, so a contract change fails loudly. The REST API design cheat sheet covers the status codes and idempotency rules these tests assert.
32. Where should your automation code live, and who owns it?
In the same repository as the product code where possible, run in the same pipeline and reviewed like product code. Separate test repos drift and failures become "QA's problem". Ownership answer: developers own unit and integration tests for their changes; SDETs own the framework, the e2e layer's architecture, test infrastructure and coaching. A team where only QA writes tests is a team where tests are always late.
Part 5: Flaky tests (33-36)
33. What causes flaky tests?
A flaky test passes and fails on the same code. Common causes, roughly in order of how often they show up:
- Timing: fixed sleeps, missing waits, asserting before an async update lands, animations.
- Shared state: tests that depend on order, share accounts or data, or leave state behind.
- Environment: slower CI machines, different time zone or locale, screen size, fonts.
- External dependencies: third-party APIs, rate limits, an unstable staging service.
- Non-determinism in the code: random IDs,
new Date(), unordered collections, race conditions - sometimes the flaky test is reporting a real bug.
- Resource limits: too many parallel workers for the CI runner, causing timeouts.
The senior point is number 5: never assume flakiness is the test's fault until you have checked.
34. How do you fix a flaky test?
Process: reproduce, find the cause, fix the cause, prove it.
- Reproduce: run it in a loop (
npx playwright test login.spec.ts --repeat-each=50), with CI-like worker counts and CPU limits.
- Diagnose: trace on failure, compare passing and failing traces, look at the network tab and console.
- Fix the cause: replace sleeps with web-first assertions, make data unique per test, mock the third party, freeze the clock (
page.clock in Playwright) for time-based UI.
- Prove it: the same 50-run loop now passes.
What not to do: bump the timeout or wrap it in retries and walk away.
35. Are test retries good or bad?
Both. Retries in CI (retries: 2 in Playwright config) keep one unlucky run from blocking a merge, and Playwright reports a test that failed then passed as flaky rather than passed, so the signal is not lost. They become bad when nobody reads that signal - the flaky count grows, real intermittent bugs hide behind retries, and the suite gets slower.
A good policy: retries on in CI and off locally, a dashboard or report of flaky tests, a quarantine tag for known-flaky tests with an owner and a deadline, and a rule that quarantined tests are fixed or deleted, never left forever.
36. How would you reduce the flakiness of a suite that 30% of runs fail?
Treat it like an incident, not a test-by-test chore. Measure first: which tests fail, how often, on which runners. Usually a small number of tests or one shared cause (a slow login, a shared account, an overloaded staging service) explains most failures. Fix the top offenders, quarantine the rest with owners, and add guardrails: lint rules banning waitForTimeout and .only, unique data helpers, --repeat-each on new tests in the PR. Report the trend weekly. Interviewers want "data first, biggest cause first", not "rewrite the framework".
Part 6: CI, scale and non-functional testing (37-43)
37. How do you run tests in CI, and what is sharding?
A typical pipeline: lint and type-check, unit tests, build, integration tests, deploy to a preview environment, smoke and e2e, publish reports and traces as artifacts.
Playwright parallelises at two levels. Workers run test files in parallel on one machine (fullyParallel: true parallelises tests within a file too). Sharding splits the suite across machines:
strategy:
fail-fast: false
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}
Each shard uploads its blob report, and a final job merges them with npx playwright merge-reports. Follow-ups: shards are only balanced if tests are similar in length - one 10-minute test caps the whole run - and fail-fast: false stops one failing shard from cancelling the others' evidence.
38. Which tests run on a pull request, and which run later?
On every PR: lint, types, unit, integration, and a fast e2e smoke set - the target is feedback in minutes. On merge to main: the full e2e regression. On a schedule: long-running suites, cross-browser matrices, performance and security scans. After deploy: smoke against the real environment. Tag tests (@smoke, @slow) and select with --grep.
39. How do you test features built on LLMs?
LLM output is non-deterministic, so exact-match assertions do not work. Layer it:
- Deterministic parts stay normal tests: prompt construction, tool-call parsing, output schema validation, retries, timeouts, cost limits. Mock the model for these.
- Structural checks on real output: valid JSON against a schema, required fields present, length limits, no banned content, citations point to real sources.
- Evaluation sets: a fixed dataset of inputs with expected properties or reference answers, scored by rules, by human review, or by another model acting as a judge (with its own spot-checks, because judges are biased too). Track the score over time and gate releases on regressions.
- Adversarial tests: prompt injection, jailbreak attempts, off-topic requests, PII leaks.
- Production monitoring: sample real conversations, collect user feedback, alert on spikes in refusals or errors.
The interview point: you are testing a distribution, not a single answer, so assert on properties and track scores. The LLM prompt engineering cheat sheet covers the prompt side you will be asked to reason about.
Types: load (expected traffic), stress (beyond expected, to find the breaking point), spike (sudden jumps), soak/endurance (steady load for hours to find leaks). Common tools: k6 (scripts in JavaScript), JMeter, Gatling, Locust.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 50,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://staging.example.com/api/products');
check(res, { 'status is 200': (r) => r.status === 200 });
}
Gotchas: report percentiles, not averages (an average hides the slow tail users feel); test in an environment sized like production; warm up before measuring; and know whether your load generator is the bottleneck. Never load test production or a third party without explicit permission.
41. How do you include accessibility in automated testing?
Run an automated scanner such as axe-core inside your e2e tests, and fail on serious violations.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('checkout page has no detectable WCAG A/AA violations', async ({ page }) => {
await page.goto('/checkout');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
The honest caveat: scanners catch only a portion of issues - missing labels, contrast, invalid ARIA. Keyboard navigation, focus order, meaningful alt text and screen reader flow still need manual checks.
42. Is code coverage a good metric?
Coverage tells you which lines (or branches) ran during tests. It does not tell you whether anything was asserted. A test that calls every function and asserts nothing scores 100%.
Useful as: a way to find untested areas, and a guard against coverage dropping on new code (diff coverage on a PR). Harmful as: a target. Once a team is rewarded for a number, it writes tests that hit lines rather than tests that catch bugs - Goodhart's law. If the interviewer pushes for something better, mention mutation testing (Stryker for JavaScript/TypeScript): it changes the code in small ways (< to <=) and checks whether any test fails. Surviving mutants show weak assertions.
43. How do you test across browsers and devices without exploding run time?
Run the full suite on one primary browser on every PR, and the cross-browser matrix (Playwright projects for Chromium, Firefox and WebKit, plus mobile viewports) on main or nightly. Emulated mobile viewports are not real devices - critical flows still get a real-device pass before release. Prioritise by what your users actually run.
Part 7: Bugs, triage and working with developers (44-46)
44. What makes a good bug report?
A developer should be able to reproduce it without asking you a question.
- Title: what is broken and where - "Checkout total ignores discount code when cart has 10+ items".
- Environment: build or commit, browser, device, account type, feature flags.
- Steps to reproduce: numbered, minimal, with test data.
- Expected vs actual, with a screenshot, video or trace.
- Frequency: always, or 3 of 10 runs.
- Evidence: logs, request IDs, console errors.
- Impact: who is affected and how badly.
The skill behind it is minimising the reproduction; cutting 20 steps to the 3 that matter often reveals the cause.
45. What is the difference between severity and priority?
Severity is technical impact (data loss, crash, cosmetic). Priority is how soon the business needs it fixed. They usually line up but not always: a typo in the company name on the home page is low severity, high priority; a crash in an admin tool used once a year is high severity, low priority. Testers usually propose severity; product owns priority. In triage, your job is to give the facts that let them decide - how many users, workaround available, data at risk.
46. A developer says your bug is "works on my machine". What do you do?
Do not argue - narrow the difference. Compare environments (build, config, flags, browser, data, user role), share the trace or recording, and try to reproduce together on their machine with your steps and data. Often the difference is the bug: a missing environment variable, a migration not applied, a race only visible on slower hardware. If it is intermittent, show the frequency data.
Part 8: Live scenarios (47-50)
These are the shape of most live SDET rounds: a small spec, an editor, and 30-45 minutes. Narrate what you would test before you type.
The form has email, password, a "Sign in" button, and shows "Invalid email or password" on failure. Start by listing cases out loud: happy path, wrong password, unknown email (same message - no account enumeration), empty fields, invalid email format, password field masked, Enter key submits, account lockout after repeated failures (ask whether it exists), accessibility of the error.
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login-page';
test.describe('login', () => {
let login: LoginPage;
test.beforeEach(async ({ page }) => {
login = new LoginPage(page);
await login.goto();
});
test('valid credentials land on the dashboard', async ({ page }) => {
await login.login('ada@example.com', 'correct-horse');
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('wrong password shows a generic error and stays on login', async ({ page }) => {
await login.login('ada@example.com', 'wrong');
await expect(login.error).toHaveText('Invalid email or password');
await expect(page).toHaveURL(/\/login/);
});
test('unknown email shows the same error (no account enumeration)', async () => {
await login.login('nobody@example.com', 'whatever');
await expect(login.error).toHaveText('Invalid email or password');
});
test('empty submit does not send a request', async ({ page }) => {
let called = false;
await page.route('**/api/login', (route) => {
called = true;
return route.continue();
});
await login.submit.click();
await expect(login.email).toBeFocused();
expect(called).toBe(false);
});
});
Then say what you would not test in e2e: lockout counters and rate limits belong in API or unit tests. The "empty submit" test assumes native required validation with focus moving to the first invalid field - state the assumption.
48. "This test passes locally but fails in CI. Walk me through it."
Have a checklist and say it in order:
- Read the failure, not the log tail: open the trace, screenshot and error from the CI artifact. Is it a timeout, an assertion mismatch or a crash?
- Timing: CI machines are slower and run more workers. Look for sleeps, non-retrying assertions (
textContent() then expect), and actions on elements before hydration. Reproduce with --workers matching CI and --repeat-each.
- State and order: does it pass alone in CI but fail in the full run? Then another test is leaking data or sharing an account. Sharding changes which tests run together.
- Environment differences: base URL, environment variables, secrets missing in CI, time zone (CI runners often use UTC), locale and number formatting, viewport size, fonts for screenshot tests, headless vs headed.
- Data: the local database has records CI's fresh database does not.
- Dependencies: a third-party call that is fast from your laptop and rate-limited from CI's IP range.
Then fix the cause, not the symptom, and prove the fix with a repeated run. The live debugging interview round covers how interviewers score this narration.
49. "Find the problems in this test."
test('add to cart', async ({ page }) => {
await page.goto('https://staging.example.com/products/42');
await page.click('#root > div > div:nth-child(3) > button');
await page.waitForTimeout(2000);
const count = await page.locator('.cart-count').textContent();
expect(count).toBe('1');
});
Problems to call out:
- Hard-coded URL: use
baseURL from config so it runs against any environment.
- Brittle CSS path: use
getByRole('button', { name: 'Add to cart' }).
- Fixed sleep: slow on fast runs, still flaky on slow ones.
- Non-retrying read:
textContent() reads once; use await expect(page.getByTestId('cart-count')).toHaveText('1').
- Hidden data dependency: assumes product 42 exists and the cart starts empty - if the test runs logged in with a shared account, a previous run's item makes the count 2.
The rewritten test is four lines and has no sleeps.
50. "Design a test strategy for a new checkout feature."
Structure the answer by risk, then by layer:
- Risks: charging the wrong amount, double charging, losing an order, tax and discount errors, payment provider failures, accessibility of the payment form.
- Unit: price, tax, discount and rounding rules with boundary and decision-table cases; property tests for totals.
- Integration/API: order creation, idempotency key on payment (retrying must not charge twice), inventory reservation, provider webhooks including out-of-order and duplicate delivery.
- Contract: between checkout and the payment and inventory services.
- E2E: a few critical journeys - guest checkout, logged-in checkout with a saved card, declined card - against the provider's sandbox; mock the provider for error states.
- Non-functional: load test the checkout API at expected peak plus headroom, axe scan the forms, security review of card handling.
- Release: behind a feature flag, canary to a small percentage, synthetic checkout monitor in production, alerts on payment error rate.
Close with what you would not do and why - that is where seniority shows.
How to practice for a QA automation or SDET interview
- Build a small suite from scratch. Pick any public demo app, write ten Playwright tests in TypeScript with a page object, a fixture, API-created test data and a mocked error state. Doing it once teaches more than reading about it.
- Break your own tests on purpose. Add a
waitForTimeout, share an account between tests, change the time zone, then run with --repeat-each=20 and watch the flakiness appear. Fixing flakiness you caused is the fastest way to recognise it in an interview.
- Wire it into CI with sharding and traces. A GitHub Actions workflow with four shards, merged reports and traces uploaded on failure is a concrete thing to describe when asked about CI.
- Practise the test-design techniques on paper. For any form you use this week, list its partitions, boundaries and a decision table. It takes five minutes and makes question 15 easy.
- Keep your coding sharp. Many SDET loops include a standard coding round; the coding challenges and the JavaScript and TypeScript banks cover the language questions that come with it.
- Narrate out loud. In live rounds, listing test cases before writing code is often worth more than the code itself. Practise saying "here is what I would test and why" until it is automatic.
Keep going
- Follow a path: the QA and SDET prep track puts these topics in order, and the frontend engineer prep track covers the browser and UI side automation depends on.
- Drill it under pressure: the TypeScript, JavaScript and DevOps question banks run as timed multiple-choice, and the free daily challenge gives you one a day with no account.
- Keep the references open: the REST API design cheat sheet for API testing and the LLM prompt engineering cheat sheet for testing AI features.
- Round out the loop: Top 50 TypeScript interview questions, Top 50 JavaScript interview questions, Top 50 DevOps interview questions, and the live debugging interview round for the "it fails in CI" scenario.
- Practise the follow-ups: the chat-based AI mock interview probes your reasoning with the kind of "and what if the test only fails on the third run?" follow-ups a real SDET round runs on.
- Related guides: Top 50 Spring Boot Interview Questions 2026, Top 50 Technical Program Manager (TPM) Interview Questions 2026.
#qa #sdet #testautomation #playwright #interviews #testing #career