mobile-web-emulation-runner
Builds a workflow to run web E2E tests under mobile viewports + DPRs (device pixel ratios): Playwright's `devices` catalog (iPhone 15, Pixel 7), suite run per-device as matrix shards, per-device screenshots, mobile assertions (`.tap()`, viewport-conditional layout). Use when a responsive web app needs mobile-breakpoint regression without a real-device farm. Mobile WEB only - for native apps use appium-testing, detox-testing, or flutter-testing; for cross-shard aggregation use mobile-device-matrix-toolkit; for gesture sequences use touch-gesture-tester.
Install with skills.sh (any agent)
npx skills add testland/qa --skill mobile-web-emulation-runnermobile-web-emulation-runner
Overview
Many web apps support mobile via responsive design - but the desktop test suite never exercises mobile breakpoints. Real-device testing (appium-testing, xcuitest-suite) is heavy; viewport emulation in browser-based testing is light.
Playwright + Cypress + Selenium all support mobile emulation: viewport size, device pixel ratio, user agent, and touch event synthesis can be configured per test.
This skill builds the workflow.
When to use
If the app is a native mobile app (RN, Flutter, native iOS/Android), this isn't the right skill - see the per-platform alternatives.
How to use
Step 1 - Pick the device profiles
Playwright ships a devices catalog with realistic viewport / DPR / user-agent combinations - import it and spread a profile into a project's use block:
import { devices } from '@playwright/test';
// each entry carries viewport + DPR + user-agent + touch flags, e.g.
// devices['iPhone 15'] -> viewport 393x852, DPR 3, isMobile, hasTouchisMobile: true triggers Playwright's mobile-mode quirks (meta viewport handling); hasTouch: true enables touch-event synthesis. The full profile catalog, the shape of each entry, and the Cypress runner equivalent are in references/device-profiles-and-cypress.md.
Step 2 - Per-device project config
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'desktop-chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile-iphone-15',
use: { ...devices['iPhone 15'] },
},
{
name: 'mobile-pixel-7',
use: { ...devices['Pixel 7'] },
},
{
name: 'tablet-ipad-pro',
use: { ...devices['iPad Pro 11'] },
},
],
});Run all projects (which is the desktop + 3 mobile shards):
npx playwright testRun only mobile:
npx playwright test --project=mobile-iphone-15 --project=mobile-pixel-7Step 3 - Mobile-specific assertions
Tests should distinguish desktop-only from mobile-aware behavior:
import { test, expect, devices } from '@playwright/test';
test.describe('Cart page - mobile layout', () => {
test.use(devices['iPhone 15']);
test('shows mobile drawer, not sidebar', async ({ page }) => {
await page.goto('/cart');
// Mobile-specific: drawer behind hamburger
await expect(page.getByRole('button', { name: /menu/i })).toBeVisible();
await expect(page.getByRole('navigation')).not.toBeVisible(); // hidden until open
});
test('tap (not click) on add-to-cart', async ({ page }) => {
await page.goto('/products/BOOK-001');
await page.getByRole('button', { name: /add to cart/i }).tap(); // .tap not .click
await expect(page.getByRole('alert', { name: /added/i })).toBeVisible();
});
});.tap() synthesizes a touch event (isMobile: true enables); .click() synthesizes mouse events. Prefer .tap() on mobile profiles.
Step 4 - Visual regression per device
test('home page mobile layout snapshot', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('home-iphone-15.png');
});Per-device screenshots produce per-device baselines; layout regressions on iPhone size catch issues that desktop-only tests miss. Pair with playwright-snapshots (in the qa-visual-regression plugin).
Step 5 - CI matrix
jobs:
e2e:
strategy:
fail-fast: false
matrix:
project:
- desktop-chromium
- mobile-iphone-15
- mobile-pixel-7
- tablet-ipad-pro
runs-on: ubuntu-latest
name: ${{ matrix.project }}
steps:
- uses: actions/checkout@v5
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --project=${{ matrix.project }}Each project runs as a separate matrix job; fail-fast: false ensures a failure on iPhone doesn't cancel Pixel.
Verify: assert every matrix shard exits 0. Because fail-fast: false lets shards fail independently, gate the merge on all shards green - one red shard (e.g. mobile-iphone-15) must block, never be averaged away.
Step 6 - Aggregating per-device results
Use the mobile-device-matrix-toolkit aggregator (Step 4) to produce a per-device summary:
| Project | Tests | Pass | Fail | Time |
|------------------------|------:|-----:|-----:|-------:|
| desktop-chromium | 42 | 42 | 0 | 120s |
| mobile-iphone-15 | 42 | 40 | 2 | 145s | ← drawer regression
| mobile-pixel-7 | 42 | 42 | 0 | 138s |
| tablet-ipad-pro | 42 | 41 | 1 | 132s | ← landscape layout
Verify: before merging, assert every row shows Fail = 0. If a shard is red
(e.g. mobile-iphone-15 above), open its report, reproduce with
`npx playwright test --project=<shard>`, fix the mobile-breakpoint
regression, and re-run that shard until green.Worked example
A responsive storefront renders its nav as a sidebar on desktop and behind a hamburger on mobile. A regression once hid the hamburger at iPhone width.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Running the same desktop tests with viewport: { width: 375 } only | Misses DPR / touch / user-agent differences. | Use devices[...] catalog (Step 1). |
.click() on mobile project | Synthesizes mouse events; misses touch-handler bugs. | .tap() for isMobile: true projects (Step 3). |
| One desktop+mobile mega-test | Branching if (viewport.width < ...) clutters; per-project tests cleaner. | Per-project tests (Step 2). |
| Mobile-only baselines without desktop comparison | Misses cases where the desktop layout regressed at the mobile breakpoint. | Both desktop + mobile in CI (Step 5). |
| Treating emulation as substitute for real devices | Emulation doesn't catch real-device perf, touch sensitivity, browser quirks. | Pair with farm runs for release tier. |
Skipping --with-deps in Playwright install | CI runner missing browser dependencies; mobile profiles fail. | Always npx playwright install --with-deps in CI (Step 5). |
Limitations
References
Mobile-web emulation - device profiles and the Cypress runner
View source (opens in new window)Mobile-web emulation - device profiles and the Cypress runner
Lookup reference for the full Playwright devices catalog and for the Cypress equivalent of viewport emulation. Referenced from Step 1 and the References of the mobile-web-emulation-runner skill.
Playwright device profiles
Playwright ships a devices catalog with realistic viewport / DPR / user-agent combinations:
import { devices } from '@playwright/test';
// Common modern profiles:
devices['iPhone 15']
devices['iPhone 15 Pro Max']
devices['iPhone 14']
devices['Pixel 7']
devices['Pixel 5']
devices['Galaxy S9+']
devices['iPad Pro 11']
devices['iPad Mini']Each entry includes:
{
viewport: { width: 393, height: 852 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; ...) AppleWebKit/...',
}isMobile: true triggers Playwright's mobile-mode quirks (meta viewport handling); hasTouch: true enables touch-event synthesis.
Cypress equivalent
// cypress.config.ts
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// Per-test or per-spec viewport
},
viewportWidth: 1280,
viewportHeight: 720,
},
});
// In a test:
beforeEach(() => {
cy.viewport('iphone-15'); // built-in preset
// OR
cy.viewport(393, 852, 'portrait');
});Cypress doesn't ship a devices catalog as rich as Playwright's; viewport sizing is the primary control. For touch-event synthesis, use cy.realTouch() (via cypress-real-events plugin).
Related skills
appium-testing
Wires Appium for cross-platform mobile UI automation - uses the WebDriver protocol, picks a driver per platform (XCUITest for iOS, UiAutomator2 / Espresso for Android, Mac2 for macOS, Windows for desktop), authors tests in JS / Python / Java / Ruby / .NET, configures `desiredCapabilities`, runs against simulators / emulators / device farms. Use when a single test suite must cover both iOS and Android, or when the team's stack is multi-platform (iOS + Android + Mac + Windows).
detox-testing
Authors React Native E2E tests with Detox (Wix) - gray-box architecture (runs in-process with the app), `element(by.id|by.text|by.label)` matchers, `waitFor()` for explicit sync beyond Detox's automatic async tracking, Jest runner. Use when the app is React Native and speed matters. For Flutter use flutter-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows; for non-RN native use xcuitest-suite or espresso-suite.
espresso-suite
Authors Espresso UI tests for Android - uses `onView(withId(...)).perform(...).check(matches(...))`, leans on Espresso's automatic synchronization (no `Thread.sleep`), wires `IdlingResource` for app-specific async, runs via `./gradlew connectedAndroidTest` and parses the JUnit XML output. Use when an Android app needs UI tests in Google's first-party framework.
flutter-testing
Authors Flutter tests across the three-layer pyramid - unit (`flutter test`), widget (`testWidgets` + `WidgetTester`), integration (`integration_test` on simulator/emulator/device). Picks the right layer per change, mocks via `mockito` + `build_runner`, LCOV coverage, CI with the Flutter Action. Use when the app is Flutter and the team wants its first-party stack. For React Native use detox-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows.
maestro-flows
Authors Maestro YAML flow files (`.maestro/*.yaml`) for mobile + web UI automation: declarative `tapOn`, `inputText`, `assertVisible`, `swipe`, supported targets (iOS, Android, Flutter, React Native, web), nested flow imports, JavaScript hooks for complex conditions. Use when the team has already chosen Maestro, is coming from an existing `.maestro/` directory, or explicitly wants YAML-declarative tests readable by non-engineers without a compile step. For framework selection or authoring tests in XCUITest / Espresso / Detox / Appium / Flutter, use a mobile driver-selection or per-flow mobile test-authoring step instead.
mobile-a11y-test-author
Authors native mobile accessibility tests covering iOS (Accessibility Inspector, XCUITest `performAccessibilityAudit()` introduced in iOS 17, VoiceOver label/trait/hint verification) and Android (Espresso `AccessibilityChecks.enable()`, Accessibility Scanner, TalkBack traversal, `contentDescription` labelling) with WCAG-aligned checks for element labels, 44pt/48dp touch targets, contrast ratios, and focus order. Use when an iOS or Android app needs automated and manual accessibility test coverage beyond what `xcuitest-suite` or `espresso-suite` provide.
mobile-device-matrix-toolkit
Dispatches mobile UI test runs across a 3-tier device matrix (smoke per-PR, regression per-merge, full farm at release) to control CI cost: generates per-target Appium capability configs from a central YAML, parallelises via GitHub Actions matrix strategy, and aggregates JUnit XML into a cross-device pass/fail table. Use when deciding which iOS / Android devices and OS versions to run tests on and at which stage (smoke / regression / full farm), not how to configure a specific test framework (for that, use xcuitest-suite, espresso-suite, etc.).
mobile-web-perf-budget
Pure-reference skill for mobile-web performance budgets - Core Web Vitals at the 75th percentile mobile (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1; FID retired March 2024 in favor of INP), Lighthouse mobile profile config, per-route resource budgets (JS bundle, image weight, font load). Use as the team's reference for "what should the mobile perf gate enforce" - paired with `lighthouse-perf` (the runner) and `lighthouse-budget-author` (the per-route author).
touch-gesture-tester
Verifies touch-gesture handlers (tap, double-tap, long-press, swipe, pinch, rotate, pan) work as expected under both mobile-emulation (Playwright) and native (XCUITest / Espresso / Detox) - distinguishes "mouse click handler also fires on tap" from "real touch event fired with correct properties." Use when the app has bespoke gesture handlers (custom carousels, sliders, drag-drop, pull-to-refresh) and the team needs targeted gesture verification beyond generic UI assertions.
xcuitest-suite
Authors XCUIest UI tests for iOS / iPadOS / tvOS - uses the three-class XCUIApplication / XCUIElement / XCUIElementQuery pattern, sets accessibility identifiers on production code, runs via `xcodebuild test` with destination, parses the `xcresult` bundle. Use when an iOS app needs UI tests in Apple's first-party framework (no external runtime; native to Xcode).