level 17 / touch-and-appium

Touch Gestures & Appium Basics

Test swipe, pinch, and scroll gestures with Playwright, and understand when to reach for Appium for native mobile testing.

Touch Events in Playwright

Playwright with isMobile: true (or a mobile device preset) enables touch event simulation. Use tap() for single touches, touchscreen API for gestures.

import { test, expect, devices } from '@playwright/test';

test.use({ ...devices['iPhone 15'] });

// Single tap
test('product tappable on mobile', async ({ page }) => {
  await page.goto('/products');
  await page.locator('.product-card').first().tap();
  await expect(page).toHaveURL(/\/product\//);
});

// Long press (tap and hold)
test('long press shows context menu', async ({ page }) => {
  await page.goto('/');
  const item = page.locator('.draggable-item').first();
  await item.dispatchEvent('pointerdown');
  await page.waitForTimeout(800);  // hold for 800ms
  await item.dispatchEvent('pointerup');

  await expect(page.getByRole('menu')).toBeVisible();
});

Swipe Gestures

Playwright’s touchscreen.tap() and mouse drag simulate swipe:

// Horizontal swipe (carousel, side menu)
test('swipe carousel to next slide', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.use({ isMobile: true, hasTouch: true });
  await page.goto('/gallery');

  const carousel = page.locator('.carousel');
  const box = await carousel.boundingBox();

  if (!box) throw new Error('Carousel not found');

  // Swipe left: start right of center, end left of center
  await page.touchscreen.tap(box.x + box.width * 0.8, box.y + box.height / 2);
  await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2);
  await page.mouse.down();
  await page.mouse.move(box.x + box.width * 0.2, box.y + box.height / 2, { steps: 10 });
  await page.mouse.up();

  await expect(page.locator('.carousel-slide.active')).toHaveAttribute('data-index', '1');
});

// Vertical scroll (infinite scroll list)
test('scroll loads more items', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/feed');

  const initialCount = await page.locator('.feed-item').count();

  // Scroll to bottom of page
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
  await page.waitForResponse(resp => resp.url().includes('/api/feed'));

  const newCount = await page.locator('.feed-item').count();
  expect(newCount).toBeGreaterThan(initialCount);
});

Pull to Refresh

test('pull to refresh reloads content', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('/dashboard');

  // Pull down gesture from near top of viewport
  await page.mouse.move(195, 100);
  await page.mouse.down();
  await page.mouse.move(195, 350, { steps: 20 });  // drag down 250px
  await page.mouse.up();

  await expect(page.getByRole('progressbar')).toBeVisible();
  await expect(page.getByRole('progressbar')).not.toBeVisible({ timeout: 5000 });
});

Playwright vs Appium

FeaturePlaywrightAppium
TargetWeb (in mobile browser)Native apps + web views
SetupZero (built-in)Appium server + driver + device
Native UI elementsNoYes (buttons, pickers, alerts)
Real deviceNot requiredSupported (iOS/Android)
EmulatorBrowser-level onlyAndroid emulator, iOS simulator
PerformanceFastSlower (WDA/UIAutomator bridge)
Best forMobile web, PWAsNative apps, hybrid apps

When to Use Appium

Reach for Appium when:

✅ Testing a native iOS or Android app
✅ Testing hybrid apps (native shell + web views)
✅ Testing biometric auth (Face ID, fingerprint)
✅ Testing push notifications
✅ Testing deep links (custom URL schemes)
✅ Testing background/foreground app state
✅ Testing device permissions (camera, contacts)

Playwright is sufficient for:

✅ Mobile web browsers (Chrome on Android, Safari on iOS)
✅ Progressive Web Apps (PWAs)
✅ Responsive design validation
✅ Touch gesture simulation

Appium Quickstart (for reference)

// Install: npm install -D webdriverio @wdio/appium-service
// Requires: Appium server, Xcode (iOS), Android SDK

import { remote } from 'webdriverio';

const driver = await remote({
  hostname: 'localhost',
  port: 4723,
  capabilities: {
    platformName: 'Android',
    'appium:deviceName': 'Pixel 7 Emulator',
    'appium:app': '/path/to/app.apk',
    'appium:automationName': 'UiAutomator2',
  },
});

// Find native element
const loginButton = await driver.$('~login-button');  // accessibility ID
await loginButton.click();

// Or XPath for native
const emailField = await driver.$('//android.widget.EditText[@text="Email"]');
await emailField.setValue('test@example.com');

await driver.deleteSession();