All jobs / Automation developer
Web Automation interview questions
Web Automation questions a Automation developer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
A test fails on a slow CI runner. A teammate adds sleep(5000). What do you do instead?
The strongest answer: Wait for the condition that means the page is ready — a visible name, a network idle you can name, a disabled button becoming enabled — not a guessed number of seconds
Why it works: Exactly. Wait for a signal. Sleep is a guess that is too short on a bad day and too slow on a good one.
- Wait for a named ready condition.
- Sleep is a guess, not a wait.
- Same wait policy on laptop and CI.
- Do not delete the oracle to go green.
Which locator do you reach for first?
The strongest answer: A role, label, test id, or other name the product owns — not nth-child, a pixel offset, or a CSS class that is a styling leftover
Why it works: Exactly. Prefer what a user or a contract would name. Styling leftovers move.
- Role, label, or test id first.
- Do not use nth-child as a habit.
- Recorded locators get edited.
- Visible copy is a weak contract.
What is a page object for?
The strongest answer: To name the page’s jobs — “submit checkout”, “read the total” — so tests do not scrape CSS in every file
Why it works: Exactly. A page object is an API for the page. Locators live once.
- Name user jobs, not CSS.
- Locators live in one place.
- Keep asserts in the test.
- Do not mirror the whole DOM.
The third “Add” button is the one you need. What do you ask the team for?
The strongest answer: A stable name — role, label, or data-testid on that control — not :nth-of-type(3)
Why it works: Exactly. Ask for a name the product will keep. Index selectors are a layout change away from lying.
- Ask for a stable name.
- Indexes are layout-fragile.
- Comments are not locators.
- Do not click coordinates.
What is a browser context, and why does each test usually get a new one?
The strongest answer: An isolated profile inside one browser process — its own cookies, storage and permissions — so tests never inherit each other’s login, and it starts far faster than a fresh browser
Why it works: Right — isolation without the cost of a cold browser is exactly why contexts exist.
- A context is an isolated profile.
- Cookies and storage do not leak.
- Cheaper than launching a browser.
- One per test by default.
When is a fixed wait acceptable?
The strongest answer: Almost never — maybe a last-resort around a third-party with no signal, and then it is a ticket, not a pattern
Why it works: Exactly. A number is a guess. If you must, own it as debt.
- Prefer a condition.
- A fixed wait is debt.
- Do not sleep “just in case.”
- CI slowness needs a signal, not a number.
Playwright can click a hidden overlay. Why do you still prefer getByRole('button', { name: 'Pay' })?
The strongest answer: Because it fails when a user could not find or activate that control — the test shares the accessibility tree, not a backdoor into the DOM
Why it works: Exactly. A role+name click is closer to a keyboard and a reader. force:true is a backdoor.
- Prefer the accessibility tree.
- Hidden overlays should fail.
- force:true is a last resort.
- Speed is not the point.
When do you reach for screenplay / actor + task instead of a page object?
The strongest answer: When the same actor does the same job across many screens — “checkout as a returning buyer” — and page objects are turning into dump trucks of methods
Why it works: Exactly. Screenplay is for repeated actor-jobs. Page objects still win for a single screen’s API.
- Actors + tasks for cross-screen jobs.
- Page objects for one screen’s API.
- Do not fashion-ban POM.
- Do not use a pattern to hide asserts.
You turn on 8 workers and 20 tests collide on the same user. What was missing?
The strongest answer: Per-test identity — users, carts, and rows namespaced by worker or uuid — parallelism without isolation is a race you scheduled
Why it works: Exactly. Parallel is a data-design problem. Workers are cheap; unique fixtures are the work.
- Namespace data per worker / test.
- Parallel without isolation is a race.
- Do not mutex the whole suite.
- Docs are not isolation.
When is a visual snapshot the right automated check?
The strongest answer: When layout, theme, or a design token can break and a DOM assert would stay green — and you have a review path for intentional diffs
Why it works: Exactly. Visuals catch what text asserts miss. They need a human on intentional change.
- Visuals for layout / theme risk.
- Review intentional diffs.
- Do not snapshot the world.
- Do not skip because “design is taste.”
The spinner is gone but the table is still empty for 200ms. What do you wait for?
The strongest answer: The condition that means data arrived — a row, an empty-state you authored, or a response you stubbed — not “spinner detached”
Why it works: Exactly. Ready is a product signal. Spinners lie; networkidle lies on chatty pages.
- Wait for the data or the empty state.
- Spinners are not oracles.
- Do not sleep after the spinner.
- Use networkidle sparingly.
A designer removes data-testid attributes “for a clean DOM.” What do you say?
The strongest answer: test ids are a supported contract, like a class we own — hide them from production if you must, but do not treat tests as uninvited guests
Why it works: Exactly. A test id is an API. Stripping it without a replacement is breaking a consumer.
- Treat test ids as an API.
- Offer a replacement, do not strip.
- nth-child is not a cleanup.
- Do not sneak a worse locator.
A teammate commits a raw Cypress / Playwright codegen file. What do you change?
The strongest answer: Names, locators, waits, and asserts — the recorder is a draft of the path, not a finished test
Why it works: Exactly. Recorders save a path. Engineers still own locators and oracles.
- Edit locators and waits.
- Add a real oracle.
- Codegen ≠ done.
- Do not double-record as quality.
Every one of your 300 UI tests starts by typing a username and password into the login form.
How do you cut that, and what do you keep?
The strongest answer: Log in once in setup, save the cookies and local storage, and load that state into each test’s context; keep a few real tests that still drive the login form itself
Why it works: Exactly — skip the login you are not testing, and keep the ones you are.
- Do not re-test the login 300 times.
- Save the signed-in state once.
- Load it per test, in a fresh context.
- Keep real login tests on purpose.
A design-system button lives in shadow DOM. How do you locate it?
The strongest answer: Through the component’s public API or a pierce that the team documents — I do not copy an internal shadow path that will move in the next kit version
Why it works: Exactly. Treat the kit as a vendor. Use what they promise, not the guts.
- Use the public component API.
- Document any pierce.
- Do not scrape shadow guts.
- Do not skip the control.
Visual tests fail every week on a one-pixel anti-alias. What do you change?
The strongest answer: Tighten the surface you snapshot, ignore motion, and set a reviewed threshold — I do not auto-accept every diff to keep CI quiet
Why it works: Exactly. Visuals need a small, still surface and a human on real diffs.
- Snapshot a still, small surface.
- Review real diffs.
- Do not auto-accept.
- Do not include clocks.
A teammate declares let total = 0 at the top of a Cypress test, assigns it inside a .then(), and reads it on the next line.
Why is it always zero, and how do you write it instead?
The strongest answer: Cypress queues commands and runs them after the test body has finished, so the next line executes before the .then() does; keep the work inside the chain, or alias the value and read it with cy.get
Why it works: Right — the body enqueues commands, it does not run them. That one fact explains most Cypress surprises.
- The test body only enqueues commands.
- Nothing has run when the body ends.
- Work inside .then(), or use aliases.
- A wait is not a fix for ordering.
Playwright offers getByRole, getByText, getByTestId. What is your default order?
The strongest answer: Role + name, then label, then test id, then text as a last resort — I want the test to fail when a user could not find the control
Why it works: Exactly. Role+name is the user path. Test id is the escape hatch you own. Text and CSS are brittle.
- Role + name first.
- Test id when the role is weak.
- Text last.
- CSS last-last.
A page object method is named clickButton3 and contains a business if/else. What do you do?
The strongest answer: Rename it to the user job and move the business branch to a helper or the app — page objects click and read; they do not hide pricing rules
Why it works: Exactly. A page object is an API for the screen, not a second application.
- Name the user job.
- Keep rules out of the POM.
- Do not paste the branch everywhere.
- Do not burn the POM over one smell.
A test intercepts /orders and returns a 200 you wrote. What must the test still prove?
The strongest answer: That the UI handled that contract — and that a contract or sandbox test still proves the real /orders — a stub is not the backend
Why it works: Exactly. An intercept proves the client. Someone else must prove the server.
- Intercepts prove the client.
- Contracts or sandboxes prove the server.
- A stubbed 200 is not the feature.
- Keep a real path somewhere.
How do you keep a visual pack from becoming a flip-the-baseline ritual?
The strongest answer: Few surfaces, frozen fonts and motion, review required, and owners for each snapshot family — I do not snapshot every route “for coverage”
Why it works: Exactly. Visuals are a scarce, owned pack. Rituals mean you snapshotted weather.
- Few, still, owned surfaces.
- Human review on diffs.
- No auto-merge of baselines.
- No album of the whole site.
A Selenium suite sets a ten-second implicit wait in the driver factory, and individual tests also use WebDriverWait. Timeouts come out wildly inconsistent.
What is happening?
The strongest answer: Implicit and explicit waits compound: each polled lookup inside the explicit wait can block for the implicit timeout first, so the real wait is unpredictable. Set the implicit wait to zero and wait explicitly
Why it works: Exactly — that is the documented reason not to mix them, and the fix is one line in the factory.
- Never mix implicit and explicit waits.
- Set the implicit wait to zero, globally.
- Wait explicitly for the condition you need.
- Sleeps are not a wait strategy.
What is a selector standard worth writing down?
The strongest answer: The order (role, label, test id), who may add test ids, and a lint that bans nth-child in CI — a page in the paved road, not a novel
Why it works: Exactly. A short, enforced order. Lint beats a lecture.
- Write a short order.
- Lint the bans.
- Name who owns test ids.
- Do not write a novel.
Cypress is in place; a new squad wants Playwright. How do you decide?
The strongest answer: By jobs: component vs e2e, trace quality, languages the team writes, and migration cost — I do not pick a winner from Twitter
Why it works: Exactly. 2026 has room for both; two stacks everywhere is the failure mode.
- Choose by jobs and cost.
- Do not fashion-ban a working stack.
- Sunk cost is one input.
- Do not run both on every path.
A visual test is green and a keyboard user cannot reach Pay. What was missing?
The strongest answer: An a11y path — axe on the journey plus a real keyboard (or role-based) walk — pixels can pass while the tree is broken
Why it works: Exactly. Visuals are layout. Names, order, and focus are a different oracle.
- Keep a keyboard / role walk.
- Scanners help; they do not replace.
- Visual ≠ a11y.
- Do not raise DPI and call it done.
What belongs in a wait policy the whole org can use?
The strongest answer: Preferred signals (role ready, response, empty state), banned sleeps, and when networkidle is allowed — a page, not a debate in every PR
Why it works: Exactly. A short, enforced policy. Feelings and magic numbers are how flakes return.
- Name allowed signals.
- Ban sleeps by default.
- Networkidle is a special case.
- Do not ban waiting.
When do you pay for Safari / a real device in CI?
The strongest answer: When a money path has failed there, or analytics show that browser is how people pay — not because a matrix slide wants “all browsers”
Why it works: Exactly. A matrix is a risk list with a bill. Fashion coverage is how you buy flakes.
- Pay for browsers that lose money.
- Use evidence, not a slide.
- Do not run the whole pack everywhere.
- Do not skip a real device you owe.
When is a visual program worth funding?
The strongest answer: When design tokens, white-label, or a store screenshot can lose money — funded review time included — not because a vendor demo looked pretty
Why it works: Exactly. Fund the risk and the reviewers. A demo is not a program.
- Fund risk + review time.
- Tokens, brands, store shots.
- Do not buy a demo.
- Do not auto-accept.
Selenium, Cypress and Playwright — how do they differ architecturally, and what does that cost a team?
The strongest answer: Selenium drives from outside over WebDriver: any language, any grid. Cypress runs inside the browser: great debugging, awkward multi-tab and cross-origin work. Playwright drives isolated contexts over one connection: cheap parallel setup
Why it works: Right. Each architecture buys something and bills for something, and a lead should be able to say which.
- Architecture decides the limits.
- Out-of-process: any language, any grid.
- In-browser: great debugging, origin limits.
- Name the cost, not only the name.
How do you make locator quality survive 20 squads?
The strongest answer: A lint in CI, a design-system API, and test-id ownership in the component kit — culture follows the gate, not a lunch-and-learn
Why it works: Exactly. Put the standard in the kit and the pipeline. Workshops fade.
- Lint + kit API + owners.
- Gates beat workshops.
- Docs without lint fade.
- Do not hire your way out.