A Playwright test fails with 'The page does not support tap' on a mobile flow — what is missing?
answer
- The message names the option to set
- It is the context, not the element
- Width and user agent are not enough
- Two flags default to false
- A descriptor sets them together
basics
~20 sThe context was built without hasTouch, which defaults to false. Playwright refuses locator.tap when there is no touchscreen. A hand-written viewport and user agent do not enable touch; a device descriptor sets hasTouch and isMobile together.
solid answer
~40 sThe full message names the fix: `Use hasTouch context option to enable touch support.` The check runs before any element work, so it is a context problem, not a selector problem. Teams reach this state by hand-rolling emulation — a project that sets only `viewport` and a phone `userAgent` renders at phone width and gets the mobile bundle from the server, but `hasTouch` and `isMobile` both stay `false`, so touch-only handlers never fire and `locator.tap()` throws. Spreading `devices['Pixel 7']` or `devices['iPhone 15']` sets all six keys at once and the error disappears; setting `hasTouch: true` explicitly also works if you really want a bespoke profile. Two constraints matter: `isMobile` is documented as unsupported in Firefox, and it cannot be combined with a null viewport.
code
typescript · 9 linesimport { test, expect, devices } from '@playwright/test';
test.use({ ...devices['Pixel 7'] });
test('room gallery advances on tap', async ({ page }) => {
await page.goto('/rooms/deluxe-king');
await page.getByRole('button', { name: 'Next photo' }).tap();
await expect(page.getByRole('img', { name: 'Photo 2' })).toBeVisible();
});go deeper
Read the error text literally: it names the option you need. Using a device descriptor rather than a hand-written viewport is the normal fix, and it prevents the same class of failure elsewhere.
Explain the mechanics: hasTouch defaults to false, the check happens on the context before any element work, and neither the viewport size nor the user agent implies touch or the mobile flag.
Diagnose it from the config. Recognise hand-rolled emulation as the root cause, know that isMobile is unsupported in Firefox and invalid with a null viewport, and decide whether the spec should have used tap at all.
Rule on how emulation is expressed across the suite. Hand-assembled device profiles drift and produce contexts matching no real device, so a policy of spreading registry entries with named, minimal overrides is worth enforcing.
## What the error means `locator.tap()` refuses to run on a context that has no touch support, and says so verbatim: `The page does not support tap. Use hasTouch context option to enable touch support.` The check happens before any element work — Playwright is not telling you the element is wrong, it is telling you the **context** was built without touch, so there is no touchscreen to dispatch to. The same underlying constraint guards the touchscreen input surface directly. `hasTouch` is a browser context option and defaults to `false`. Device descriptors for phones and tablets set it to `true`; nothing else turns it on implicitly. ## How a suite ends up here Almost always because someone hand-rolled the emulation. A hotel-booking suite wants to cover the mobile room-detail page, and the config grows a project like `use: { viewport: { width: 390, height: 844 }, userAgent: '...iPhone...' }`. It looks convincing: - The page renders at phone width, so the responsive CSS branch is exercised. - The server sees a phone user agent, so any UA-based branching serves the mobile bundle. - Screenshots look like a phone. But two flags were never set, and neither is inferred from the other two: | option | hand-rolled value | consequence | |---|---|---| | `hasTouch` | `false` | `locator.tap()` throws; touch-only handlers never fire | | `isMobile` | `false` | the page's `meta viewport` tag is ignored, so layout can differ subtly | The suite is green on everything that only needs width, and fails the moment a test taps. ## The fix Spread a device descriptor instead of assembling one. `test.use({ ...devices['Pixel 7'] })` sets `viewport`, `screen`, `userAgent`, `deviceScaleFactor`, `isMobile` and `hasTouch` in one line, and the error disappears because the context genuinely has touch. If you truly want a bespoke combination, set `hasTouch: true` explicitly and accept that you are now maintaining a device profile by hand. There is a second, quieter question hiding here: should the test tap at all? `locator.click()` works on a touch context too, and most flows read fine as clicks. Reach for `tap()` when the behaviour under test is touch-specific — a swipeable gallery on the room-detail page, a control bound to `touchstart` — and use `click()` otherwise, so a context misconfiguration cannot break unrelated specs. Where the fix goes matters as well. Put the spread in the project when every spec that project runs is a mobile spec, and at the top level of a file when only that file is mobile. Do not scatter `hasTouch: true` through individual specs to silence the error: each one is a hand-maintained fragment of a device profile, and the next person cannot tell which of them are deliberate. ## Constraints worth knowing - `isMobile` is documented as **not supported in Firefox**. A mobile descriptor spread into a Firefox project will not give you mobile viewport semantics, so phone projects in practice target Chromium or WebKit. - `isMobile` cannot be combined with a null viewport: Playwright rejects it with `"isMobile" option is not supported with null "viewport"`. - `hasTouch` and `isMobile` are independent. Touch without the mobile flag is a real configuration (a touchscreen laptop), and the mobile flag without touch is a nonsense one that emulation will nonetheless let you build. ## Diagnosing it quickly 1. Read the message: it names the option, which is unusual and worth taking literally. 2. Find the `use` block that applies to the failing project or file, and look for a spread descriptor. 3. If there is no spread, that is your answer — the emulation was assembled by hand. 4. If there is a spread, check whether a later override reset `hasTouch`, or whether an inner `test.use` narrowed the options for that describe group. 5. Confirm from the page rather than the config where you can: touch-capable contexts expose touch event support to the page, and a quick evaluation of `'ontouchstart' in window` settles it. ## The one-sentence answer The context has `hasTouch: false`, because the mobile emulation was hand-assembled from a viewport and a user agent; spread a device descriptor (or set `hasTouch: true`) and the tap is permitted.
- Should a mobile spec use `tap()` everywhere instead of `click()`?No. `click()` works fine on a touch-enabled context and reads more naturally for ordinary flows. Reserve `tap()` for behaviour that is genuinely touch-specific, such as a swipeable room gallery or a control bound to touch events, so one context misconfiguration cannot break specs that never needed touch.
- Why do phone projects in practice target Chromium or WebKit rather than Firefox?Playwright documents `isMobile` as not supported in Firefox, so a mobile descriptor spread into a Firefox project does not give you the mobile viewport semantics the descriptor implies. Touch and metrics may still apply, but the emulation is incomplete, so the mobile axis of a suite is normally built on the other two engines.
saying these in an interview costs you the question
- Blames the locator or the selector for the error
- Thinks a phone user agent turns on touch events
- Assumes the viewport size implies the mobile flag
- Adds a wait or a retry instead of fixing the context
- Believes tap is required for all mobile interactions