{"id":2027,"date":"2026-07-19T23:01:59","date_gmt":"2026-07-19T17:31:59","guid":{"rendered":"https:\/\/lastroundai.com\/blog\/?post_type=iq&#038;p=2027"},"modified":"2026-07-19T23:02:00","modified_gmt":"2026-07-19T17:32:00","slug":"playwright","status":"publish","type":"iq","link":"https:\/\/lastroundai.com\/interview-questions\/playwright","title":{"rendered":"Playwright Interview Questions (2026): Q&#038;A Guide"},"content":{"rendered":"<p>A QA engineer I talked to last year described her first Playwright interview loop like this: the interviewer pulled up a flaky Selenium test suite from the company&#8217;s own repo, thirty seconds of retries and <code>Thread.sleep<\/code> calls scattered through it, and just asked &#8220;how would you fix this.&#8221; Not &#8220;what is Playwright.&#8221; Not &#8220;list the browsers it supports.&#8221; The whole thirty minutes was one broken test and a shared screen. That&#8217;s the shape most Playwright interviews actually take now, less trivia, more &#8220;here&#8217;s something ugly, make it not flaky.&#8221;<\/p>\n<p>Part of why that question works is that Playwright was built specifically to remove the class of bugs that made tools like Selenium fragile. Microsoft&#8217;s own framework performs actionability checks, visible, stable, receives events, enabled, editable, before every action, and blocks until an element actually satisfies them or the timeout runs out (<a href=\"https:\/\/playwright.dev\/docs\/actionability\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, actionability<\/a>). A candidate who can explain why that removes most sleep() calls, and where it still can&#8217;t save you, usually has real hands-on time with the tool rather than a afternoon reading the quickstart.<\/p>\n<p>This page covers Playwright interview questions across locators and selector strategy, auto-waiting and web-first assertions, test structure with fixtures and isolation, network interception and API testing, authentication and parallel execution, and debugging with the trace viewer, plus a section of harder edge cases: iframes, shadow DOM, multiple contexts, and visual regression. Code examples use TypeScript, Playwright&#8217;s primary language, throughout.<\/p>\n<div class=\"iq-stats not-prose\"><div class=\"iq-stat\"><span class=\"iq-stat__value\">50<\/span><span class=\"iq-stat__label\">Questions<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">Auto-Waiting<\/span><span class=\"iq-stat__label\">Core Mechanism<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">@playwright\/test<\/span><span class=\"iq-stat__label\">Default Test Runner<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">Chromium, Firefox, WebKit<\/span><span class=\"iq-stat__label\">Browsers Covered<\/span><\/div><\/div>\n<h2>Fundamentals: what Playwright is and how it talks to the browser<\/h2>\n<p>Every loop opens here, even for candidates who&#8217;ve shipped a full suite. The point isn&#8217;t to test memorization, it&#8217;s to see whether you can explain the mechanism, not just the marketing.<\/p>\n<div class=\"iq-dsec iq-dsec--easy\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"easy\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Easy questions<\/h2><span class=\"iq-dsec__n\">15<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is Playwright and what problem was it actually built to solve?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Playwright is an end-to-end testing and browser automation framework from Microsoft that drives Chromium, Firefox, and WebKit through a single API. It was built by engineers who had previously worked on Puppeteer, and the design goal was cross-browser automation that doesn&#8217;t need a separate driver binary per browser the way Selenium does with its WebDriver protocol.<\/p>\n<p>The bigger problem it targets is flakiness. Most broken browser tests fail because the test clicks an element before the page has finished rendering it, or before an animation settles, not because the test logic is wrong. Playwright bakes waiting and retry behavior into the API itself instead of leaving it to the test author to sprinkle sleeps and manual waits everywhere.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a BrowserContext and a Page in Playwright?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A Browser is one launched browser process. A BrowserContext is an isolated, incognito-like session inside that browser, its own cookies, local storage, and cache, completely separate from any other context in the same process. A Page is a single tab inside a context. One context can hold multiple pages, useful for testing flows that open a new tab or a popup.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst browser = await chromium.launch();\n\nconst context = await browser.newContext(); \/\/ fresh, isolated session\n\nconst page = await context.newPage();\nconst secondContext = await browser.newContext(); \/\/ completely separate cookies\/storage\n<\/code><\/pre><\/div><\/p>\n<p>Spinning up a new context is cheap compared to launching a new browser, which is exactly why the test runner creates a fresh context per test by default instead of a fresh browser.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is a Locator in Playwright, and how is it different from the old ElementHandle?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locators<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A Locator is a description of how to find an element, not a reference to a specific DOM node captured at one moment. Every time you call an action on a locator, Playwright re-queries the DOM and locates the current matching element (<a href=\"https:\/\/playwright.dev\/docs\/locators\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, locators<\/a>). An ElementHandle, by contrast, is a live reference to one specific node, and it goes stale the moment that node gets detached and re-rendered, which happens constantly in a React or Vue app.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst button = page.locator(\u2018button.submit\u2019);\n\n\/\/ button doesn\u2019t point at anything yet, it\u2019s a query\nawait button.click(); \/\/ resolves the current matching element, then clicks it\n\n\/\/ if the DOM re-rendered that button in between, this still works\n<\/code><\/pre><\/div><\/p>\n<p>ElementHandle still exists for narrow low-level cases, but the official guidance is to reach for locators for essentially everything now.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you locate an element that only has visible text, no test id, no accessible role that helps?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locators<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>getByText is the direct tool for this, and it works on exact text or a substring, with an option for exact matching. For text nested inside other markup, it still matches on the element&#8217;s accumulated text content, not just a direct text node.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait page.getByText(\u2018Order confirmed\u2019).click();\n\nawait page.getByText(\u2018Order\u2019, { exact: false }); \/\/ substring match, default behavior\n\nawait page.getByText(\u2018Order confirmed\u2019, { exact: true }); \/\/ must match exactly\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;s a reasonable fallback for read-only content like confirmation banners or toast messages where there&#8217;s no interactive role to hang getByRole off of.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a web-first assertion like expect(locator).toBeVisible() and a plain boolean check?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Assertions<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A web-first assertion, anything using <code>expect(locator)<\/code>, retries automatically until the condition is true or a timeout elapses. A plain check that reads a value once and compares it immediately has no retry built in, so it fails the instant it&#8217;s evaluated even if the element would have become visible half a second later.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait expect(page.getByText(\u2018Saved\u2019)).toBeVisible(); \/\/ retries up to the timeout\nconst isVisible = await page.getByText(\u2018Saved\u2019).isVisible();\n\nexpect(isVisible).toBe(true); \/\/ checked once, right now, no retry\n<\/code><\/pre><\/div><\/p>\n<p>The second version is a classic source of flaky tests, it passes locally where the machine is fast and fails intermittently in CI where a render is a beat slower.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why does Playwright give every test its own browser context instead of reusing one context across a whole file?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Test Isolation<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Because reusing a context means reusing its cookies, local storage, and cached auth state, and any test that logs in, logs out, or mutates client-side state contaminates whatever runs after it. A fresh context per test guarantees test order doesn&#8217;t matter and tests can run in parallel across workers without one interfering with another&#8217;s session.<\/p>\n<p>The tradeoff is cost. Spinning up a context per test is fast, but not free, which is exactly why login is usually done once via storageState reuse rather than repeating a full UI login flow inside every single test.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between Playwright&#039;s UI Mode and running with the Inspector via PWDEBUG?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Debugging<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>UI Mode is an interactive test runner with a visual timeline, time-travel through each step, a live locator picker, and a watch mode that reruns tests as you edit them, meant for active day-to-day authoring and debugging. The Inspector, triggered by setting <code>PWDEBUG=1<\/code> or calling <code>page.pause()<\/code>, pauses execution at a specific point and opens a lighter debugging panel with step controls, more useful for pinpointing exactly where inside one specific test a failure happens.<\/p>\n<p>In practice most people live in UI Mode while writing new tests and reach for <code>page.pause()<\/code> when they need to freeze execution at one exact line inside an existing test that&#8217;s already mostly working.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What browsers does Playwright actually support, and are they the same binaries as the real Chrome and Firefox?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Playwright drives Chromium, Firefox, and WebKit. Chromium is the open-source engine Google Chrome and Edge are built on, close enough that most Chrome-specific behavior applies, but it isn&#8217;t Google&#8217;s exact shipped Chrome binary with all of Google&#8217;s proprietary bits. Firefox and WebKit are patched builds that Playwright&#8217;s own team maintains specifically to add the automation hooks the framework needs, not the stock browser installs you&#8217;d download yourself.<\/p>\n<p>Because Playwright bundles and tests against these specific builds, a given Playwright version is guaranteed to work with the exact browser version it shipped with, which is why <code>npx playwright install<\/code> downloads its own browser copies instead of using whatever&#8217;s already on your machine.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What languages can you write Playwright tests in?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>TypeScript and JavaScript are the primary, most fully-featured bindings, and that&#8217;s what almost all official docs and examples use. Playwright also ships official bindings for Python, Java, and .NET, with the same underlying API shape and actionability model across all four, just adapted to each language&#8217;s conventions.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does codegen do, and is it a reasonable way to write real tests?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Tooling<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>codegen opens a browser, records your clicks and inputs as you interact with the page, and generates the corresponding Playwright code live, including reasonably sensible locators picked from the page&#8217;s roles and text.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nnpx playwright codegen https:\/\/example.com\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;s genuinely useful for scaffolding a first draft or for quickly finding a good locator for a tricky element, but the generated code usually needs cleanup afterward, assertions it never adds on its own, and locators that sometimes need tightening. Treating its raw output as a finished test is a common beginner mistake.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you test a file upload in Playwright?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Forms<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>setInputFiles sets the files on a file input directly, without needing to drive the OS&#8217;s native file picker dialog, which automation tools generally can&#8217;t interact with anyway since it&#8217;s outside the browser process.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait page.getByLabel(\u2018Upload resume\u2019).setInputFiles(\u2018fixtures\/resume.pdf\u2019);\n\nawait page.getByLabel(\u2018Upload resume\u2019).setInputFiles([]); \/\/ clears selected files\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you handle a native browser dialog like window.confirm() or an alert?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Dialogs<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Register a listener on the page&#8217;s &#8216;dialog&#8217; event before the action that triggers it, since native dialogs block the page and Playwright auto-dismisses any dialog it doesn&#8217;t have a listener for by default, which can otherwise silently hang a test.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\npage.on(\u2018dialog\u2019, async (dialog) =&gt; {\n\n  console.log(dialog.message());\n\n  await dialog.accept(); \/\/ or dialog.dismiss()\n\n});\n\nawait page.getByRole(\u2018button\u2019, { name: \u2018Delete account\u2019 }).click();\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does the retries option in playwright.config actually do, and is turning it up a fix for flaky tests?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CI<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>retries tells the runner to re-run a failed test up to that many additional times before marking it as truly failed, and it&#8217;s commonly set higher in CI than locally since CI environments are more prone to transient slowness.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nexport default defineConfig({\n\n  retries: process.env.CI ? 2 : 0,\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;s a mitigation, not a fix. A test that fails one run in five because of a real race condition in the app or the test will often pass on retry and hide that bug from view instead of surfacing it, so a suite that leans on retries to stay green without anyone investigating why tests fail the first time is accumulating debt, not stability.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you run only the tests in one file, or one test by name, instead of the whole suite?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CLI<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Pass the file path directly to the CLI, or use <code>-g<\/code> with a pattern matching the test&#8217;s title, or add <code>.only<\/code> to a specific test or describe block in the source itself.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nnpx playwright test checkout.spec.ts\n\nnpx playwright test -g \u201cuser can complete checkout\u201d\ntest.only(\u2018this is the one test that runs\u2019, async ({ page }) =&gt; { \/* \u2026 *\/ });\n<\/code><\/pre><\/div><\/p>\n<p>test.only is convenient while actively debugging one test, but it&#8217;s also a common cause of &#8220;why did CI skip half my suite&#8221; when someone forgets to remove it before committing, which is why some teams add a lint rule that fails CI if test.only shows up anywhere.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does running tests headless versus headed change what you can debug, and why does CI default to headless?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">CI<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Headless mode runs the browser with no visible window, which is faster and lighter on resources, exactly what you want on a CI runner executing hundreds of tests where nobody&#8217;s watching a screen. Headed mode renders an actual visible browser window, useful locally when you want to watch a test execute step by step to understand why it&#8217;s failing.<\/p>\n<p>Rendering differences between headless and headed used to be a real source of flaky tests in older Chrome versions, headless Chrome now uses the same rendering engine as headed, so that gap has mostly closed, but it&#8217;s still worth knowing as a possible explanation if a test behaves differently between the two modes.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--medium\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"medium\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Medium questions<\/h2><span class=\"iq-dsec__n\">25<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does Playwright talk to the browser without WebDriver?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Architecture<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Playwright communicates with the browser over each browser&#8217;s own DevTools-style protocol (CDP for Chromium, and equivalent internal protocols patched into custom Firefox and WebKit builds it ships and controls), rather than going through the W3C WebDriver wire protocol that Selenium uses. That&#8217;s a meaningfully different architecture: Playwright bundles browser binaries it has tested against, so a given Playwright version is paired to specific browser builds instead of depending on whatever driver version happens to be installed on the machine.<\/p>\n<p>The practical upside is fewer version-mismatch headaches between your automation code and the browser, and lower-level access to network requests, console messages, and page lifecycle events, since the protocol wasn&#8217;t designed around a generic cross-vendor spec.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why do people say Playwright tests are isolated by default, and what actually provides that isolation?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The isolation comes from the browser context, not the browser or the test file. The <code>@playwright\/test<\/code> runner hands every test a brand new context and page fixture, so cookies, local storage, session storage, and cache from one test never leak into the next. That&#8217;s what lets you run tests in any order, or in parallel across workers, without one test&#8217;s login session bleeding into an unrelated test.<\/p>\n<p>It&#8217;s not free of gotchas though. If two tests both write to the same backend database record, isolation at the browser level doesn&#8217;t protect you from that collision, you still need to think about test data the same way you would with any suite hitting a shared API.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What order of locator strategies does Playwright recommend, and why put getByRole first?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locators<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The documented priority is getByRole, then getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle, with getByTestId as a fallback, and raw CSS or XPath discouraged as a last resort (<a href=\"https:\/\/playwright.dev\/docs\/locators\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, locators<\/a>). getByRole comes first because it queries the accessibility tree the same way a screen reader would, which means the test is checking something a real user actually perceives, a button that says &#8220;Submit&#8221;, not an implementation detail like a class name.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait page.getByRole(\u2018button\u2019, { name: \u2018Submit\u2019 }).click();\n\nawait page.getByLabel(\u2018Email address\u2019).fill(\u2018a@b.com\u2019);\n\nawait page.getByTestId(\u2018cart-total\u2019).textContent();\n<\/code><\/pre><\/div><\/p>\n<p>The side benefit is durability. A designer renaming a CSS class or a developer swapping a div for a button breaks a CSS selector but not a role-based query, since the accessible role and name usually survive markup changes that visual styling doesn&#8217;t.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does &#039;strict mode&#039; mean for Playwright locators, and what happens when a locator matches more than one element?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locators<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Strict mode is the default behavior where an action-triggering call on a locator throws an error if the locator resolves to more than one element, instead of silently acting on the first match. It exists to catch a specific bug class: a selector that&#8217;s technically valid but ambiguous, matching three rows in a table when the test author only meant to interact with one.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait page.locator(\u2018li.item\u2019).click();\n\n\/\/ throws: strict mode violation, resolved to 5 elements\nawait page.locator(\u2018li.item\u2019).first().click(); \/\/ opts out explicitly\n\nawait page.locator(\u2018li.item\u2019).nth(2).click();   \/\/ also opts out explicitly\n<\/code><\/pre><\/div><\/p>\n<p>Reaching for first() or nth() to silence a strict-mode error, rather than tightening the locator to be unambiguous, is a smell interviewers watch for. It usually means the test will pass today and break the moment the list order changes.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you chain and filter locators to scope a query to inside a specific section of the page?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Locators<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Locators support chaining with <code>.locator()<\/code> and filtering with <code>.filter()<\/code>, both of which narrow the search to descendants of the parent locator instead of the whole page. This is how you disambiguate two elements with identical text or role that only differ by which container they sit in.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst row = page.locator(\u2018tr\u2019, { hasText: \u2018Invoice #4821\u2019 });\n\nawait row.getByRole(\u2018button\u2019, { name: \u2018Delete\u2019 }).click();\nconst product = page.locator(\u2018.product-card\u2019).filter({ hasText: \u2018In Stock\u2019 });\n\nawait product.getByRole(\u2018button\u2019, { name: \u2018Add to cart\u2019 }).click();\n<\/code><\/pre><\/div><\/p>\n<p>This matters most on pages built from repeated components, product grids, table rows, kanban cards, where the same button text shows up dozens of times and only container scoping tells them apart.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What actionability checks does Playwright run before clicking an element, and which action skips which checks?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Auto-Waiting<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Before most actions, Playwright checks that the element is visible, stable (same bounding box across two consecutive animation frames), receives pointer events (nothing else is on top of it), and enabled. Fill also checks editable. If a check keeps failing, the action retries until it passes or the timeout runs out, then throws a TimeoutError with a clear reason instead of a generic failure (<a href=\"https:\/\/playwright.dev\/docs\/actionability\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, actionability<\/a>).<\/p>\n<p>Not every action needs every check. <code>fill()<\/code> doesn&#8217;t need the stable or receives-events checks the way <code>click()<\/code> does, since typing into a field doesn&#8217;t depend on it sitting still relative to an overlay. Knowing that different actions apply a different subset is the detail that separates someone who&#8217;s read the source from someone who&#8217;s just used the API casually.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why does explicit page.waitForTimeout still show up in real test suites even though it&#039;s discouraged?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Auto-Waiting<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Usually because of something auto-waiting genuinely can&#8217;t see: an animation that finishes with no DOM change, a debounced input that only fires an API call 300ms after the last keystroke, or a third-party widget that renders on a delay outside the page&#8217;s own event lifecycle. Auto-waiting tracks specific, observable conditions, visibility, stability, network idle in some helpers, and can&#8217;t wait for something with no detectable signal.<\/p>\n<p>The honest fix in most of those cases isn&#8217;t a hardcoded sleep either, it&#8217;s waiting on the actual signal: <code>waitForResponse<\/code> for the debounced call, or a data attribute the app sets when the animation completes. A fixed sleep is a workaround people reach for under deadline pressure, and it&#8217;s worth saying so plainly in an interview rather than pretending it&#8217;s never in your code.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you assert against text that changes slightly, like a timestamp or a dynamic count, without hardcoding it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Assertions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Use a regular expression or a partial matcher instead of an exact string, or assert on a stable substring rather than the whole text node.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait expect(page.getByTestId(\u2018order-count\u2019)).toHaveText(\/d+ orders\/);\n\nawait expect(page.getByTestId(\u2018timestamp\u2019)).toContainText(\u2018Updated\u2019);\n<\/code><\/pre><\/div><\/p>\n<p>For values that are genuinely unpredictable, count, id, timestamp, asserting the shape of the text rather than the exact value keeps the test meaningful without becoming brittle to data that changes between runs.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What are Playwright test fixtures, and what problem do they solve compared to beforeEach hooks?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fixtures<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A fixture is a piece of test setup and teardown wrapped into a reusable, dependency-injected unit that a test declares by naming it as a parameter, rather than a hook that runs unconditionally for every test in a file (<a href=\"https:\/\/playwright.dev\/docs\/test-fixtures\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, fixtures<\/a>). Built-in fixtures like page, context, and browser are provided automatically. Custom ones are defined with <code>test.extend()<\/code>.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nexport const test = base.extend&lt;{ loggedInPage: Page }&gt;({\n\n  loggedInPage: async ({ page }, use) =&gt; {\n\n    await page.goto(\u2018\/login\u2019);\n\n    await page.getByLabel(\u2018Email\u2019).fill(\u2018user@test.com\u2019);\n\n    await page.getByRole(\u2018button\u2019, { name: \u2018Log in\u2019 }).click();\n\n    await use(page); \/\/ hand control to the test\n\n    \/\/ teardown code, if any, runs here after the test finishes\n\n  },\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>The key advantage over a shared beforeEach is that fixtures are lazy and composable, a test only pays the setup cost for the fixtures it actually names, and fixtures can depend on each other in a clear chain instead of everything running through one big shared hook that every test in the file inherits whether it needs it or not.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a test-scoped fixture and a worker-scoped fixture?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fixtures<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Test-scoped fixtures, the default, get set up fresh and torn down for every single test, which is what gives you full isolation. Worker-scoped fixtures are created once per worker process and reused across every test that worker runs, which matters for anything expensive to set up that doesn&#8217;t need to be unique per test, seeding a test database, starting a local server, or authenticating a shared service account.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nexport const test = base.extend&lt;{}, { apiToken: string }&gt;({\n\n  apiToken: [async ({}, use) =&gt; {\n\n    const token = await authenticateServiceAccount(); \/\/ runs once per worker\n\n    await use(token);\n\n  }, { scope: \u2018worker\u2019 }],\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>Getting this wrong in the expensive direction, worker scope for something that should be test scope, is how state leaks between tests that are supposed to be independent.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you share setup logic across multiple test files without copy-pasting the same beforeEach in each one?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fixtures<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Define the shared setup as a fixture in one file and export a customized test object built on <code>test.extend()<\/code>, then have every test file import that test object instead of the base one from <code>@playwright\/test<\/code>. Any file that imports the extended test gets the fixture automatically, no repeated hook code, and it composes cleanly if you need to extend it further for a subset of tests.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\n\/\/ fixtures.ts\n\nexport const test = base.extend&lt;{ adminPage: Page }&gt;({\n\n  adminPage: async ({ browser }, use) =&gt; {\n\n    const context = await browser.newContext({ storageState: \u2018admin.json\u2019 });\n\n    const page = await context.newPage();\n\n    await use(page);\n\n    await context.close();\n\n  },\n\n});\n\/\/ some-admin.spec.ts\n\nimport { test } from \u2018.\/fixtures\u2019;\n\ntest(\u2018admin can delete a user\u2019, async ({ adminPage }) =&gt; { \/* \u2026 *\/ });\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does page.route work, and what can you actually do with an intercepted request?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Network<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>page.route registers a handler for requests matching a URL pattern, glob or regex, and gives you full control before the request reaches the network: you can let it continue unmodified, abort it, fulfill it with a fabricated response, or continue it with modified headers or a modified body (<a href=\"https:\/\/playwright.dev\/docs\/network\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, network<\/a>).<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait page.route(\u2018**\/api\/user-profile\u2019, async (route) =&gt; {\n\n  await route.fulfill({\n\n    status: 200,\n\n    contentType: \u2018application\/json\u2019,\n\n    body: JSON.stringify({ name: \u2018Test User\u2019, plan: \u2018pro\u2019 }),\n\n  });\n\n});\n\nawait page.goto(\u2018\/dashboard\u2019); \/\/ renders using the mocked response, no real API hit\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">When would you mock an API response instead of just letting the test hit the real backend?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Network<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Mock it when you need to test a state that&#8217;s hard or slow to produce for real, an error response, an empty result set, a specific edge-case payload, or when the real backend is flaky, rate-limited, or simply not something the test owner controls. Mocking also decouples a frontend test suite from backend deploy timing, so a UI regression test doesn&#8217;t fail just because a staging API happened to be down.<\/p>\n<p>The tradeoff is real: a heavily mocked suite can pass while the actual integration is broken, since it&#8217;s testing the frontend&#8217;s handling of a shape you defined by hand, not what the backend actually returns today. Most mature suites keep a smaller set of true end-to-end tests against a real staging environment alongside the larger, faster, mocked suite.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you wait for a specific network response before asserting on the page, without a fixed sleep?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Network<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>waitForResponse takes a URL pattern, regex, or predicate function and resolves once a matching response comes back, so you can pair it with the action that triggers the request and get a promise that settles exactly when the network call finishes, not an arbitrary number of milliseconds later.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst responsePromise = page.waitForResponse((res) =&gt;\n\n  res.url().includes(\u2018\/api\/checkout\u2019) &amp;&amp; res.status() === 200\n\n);\n\nawait page.getByRole(\u2018button\u2019, { name: \u2018Place order\u2019 }).click();\n\nconst response = await responsePromise;\n<\/code><\/pre><\/div><\/p>\n<p>Starting the wait before triggering the action matters, if you await the click first and only then set up waitForResponse, the response can arrive and resolve before your listener even exists.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does Playwright&#039;s APIRequestContext give you that a UI test alone doesn&#039;t?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">API Testing<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>APIRequestContext lets you send real HTTP requests directly, no browser rendering involved, which makes it useful both for pure API testing and for fast test setup, creating a user account or seeding data through an API call instead of clicking through a signup form every time a test needs a logged-in user.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst response = await request.post(\u2018\/api\/users\u2019, {\n\n  data: { email: \u2018test@example.com\u2019, password: \u2018secret123\u2019 },\n\n});\n\nexpect(response.status()).toBe(201);\n<\/code><\/pre><\/div><\/p>\n<p>Using the API to set up state and the UI only to test the actual behavior under test is one of the bigger speedups available in a Playwright suite, since API calls run in milliseconds compared to a full form submission through a rendered page.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you avoid logging in through the UI in every single test?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Auth<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Log in once in a dedicated setup step, save the resulting cookies and storage with storageState, and load that saved state into every test&#8217;s context afterward. In the Playwright test runner this is usually wired up as a setup project that other projects declare a dependency on, so it always runs first and authenticates before the real tests start (<a href=\"https:\/\/playwright.dev\/docs\/auth\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, auth<\/a>).<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\n\/\/ auth.setup.ts\n\nsetup(\u2018authenticate\u2019, async ({ page }) =&gt; {\n\n  await page.goto(\u2018\/login\u2019);\n\n  await page.getByLabel(\u2018Email\u2019).fill(\u2018user@test.com\u2019);\n\n  await page.getByRole(\u2018button\u2019, { name: \u2018Log in\u2019 }).click();\n\n  await page.context().storageState({ path: \u2018playwright\/.auth\/user.json\u2019 });\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>Tests then just reference that storage state file in config instead of repeating the login flow, which turns a five-second UI login into essentially zero added time per test.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does Playwright&#039;s default parallelism work, and what&#039;s the difference between running tests in parallel and sharding across machines?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Parallelism<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>By default the test runner spins up multiple worker processes and distributes test files across them, each worker running its own tests sequentially inside its own isolated set of browser contexts. That&#8217;s parallel execution within one machine. Sharding is a separate mechanism, splitting the whole test suite across multiple machines or CI jobs using flags like <code>--shard=1\/3<\/code>, so a suite that takes twenty minutes on one runner can finish in under seven across three parallel CI jobs.<\/p>\n<p>The two stack: each shard still runs its own set of workers locally within that machine. Getting the worker count and shard count both right for your CI runner&#8217;s actual CPU and memory limits, not just cranking both numbers up, is usually where the real speed gains are.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does the Playwright trace viewer actually capture, and when would you turn it on?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Debugging<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A trace captures a full DOM snapshot before, during, and after every action, all network requests with headers and bodies, console logs from both the page and the test, and a timeline with a filmstrip of what the page looked like at each step (<a href=\"https:\/\/playwright.dev\/docs\/trace-viewer\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright docs, trace viewer<\/a>). It&#8217;s usually left off for the happy path locally and turned on with <code>trace: 'on-first-retry'<\/code> in CI, so you only pay the recording cost for tests that actually failed once and are being retried, not for every passing test.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\n\/\/ playwright.config.ts\n\nexport default defineConfig({\n\n  use: { trace: \u2018on-first-retry\u2019 },\n\n});\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you interact with an element inside an iframe?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Iframes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Get a FrameLocator for the iframe first, then locate elements inside it the same way you would on the main page. Playwright treats the frame as its own scoped document, and you can&#8217;t accidentally reach into it with a page-level locator, the iframe boundary is enforced.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst frame = page.frameLocator(\u2018iframe[name=\u201dpayment\u201d]\u2019);\n\nawait frame.getByLabel(\u2018Card number\u2019).fill(\u20184242 4242 4242 4242\u2019);\n\nawait frame.getByRole(\u2018button\u2019, { name: \u2018Pay\u2019 }).click();\n<\/code><\/pre><\/div><\/p>\n<p>Nested iframes just chain another frameLocator call. Cross-origin iframes, common with embedded payment widgets like Stripe, work the same way from Playwright&#8217;s side, since the automation protocol operates below the browser&#8217;s own same-origin restrictions.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you test a flow where clicking a button opens a new tab?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Multiple Pages<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Set up a listener for the context&#8217;s &#8216;page&#8217; event before triggering the click, since the new tab is a new Page object belonging to the same BrowserContext, not something the original page reference knows about on its own.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst [newPage] = await Promise.all([\n\n  context.waitForEvent(\u2018page\u2019),\n\n  page.getByRole(\u2018link\u2019, { name: \u2018Open in new tab\u2019 }).click(),\n\n]);\n\nawait newPage.waitForLoadState();\n\nawait expect(newPage).toHaveURL(\/dashboard\/);\n<\/code><\/pre><\/div><\/p>\n<p>The Promise.all pattern matters here for the same reason it matters with waitForResponse, you need the listener armed before the action fires, or there&#8217;s a race where the new tab opens before anything is listening for it.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you emulate a mobile device or a specific geolocation for a test?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Emulation<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Playwright ships a library of predefined device descriptors, viewport, user agent, touch support, device scale factor, that you spread into a browser context&#8217;s launch options, and geolocation is a separate context option that also requires granting the geolocation permission explicitly.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nimport { devices } from \u2018@playwright\/test\u2019;\nconst context = await browser.newContext({\n\n  \u2026devices[\u2019iPhone 13\u2019],\n\n  geolocation: { latitude: 51.5074, longitude: -0.1278 },\n\n  permissions: [\u2019geolocation\u2019],\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>This is context-level configuration, not page-level, which matters if a test needs two different simulated locations open side by side, that&#8217;s two separate contexts, not two pages in one.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Can you test that a specific browser permission prompt was requested, like camera or notifications?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Emulation<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>You can pre-grant or deny permissions at the context level so the app behaves as if the user already responded, and you can assert on the app&#8217;s resulting behavior, but Playwright doesn&#8217;t give you a hook to intercept and assert on the native permission prompt UI itself, since that prompt is rendered by the browser chrome, outside the page Playwright automates.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst context = await browser.newContext({ permissions: [\u2019camera\u2019, \u2018microphone\u2019] });\n<\/code><\/pre><\/div><\/p>\n<p>The practical takeaway for an interview: you test the app&#8217;s reaction to a granted or denied permission, not the browser&#8217;s own permission UI, that distinction trips people up who haven&#8217;t hit it before.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you simulate a slow or offline network connection to test loading and error states?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Network<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>context.setOffline(true) simulates a fully offline network, useful for testing offline banners or retry logic. For throttling rather than a full outage, route handlers can add an artificial delay before fulfilling or continuing a request, which is a more common way to test loading spinners than trying to configure real network throttling.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait context.setOffline(true);\n\nawait page.reload();\n\nawait expect(page.getByText(\u2018You are offline\u2019)).toBeVisible();\nawait page.route(\u2018**\/api\/**\u2019, async (route) =&gt; {\n\n  await new Promise((r) =&gt; setTimeout(r, 2000)); \/\/ simulate slow network\n\n  await route.continue();\n\n});\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s a soft assertion, and when would you actually want one instead of a normal expect that stops the test immediately?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Assertions<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A soft assertion, expect.soft, records a failure but lets the test keep executing instead of throwing immediately, so you can collect every assertion failure in a single test run into one report instead of stopping at the first mismatch and having to fix and rerun repeatedly to find the next one.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait expect.soft(page.getByTestId(\u2018title\u2019)).toHaveText(\u2018Dashboard\u2019);\n\nawait expect.soft(page.getByTestId(\u2018subtitle\u2019)).toHaveText(\u2018Welcome back\u2019);\n\n\/\/ test continues even if the first assertion above failed\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;s most useful for a single test that&#8217;s checking many independent pieces of one page, a full form&#8217;s worth of validation messages, say, where you want the complete picture of what&#8217;s broken in one run rather than fixing failures one at a time.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you configure a project in playwright.config to run the same suite against Chromium, Firefox, and WebKit without writing three copies of every test?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Configuration<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Define multiple entries in the projects array, one per browser, each pointing at the same test files but with a different browserName or use option. The test runner then runs every spec file once per project, so a single test file effectively runs three times, once per configured browser, without any duplicated test code.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nexport default defineConfig({\n\n  projects: [\n\n    { name: \u2018chromium\u2019, use: { \u2026devices[\u2019Desktop Chrome\u2019] } },\n\n    { name: \u2018firefox\u2019, use: { \u2026devices[\u2019Desktop Firefox\u2019] } },\n\n    { name: \u2018webkit\u2019, use: { \u2026devices[\u2019Desktop Safari\u2019] } },\n\n  ],\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>This is also how most teams structure device emulation projects, a &#8220;mobile-chrome&#8221; project alongside &#8220;desktop-chrome&#8221;, both pulling from the same test files, just with different device presets applied.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--hard\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"hard\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Hard questions<\/h2><span class=\"iq-dsec__n\">10<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">If your suite mutates shared server-side state, like creating and deleting the same account, why does the shared storageState pattern break down?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Auth<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Because every test sharing that one storageState is effectively acting as the same logged-in user against a shared backend, so two tests running in parallel can race each other, one test deletes a record the other test just created and is about to assert against. The fix is per-worker authentication instead of one global shared login: each worker authenticates its own account, via a worker-scoped fixture, so parallel workers never fight over the same server-side state even though they&#8217;re running simultaneously.<\/p>\n<p>This is one of the places where &#8220;just add more parallel workers&#8221; quietly breaks a suite that looked stable at low concurrency and starts failing intermittently once it&#8217;s scaled up in CI.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A test fails only in CI, never locally. What&#039;s your actual debugging process, not just &#039;add more logs&#039;?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Debugging<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>First pull the trace file from the CI artifact and open it in the trace viewer, it usually shows exactly what the DOM looked like at the moment of failure, which is often the fastest way to see a race condition a log line would never surface. Common root causes worth checking specifically: CI machines are slower and can expose a real timing bug that a fast local machine masks, CI often runs headless while local runs headed and rendering differs subtly between the two, and CI frequently runs at higher parallelism, exposing shared-state races that never show up running one test at a time locally.<\/p>\n<p>If the trace doesn&#8217;t make it obvious, reproducing the exact CI conditions locally, headless mode, the same worker count, the same viewport, is usually more productive than guessing and adding console.log calls one at a time.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Does Playwright pierce shadow DOM by default, and what does that actually mean for a locator?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Shadow DOM<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Yes. Playwright&#8217;s built-in locator engine automatically pierces open shadow roots, so a CSS selector or a getByRole call matches elements inside a shadow root the same way it would in the light DOM, without any special shadow-DOM-aware syntax. This is one of the concrete advantages over Selenium&#8217;s default CSS engine, which stops at a shadow boundary and needs explicit traversal to get inside.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\n\/\/ works even if &lt;my-widget&gt; uses open shadow DOM internally\n\nawait page.locator(\u2018my-widget\u2019).getByRole(\u2018button\u2019, { name: \u2018Confirm\u2019 }).click();\n<\/code><\/pre><\/div><\/p>\n<p>The caveat is closed shadow roots, which no automation tool can see into from outside, since the browser itself doesn&#8217;t expose them. If a component author deliberately closed the shadow root, there&#8217;s no locator workaround, only a conversation with whoever owns that component about testability.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does Playwright&#039;s visual comparison testing work, and what&#039;s the honest downside of relying on it heavily?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Visual Testing<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>toHaveScreenshot captures a screenshot and diffs it pixel-by-pixel against a stored baseline image, failing if the difference exceeds a configurable threshold. It&#8217;s genuinely good at catching a whole class of bugs that DOM assertions miss entirely, a broken CSS grid, an overlapping z-index, a font that failed to load.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait expect(page).toHaveScreenshot(\u2018checkout-page.png\u2019, { maxDiffPixels: 100 });\n<\/code><\/pre><\/div><\/p>\n<p>The honest downside is baseline maintenance. Screenshots are sensitive to font rendering differences between operating systems, anti-aliasing, and even minor browser version updates, so teams running visual tests across different OS environments than where the baseline was captured often chase false-positive diffs that have nothing to do with an actual bug. Most teams that use it seriously pin a specific Docker image for generating and comparing screenshots, precisely to remove that variable.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s Playwright&#039;s component testing feature, and how is it different from a full end-to-end test?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Component Testing<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Component testing mounts a single UI component in isolation, React, Vue, or Svelte, inside a real browser, and lets you interact with it and assert on it using the same locator and assertion API as a full page test, without needing a running application or backend behind it.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nimport { test, expect } from \u2018@playwright\/experimental-ct-react\u2019;\n\nimport { Counter } from \u2018.\/Counter\u2019;\ntest(\u2018increments on click\u2019, async ({ mount }) =&gt; {\n\n  const component = await mount(&lt;Counter initial={0} \/&gt;);\n\n  await component.getByRole(\u2018button\u2019, { name: \u2018+\u2019 }).click();\n\n  await expect(component.getByText(\u20181\u2019)).toBeVisible();\n\n});\n<\/code><\/pre><\/div><\/p>\n<p>It sits between a unit test and a full end-to-end test, faster and more isolated than driving the whole app through a browser, but still running in a real browser engine rather than a simulated DOM like jsdom, which catches real rendering and CSS behavior that a jsdom-based unit test can&#8217;t see.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you handle a date picker or drag-and-drop widget that doesn&#039;t respond to a plain click and fill?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Complex Interactions<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>For drag-and-drop, use dragTo or the lower-level hover, mouse.down, mouse.move, mouse.up sequence when a component&#8217;s drag handling doesn&#8217;t respond to the simplified dragTo helper, since some custom drag implementations listen for specific intermediate mousemove events that a single jump doesn&#8217;t trigger.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nawait page.locator(\u2018#task-1\u2019).dragTo(page.locator(\u2018#done-column\u2019));\n\/\/ lower-level fallback for finicky custom drag implementations\n\nconst source = page.locator(\u2018#task-1\u2019);\n\nawait source.hover();\n\nawait page.mouse.down();\n\nawait page.mouse.move(400, 300, { steps: 10 });\n\nawait page.mouse.up();\n<\/code><\/pre><\/div><\/p>\n<p>For a custom date picker, the reliable approach is usually to interact with it exactly the way a user would, click to open it, click through to the right month, click the day, rather than trying to set the underlying input&#8217;s value directly, since most date picker components ignore a value set outside their own click handlers and just overwrite it on the next render.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you emulate a specific timezone or locale so date and currency formatting can be tested consistently?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Emulation<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Both are context-level options set at newContext time, since they affect how the browser&#8217;s own Intl and Date APIs behave for everything rendered inside that context, not something you can toggle mid-test on an existing page.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">typescript<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-typescript\">\n\nconst context = await browser.newContext({\n\n  locale: \u2018de-DE\u2019,\n\n  timezoneId: \u2018Europe\/Berlin\u2019,\n\n});\n\nconst page = await context.newPage();\n\nawait page.goto(\u2018\/checkout\u2019);\n\n\/\/ currency and date formatting now reflect de-DE \/ Europe\/Berlin\n<\/code><\/pre><\/div><\/p>\n<p>This matters more than it sounds for any product with real internationalization, a bug where a price renders with a comma instead of a period as a decimal separator only shows up under the right locale, and a suite that never varies locale will never catch it.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A test passes when run alone but fails when the full suite runs. What&#039;s your process for tracking that down?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Debugging<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>That symptom almost always means test pollution, shared state one test leaves behind that a later test unknowingly depends on or gets broken by. Start by running just that test alongside the one immediately before it in file order, then narrow from there, since Playwright workers usually run files in a fairly stable order and the culprit is often adjacent.<\/p>\n<p>Common concrete causes: a shared backend record two tests both mutate, a worker-scoped fixture that was supposed to be test-scoped, or global app state, a feature flag, a seeded database row, that one test flips and never resets. The fix is almost never &#8220;add a wait&#8221;, it&#8217;s finding and removing the shared dependency, usually by making the affected fixture test-scoped or by having each test create its own uniquely-named test data instead of reusing a fixed record.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why can adding more parallel workers make a previously stable suite start failing intermittently?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Parallelism<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Because parallelism exposes races that never had a chance to happen at low concurrency. Two workers hitting the same shared backend resource, the same rate-limited third-party API, the same seeded test account, at the same time is a scenario that simply didn&#8217;t exist when tests ran one at a time. The suite wasn&#8217;t actually free of the bug before, it just never had two tests colliding on the same second.<\/p>\n<p>The fix is almost always isolating whatever&#8217;s shared, unique test accounts per worker, unique data per test rather than a fixed seed record, rate-limit-aware retry logic if a third-party API is genuinely shared and can&#8217;t be worker-scoped. Turning parallelism back down is a valid short-term mitigation, but it doesn&#8217;t fix the underlying design issue, it just makes the race less likely to fire.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does &#039;element is not attached to the DOM&#039; actually mean, and why does it happen more with ElementHandle than with a Locator?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Debugging<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>It means the specific DOM node a reference pointed at got removed or replaced, usually by a framework re-render, between when the reference was captured and when the test tried to act on it. ElementHandle captures one static reference at query time, so any re-render in between invalidates it. A Locator re-queries the DOM fresh on every action, so it naturally survives a re-render as long as a matching element still exists somewhere on the page afterward.<\/p>\n<p>Seeing this error in a codebase still using page.$ or ElementHandle directly is usually a sign it should be migrated to Locator-based code, since the error is largely a symptom of using the older, more brittle API on a page that re-renders, which most modern single-page apps do constantly.<\/p>\n<p><\/div><\/div><\/div>\n<h2>How to prepare for a Playwright interview<\/h2>\n<p>Skip re-reading the getting-started guide. Take a UI you already know has some rough edges, a table with pagination, a form with a debounced autosave, a modal that animates in, and write tests against it using only role-based locators and web-first assertions, no CSS selectors, no waitForTimeout. If you get stuck, that&#8217;s the exact gap an interviewer is likely to probe. Then break something on purpose, delete a data-testid, rename a button&#8217;s visible label, and watch which of your tests survive it and which don&#8217;t.<\/p>\n<p>Across the mock interview sessions LastRoundAI runs for QA and SDET roles, the question that trips up more candidates than any other isn&#8217;t about locators or assertions, it&#8217;s the one about shared authentication state breaking under parallel execution. Most people who&#8217;ve written Playwright tests solo have never hit that race condition, because it only shows up once a suite runs with real concurrency against a shared backend. It&#8217;s worth thinking through before you&#8217;re asked, not during.<\/p>\n<p>One more thing worth saying plainly: a lot of interview prep treats Playwright as interchangeable with Selenium or Cypress and tests the same generic automation trivia either way. The questions that actually distinguish a strong Playwright candidate are about the mechanisms specific to it, actionability checks, context-level isolation, and locator re-resolution, not &#8220;what is a test case&#8221; in the abstract.<\/p>\n<h2>Get the reps in before the real thing<\/h2>\n<p>Explaining actionability checks on paper is not the same as walking an interviewer through a live flaky test and fixing it out loud while they watch. <a href=\"\/products\/mock-interviews\">LastRoundAI&#8217;s mock interview mode<\/a> runs live technical rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19\/mo if a handful of sessions isn&#8217;t enough runway.<\/p>\n<p>Once your answers hold up under a follow-up question, the slower part of a QA or SDET job search is usually just getting in front of enough roles that actually test browser automation instead of treating it as a checkbox on a job description. <a href=\"\/products\/auto-apply\">Auto-Apply<\/a> queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.<\/p>\n<p>Questions about either product go to contact@lastroundai.com. That&#8217;s the only inbox we check.<\/p>\n<div class=\"iq-sources not-prose\"><div class=\"iq-sources__title\">Sources &amp; further reading<\/div><ul class=\"iq-sources__list\"><li><a href=\"https:\/\/playwright.dev\/docs\/actionability\" target=\"_blank\" rel=\"nofollow noopener\">Playwright Docs: Actionability<\/a><\/li><li><a href=\"https:\/\/playwright.dev\/docs\/locators\" target=\"_blank\" rel=\"nofollow noopener\">Playwright Docs: Locators<\/a><\/li><li><a href=\"https:\/\/playwright.dev\/docs\/test-fixtures\" target=\"_blank\" rel=\"nofollow noopener\">Playwright Docs: Test Fixtures<\/a><\/li><li><a href=\"https:\/\/playwright.dev\/docs\/network\" target=\"_blank\" rel=\"nofollow noopener\">Playwright Docs: Network<\/a><\/li><li><a href=\"https:\/\/playwright.dev\/docs\/auth\" target=\"_blank\" rel=\"nofollow noopener\">Playwright Docs: Authentication<\/a><\/li><li><a href=\"https:\/\/playwright.dev\/docs\/trace-viewer\" target=\"_blank\" rel=\"nofollow noopener\">Playwright Docs: Trace Viewer<\/a><\/li><\/ul><\/div>\n","protected":false},"excerpt":{"rendered":"<p>A QA engineer I talked to last year described her first Playwright interview loop like this: the interviewer pulled up a flaky Selenium test suite from the company&#8217;s own repo, thirty seconds of retries and Thread.sleep calls scattered through it, and just asked &#8220;how would you fix this.&#8221; Not &#8220;what is Playwright.&#8221; Not &#8220;list the&#8230;<\/p>\n","protected":false},"author":7,"featured_media":2052,"comment_status":"open","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"tags":[],"class_list":["post-2027","iq","type-iq","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Playwright Interview Questions (2026) | LastRoundAI<\/title>\n<meta name=\"description\" content=\"50 real Playwright interview questions: locators, auto-waiting, fixtures, network mocking, auth reuse, trace viewer, shadow DOM, and flaky test debugging.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/lastroundai.com\/interview-questions\/playwright\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Playwright Interview Questions (2026) | LastRoundAI\" \/>\n<meta property=\"og:description\" content=\"50 real Playwright interview questions: locators, auto-waiting, fixtures, network mocking, auth reuse, trace viewer, shadow DOM, and flaky test debugging.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/lastroundai.com\/interview-questions\/playwright\" \/>\n<meta property=\"og:site_name\" content=\"LastRound AI\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-19T17:32:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-playwright-og.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"33 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright\",\"name\":\"Playwright Interview Questions (2026) | LastRoundAI\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-playwright-og.png\",\"datePublished\":\"2026-07-19T17:31:59+00:00\",\"dateModified\":\"2026-07-19T17:32:00+00:00\",\"description\":\"50 real Playwright interview questions: locators, auto-waiting, fixtures, network mocking, auth reuse, trace viewer, shadow DOM, and flaky test debugging.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright#primaryimage\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-playwright-og.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-playwright-og.png\",\"width\":1200,\"height\":630,\"caption\":\"Playwright interview questions \u2014 LastRoundAI\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/playwright#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/blog\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Interview Questions\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Playwright Interview Questions (2026): Q&#038;A Guide\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"name\":\"LastRound AI\",\"description\":\"Interview Assistant prep, tech careers and AI tools\",\"publisher\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\",\"name\":\"LastRound AI\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"width\":400,\"height\":400,\"caption\":\"LastRound AI\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Playwright Interview Questions (2026) | LastRoundAI","description":"50 real Playwright interview questions: locators, auto-waiting, fixtures, network mocking, auth reuse, trace viewer, shadow DOM, and flaky test debugging.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/lastroundai.com\/interview-questions\/playwright","og_locale":"en_US","og_type":"article","og_title":"Playwright Interview Questions (2026) | LastRoundAI","og_description":"50 real Playwright interview questions: locators, auto-waiting, fixtures, network mocking, auth reuse, trace viewer, shadow DOM, and flaky test debugging.","og_url":"https:\/\/lastroundai.com\/interview-questions\/playwright","og_site_name":"LastRound AI","article_modified_time":"2026-07-19T17:32:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-playwright-og.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"33 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/lastroundai.com\/interview-questions\/playwright","url":"https:\/\/lastroundai.com\/interview-questions\/playwright","name":"Playwright Interview Questions (2026) | LastRoundAI","isPartOf":{"@id":"https:\/\/lastroundai.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/lastroundai.com\/interview-questions\/playwright#primaryimage"},"image":{"@id":"https:\/\/lastroundai.com\/interview-questions\/playwright#primaryimage"},"thumbnailUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-playwright-og.png","datePublished":"2026-07-19T17:31:59+00:00","dateModified":"2026-07-19T17:32:00+00:00","description":"50 real Playwright interview questions: locators, auto-waiting, fixtures, network mocking, auth reuse, trace viewer, shadow DOM, and flaky test debugging.","breadcrumb":{"@id":"https:\/\/lastroundai.com\/interview-questions\/playwright#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/lastroundai.com\/interview-questions\/playwright"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/interview-questions\/playwright#primaryimage","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-playwright-og.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-playwright-og.png","width":1200,"height":630,"caption":"Playwright interview questions \u2014 LastRoundAI"},{"@type":"BreadcrumbList","@id":"https:\/\/lastroundai.com\/interview-questions\/playwright#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/lastroundai.com\/blog"},{"@type":"ListItem","position":2,"name":"Interview Questions","item":"https:\/\/lastroundai.com\/interview-questions"},{"@type":"ListItem","position":3,"name":"Playwright Interview Questions (2026): Q&#038;A Guide"}]},{"@type":"WebSite","@id":"https:\/\/lastroundai.com\/blog\/#website","url":"https:\/\/lastroundai.com\/blog\/","name":"LastRound AI","description":"Interview Assistant prep, tech careers and AI tools","publisher":{"@id":"https:\/\/lastroundai.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/lastroundai.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/lastroundai.com\/blog\/#organization","name":"LastRound AI","url":"https:\/\/lastroundai.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","width":400,"height":400,"caption":"LastRound AI"},"image":{"@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/2027","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq"}],"about":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/types\/iq"}],"author":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/comments?post=2027"}],"version-history":[{"count":1,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/2027\/revisions"}],"predecessor-version":[{"id":2057,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/2027\/revisions\/2057"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media\/2052"}],"wp:attachment":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media?parent=2027"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/tags?post=2027"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}