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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill touch-gesture-testertouch-gesture-tester
Overview
Touch gestures are not just clicks-on-mobile. A tap on a phone fires a different event sequence than a click on a desktop (touchstart → touchend → click vs mousedown → mouseup → click). A component that listens only for click works for both; a component that listens for touchstart / gesturestart / pointerdown may have bugs that mouse-only tests miss.
This skill is build-an-X: build per-gesture verification tests that exercise the actual touch event sequences.
When to use
If the app uses only standard click handlers, this is overkill - the standard E2E suite covers it.
Step 1 - Map the app's gesture handlers
// Inventory: grep production code for touch event listeners
// JS / RN / web:
grep -rn "touchstart\|touchmove\|touchend\|gesturestart\|pointerdown" src/
// React Native:
grep -rn "PanResponder\|onSwipe\|onLongPress\|GestureDetector" src/
// iOS Swift:
grep -rn "UITapGestureRecognizer\|UISwipeGestureRecognizer\|UILongPressGestureRecognizer\|UIPinchGestureRecognizer" .
// Android Kotlin:
grep -rn "GestureDetector\|onTouchEvent\|MotionEvent" .The output is the test target list. Each handler needs at least one gesture test.
Step 2 - Per-gesture test patterns (Playwright + mobile project)
Tap
import { test, expect, devices } from '@playwright/test';
test.use(devices['iPhone 15']);
test('tap fires the handler (not just click)', async ({ page }) => {
await page.goto('/');
// Tap synthesizes touchstart + touchend + click (standard pattern)
await page.getByRole('button', { name: 'Like' }).tap();
await expect(page.getByText('Liked')).toBeVisible();
});Double-tap
test('double-tap zooms image', async ({ page }) => {
await page.goto('/photos/BOOK-001');
const image = page.getByRole('img', { name: 'Cover' });
// Two rapid taps
await image.tap();
await image.tap(); // within 500ms
// Verify zoom state (e.g., transform style or class change)
await expect(image).toHaveCSS('transform', /scale\(2/);
});Long-press
test('long-press opens context menu', async ({ page }) => {
await page.goto('/messages/M-001');
const message = page.getByText('Hello');
// Hold for 1000ms (Playwright tap doesn't have built-in long-press;
// synthesize via touchscreen primitives)
const box = await message.boundingBox();
await page.touchscreen.tap(box.x + box.width / 2, box.y + box.height / 2);
// For real long-press, use lower-level API:
await page.evaluate((selector) => {
const el = document.querySelector(selector);
el.dispatchEvent(new TouchEvent('touchstart', {
touches: [new Touch({ identifier: 0, target: el, clientX: 100, clientY: 100 })],
bubbles: true, cancelable: true,
}));
}, '[data-testid="message"]');
await page.waitForTimeout(1100); // exceed long-press threshold
await expect(page.getByRole('menu', { name: 'Message actions' })).toBeVisible();
});Swipe
test('swipe-left reveals delete action', async ({ page }) => {
await page.goto('/messages');
const message = page.getByTestId('message-001');
const box = await message.boundingBox();
// Swipe from right to left
await page.touchscreen.swipe(
box.x + box.width - 10, box.y + box.height / 2, // start (right edge)
box.x + 10, box.y + box.height / 2, // end (left edge)
{ steps: 10 }
);
await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();
});(Note: page.touchscreen.swipe API may not exist on all Playwright versions; equivalent via page.mouse.move + page.touchscreen.tap or evaluate-based touch event synthesis.)
Pinch / zoom (multi-touch)
Multi-touch is hardest to emulate. Native testing is more reliable:
// XCUITest
let map = app.images["map"]
map.pinch(withScale: 2.0, velocity: 1.0) // zoom in 2×// Espresso doesn't ship pinch; use UIAutomator or third-party
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.executeShellCommand("input ...") // simulate pinchFor browser-based testing, multi-touch is generally not reliable - defer to native tests for pinch/rotate features.
Step 3 - Native gesture testing (XCUITest)
// XCUITest gesture catalog
app.buttons["btn"].tap()
app.buttons["btn"].doubleTap()
app.buttons["btn"].press(forDuration: 1.0) // long-press
app.cells.element(boundBy: 0).swipeLeft()
app.cells.element(boundBy: 0).swipeRight()
app.cells.element(boundBy: 0).swipeUp()
app.cells.element(boundBy: 0).swipeDown()
app.images["map"].pinch(withScale: 2.0, velocity: 1.0)
app.images["map"].twoFingerTap()
app.images["map"].rotate(.pi/4, withVelocity: 1.0)XCUITest's gesture API is the most complete - use for high-fidelity gesture tests on iOS.
Step 4 - Native gesture testing (Detox, RN)
await element(by.id('msg-001')).swipe('left', 'fast');
await element(by.id('btn')).tap();
await element(by.id('btn')).longPress();
await element(by.id('btn')).multiTap(2); // double-tap
await element(by.id('map')).pinch(2.0); // zoom in 2×Detox covers most common gestures; for less common ones, fall back to UIAutomator (Android) / XCUITest (iOS) via custom matchers.
Step 5 - Assertions per gesture class
Each gesture test should assert:
// Comprehensive gesture test
test('swipe-left + tap delete', async ({ page }) => {
// 1. Swipe
await swipeLeft(page, '[data-testid="msg-001"]');
// 2. State changed
await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();
// 3. Tap delete
await page.getByRole('button', { name: 'Delete' }).tap();
// 4. Final state
await expect(page.getByTestId('msg-001')).not.toBeVisible();
});Step 6 - CI integration
jobs:
gesture-tests:
strategy:
matrix:
project:
- mobile-iphone-15
- mobile-pixel-7
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test gestures.spec.ts --project=${{ matrix.project }}For native tests, see per-platform CI in xcuitest-suite / espresso-suite / detox-testing.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
.click() instead of .tap() on mobile profiles | Doesn't fire touchstart/touchend; misses gesture handlers. | .tap() (Step 2). |
| Multi-touch tests in browser emulation | Unreliable; pinch/rotate emulation is fragile. | Native tests for multi-touch (Step 3-4). |
Long-press via setTimeout only | Doesn't synthesize touch event sequence; handler may not fire. | Lower-level touch event APIs (Step 2). |
| Only testing the happy gesture | Cancel mid-gesture, fail mid-swipe scenarios uncovered. | Test cancel scenarios too (touchcancel, interrupted swipe). |
| Skipping animation wait | Assertion fires before transition completes; flaky. | waitForAnimations / pumpAndSettle (Step 5). |
| One mega gesture test | Failure obscures which gesture broke. | One test per gesture (Step 5 sample). |
Limitations
References
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-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.
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).
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).