How to Test Mobile Web and Responsive Checkout
Short answer
Responsive checkout E2E fails when teams run desktop-only CI, use mouse clicks on touch-only controls, or assert wallet buttons that never appear in headless Linux. Emulate mobile viewports and hasTouch, split wallet vs card coverage, probe order rows on every PR, and follow Mobile testing (Mobilewright) for native app parity—not just narrow desktop windows.
Part of Testing Guides by UI patterns.
Who this is for
Ecommerce, SaaS billing, and marketplace teams where 60%+ revenue is mobile web—sticky CTAs, drawer carts, Payment Request / Apple Pay, address autofill, and breakpoint-specific layouts (hamburger nav, bottom sheets).
Why mobile checkout needs different Arrange/Assert
| Layer | Pitfall | E2E fix |
|---|---|---|
| Viewport | Desktop grid hides mobile bug | test.use({ viewport, isMobile, hasTouch }) |
| Touch | click() vs tap targets | tap(); min 44px hit targets |
| Wallet pay | No Apple Pay in Linux CI | Probe order path + macOS nightly wallet job |
| Sticky header | CTA covered by cookie banner | Dismiss banners in Arrange |
| Input zoom | iOS zoom on focus breaks layout | Font-size ≥16px or probe-only field checks |
| Orientation | Landscape cart drawer clip | Separate landscape spec slice |
Mobile web is not "desktop but narrow"—scroll containers, 100vh, and safe-area insets differ.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| iPhone 14 viewport | Not mobile user-agent | Wrong CSS branch | devices['iPhone 14'] preset |
| Android Chrome | Different font metrics | Overflow clip | Pixel 7 preset + screenshot on fail |
| Touch scroll | Element not in viewport | tap timeout | locator.scrollIntoViewIfNeeded() |
| Drawer cart | Off-canvas not visible | Assert zero items | Open cart drawer first |
| Bottom sheet checkout | z-index overlay | Click intercepted | getByRole in dialog scope |
| Apple Pay sheet | Unavailable headless | Button missing—not bug | Tiered: probe PR + wallet nightly |
| Google Pay | Chrome headless hidden | Skip wrongly | Document coverage matrix |
| Autofill address | Browser heuristics | Flaky fill | Seed via API; minimal UI fill |
| Promo code accordion | Collapsed on mobile | Field not found | Expand section before fill |
| 3DS on mobile | Nested iframe scroll | Challenge off-screen | frameLocator + scroll into view |
| Network slow 3G | Timeouts | False fail | Route probe Assert before UI |
| PWA standalone | display-mode CSS | Untested mode | emulateMedia or dedicated spec |
| Safe area notch | Fixed footer overlap | CTA untappable | Visual regression optional |
| Rotate landscape | Two-column breakpoint | Layout jump | setViewportSize mid-test |
| Cookie consent | Blocks pay button | Random fail | Seed consent cookie in Arrange |
Viewport and touch fixture
// playwright.config.ts (excerpt)
import { devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'desktop-chrome', use: { ...devices['Desktop Chrome'] } },
{
name: 'mobile-safari',
use: {
...devices['iPhone 14'],
// hasTouch and isMobile included in preset
},
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
],
});
// tests/checkout/mobile-guest.spec.ts
// @Scenario: checkout/mobile-guest-card
import { test, expect } from '../fixtures/run';
test.use({ ...test.info().project.use }); // inherits device preset
test('guest completes card checkout on mobile', async ({ page, request, runId }) => {
await request.post('/api/test/seed-cart', {
data: { runId, sku: 'tee-mobile', qty: 1 },
});
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).tap();
// Dismiss sticky cookie banner if present
const accept = page.getByRole('button', { name: 'Accept cookies' });
if (await accept.isVisible()) await accept.tap();
await page.getByLabel('Email').fill(`guest+${runId}@test.local`);
await page.getByRole('button', { name: 'Pay now' }).tap();
const cardFrame = page.frameLocator('iframe[name^="__privateStripeFrame"]');
await cardFrame.getByLabel('Card number').fill('4242424242424242');
await cardFrame.getByLabel('Expiration').fill('12/34');
await cardFrame.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Place order' }).tap();
await expect.poll(async () => {
const res = await request.get(`/api/test/probe-order?runId=${runId}`);
return (await res.json()).status;
}, { timeout: 30_000 }).toBe('paid');
});
Wallet pay tiered strategy
Full wallet UI rarely runs in default Linux CI—mirror wallet payments guide:
| Tier | Coverage | When |
|---|---|---|
| PR | Card checkout mobile + probe order | Every commit |
| Nightly macOS | Apple Pay sheet Safari | Scheduled |
| Manual / device farm | Google Pay biometrics | Release gate |
On PR: assert wallet button visible where supported OR card fallback path completes—probe order either way.
Responsive breakpoint matrix
Do not duplicate every desktop spec—prioritize by TrueCoverage mobile sessions:
- Cart → checkout → pay (critical path)
- Nav → PLP → add to cart (discovery)
- Account → order history (post-purchase)
Use page.setViewportSize({ width: 390, height: 844 }) for quick breakpoint probes inside desktop project when adding one-off regression tests.
Native mobile vs mobile web
Mobile web Playwright covers responsive sites in mobile browsers. For native iOS/Android apps, use Mobile testing (Mobilewright)—shared // @Scenario links and markdown plans, different driver. Do not assume one spec runs both.
TestChimp workflow
// @Scenario: checkout/mobile-guest-card links to markdown plans. /testchimp test repairs tap selectors when responsive refactors move CTAs to bottom sheets. /testchimp init scaffolds probe-order routes. ExploreChimp highlights mobile-only screen states when markScreenState fixtures annotate checkout steps.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Desktop-only CI | Mobile layout bugs ship | mobile-safari + mobile-chrome projects |
click() everywhere | Touch event paths untested | tap() on mobile projects |
| Assert wallet in Linux | False failures | Tiered wallet + probe order |
| 320px only | Miss tablet breakpoint | Pixel + iPad slice |
| Full page screenshot assert | Brittle | Probe order + targeted roles |
| Ignore cookie banner | Intercepted taps | Dismiss or seed consent |
| Same spec both orientations | Doubles flake | Split only if prod data shows usage |
| Skip 3DS mobile | Highest fraud segment | frameLocator mobile spec |
External references
- Playwright emulation
- Playwright devices
- Apple Human Interface Guidelines — touch targets
- WCAG target size
Example scenario
Situation: Guest on iPhone completes checkout with card after dismissing cookie banner.
Expected outcome: Order paid; confirmation reachable without horizontal scroll trap.
Why UI-only automation breaks: Thank-you page shows while order row still pending—mobile redirect raced webhook.
- Arrange: Seed cart with runId via API; set iPhone 14 project.
- Act: Tap checkout; fill guest email; complete Stripe card in frame; place order.
- Assert: Poll probe-order status paid; optional confirmation heading visible.
TestChimp workflow: // @Scenario: links mobile checkout to markdown; /testchimp test preserves tap + probe Assert when bottom sheet CSS changes.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Related
- Mobile testing (Mobilewright)
- Wallet payments (Apple Pay, Google Pay)
- Stripe payments
- Ecommerce checkout flows
- Form validation
Frequently asked questions
Which mobile devices should Playwright emulate?
Minimum: iPhone 14 (Safari) and Pixel 7 (Chrome). Add iPad if tablet checkout differs. Match top TrueCoverage devices over time.
click vs tap in Playwright?
Use tap() on mobile projects with hasTouch. click() may pass but miss touch-specific handlers.
Can Apple Pay run in GitHub Actions?
Not on Linux workers. Run wallet UI on macOS nightly; keep probe-order Assert on every PR with card path.
How to test sticky mobile headers?
scrollIntoViewIfNeeded before tap; dismiss overlays. Optional visual snapshot on failure only.
Mobile web vs native app testing?
Mobile web uses Playwright device presets. Native apps use Mobilewright—see /smart-tests/mobile-testing for shared scenario links.
Responsive vs separate m. subdomain?
Same probe strategy. If m. subdomain, add baseURL project per host; assert canonical cart IDs via probe.
iOS input zoom on focus?
Ensure 16px+ font on inputs or accept zoom behavior. Prefer probe for validation errors over pixel asserts.
TestChimp with mobile checkout?
/testchimp init scaffolds probe-order; markScreenState annotates mobile screen states for ExploreChimp; /testchimp test repairs specs when responsive refactors move CTAs.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.