Lesson 5 of 6

Lesson 05 — Gestures, Waits, and App Lifecycle

Title: Stable mobile interactions beyond simple taps
Description: Learn how to automate mobile behavior that does not exist on desktop web: swipes, scrolling, long press, app backgrounding, orientation changes, and permission dialogs. This lesson focuses on reliable patterns and explains why hardcoded coordinates and sleeps create flaky tests.

Why it matters for QA: Mobile users swipe, rotate, background apps, receive dialogs, and use unstable networks. Appium tests that only tap buttons miss the most important mobile risks.


1. Wait for conditions, not time

Avoid fixed sleeps:

javascriptjavascript
// Bad: slow and unreliable
await browser.pause(3000);

Prefer explicit waits:

javascriptjavascript
const loginButton = await $("~loginButton");
await loginButton.waitForEnabled({
  timeout: 10_000,
  timeoutMsg: "Login button did not become enabled",
});
await loginButton.click();

Wait for the condition your test actually needs.


2. Tap by locator first

javascriptjavascript
const submitButton = await $("~submitOrderButton");
await submitButton.waitForDisplayed({ timeout: 10_000 });
await submitButton.click();

If an element has a good accessibility id, use it. Coordinates should be a last resort.


3. Swipe with percentages

Hardcoded pixels fail across devices. Use the screen size.

javascriptjavascript
async function swipeUp() {
  const { width, height } = await driver.getWindowSize();
  const startX = Math.floor(width * 0.5);
  const startY = Math.floor(height * 0.8);
  const endY = Math.floor(height * 0.2);

  await driver.performActions([
    {
      type: "pointer",
      id: "finger1",
      parameters: { pointerType: "touch" },
      actions: [
        { type: "pointerMove", duration: 0, x: startX, y: startY },
        { type: "pointerDown", button: 0 },
        { type: "pointerMove", duration: 600, x: startX, y: endY },
        { type: "pointerUp", button: 0 },
      ],
    },
  ]);

  await driver.releaseActions();
}

This still uses coordinates, but they adapt to the device size.


4. Scroll until an element is visible

javascriptjavascript
async function scrollUntilVisible(selector, maxSwipes = 5) {
  for (let attempt = 0; attempt < maxSwipes; attempt++) {
    const element = await $(selector);

    if (await element.isDisplayed()) {
      return element;
    }

    await swipeUp();
  }

  throw new Error(`Element ${selector} was not visible after scrolling`);
}

Use a maximum swipe count so a broken test does not scroll forever.


5. Long press

javascriptjavascript
async function longPress(selector) {
  const element = await $(selector);
  await element.waitForDisplayed({ timeout: 10_000 });

  const location = await element.getLocation();
  const size = await element.getSize();
  const centerX = Math.floor(location.x + size.width / 2);
  const centerY = Math.floor(location.y + size.height / 2);

  await driver.performActions([
    {
      type: "pointer",
      id: "finger1",
      parameters: { pointerType: "touch" },
      actions: [
        { type: "pointerMove", duration: 0, x: centerX, y: centerY },
        { type: "pointerDown", button: 0 },
        { type: "pause", duration: 1000 },
        { type: "pointerUp", button: 0 },
      ],
    },
  ]);

  await driver.releaseActions();
}

The locator finds the element. Coordinates are calculated from the element's current position.


6. Background and foreground

javascriptjavascript
it("should preserve draft text after backgrounding", async () => {
  const notesInput = await $("~notesInput");
  await notesInput.setValue("Draft text");

  await driver.background(5); // app goes to background for 5 seconds

  await notesInput.waitForDisplayed({ timeout: 10_000 });
  await expect(await notesInput.getText()).toContain("Draft text");
});

This catches state loss bugs that never appear in simple happy-path tests.


7. Orientation changes

javascriptjavascript
it("should keep login form values after rotation", async () => {
  const emailInput = await $("~emailInput");
  await emailInput.setValue("qa@example.com");

  await driver.setOrientation("LANDSCAPE");
  await driver.setOrientation("PORTRAIT");

  await emailInput.waitForDisplayed({ timeout: 10_000 });
  await expect(await emailInput.getText()).toContain("qa@example.com");
});

On Android, rotation can recreate the Activity. That makes this a valuable test.


8. Android permission dialogs

Permission dialogs are system UI, not your app UI. The resource IDs come from Android.

javascriptjavascript
async function denyAndroidPermission() {
  const denyButton = await $(
    "id=com.android.permissioncontroller:id/permission_deny_button"
  );

  await denyButton.waitForDisplayed({ timeout: 10_000 });
  await denyButton.click();
}

For tests where permission is not the focus, auto-grant permissions in capabilities:

javascriptjavascript
{
  "appium:autoGrantPermissions": true
}

For tests where permission denial is the focus, start from a clean app state and tap "Deny" intentionally.


Official docs


Quick recap

NeedRecommended pattern
Tap buttonLocate by accessibility id, wait, click
Wait for loadingExplicit wait for a specific condition
SwipePercentages of screen size
Scroll to elementLoop with max attempts
Long pressCalculate center from located element
PermissionsAuto-grant by default, deny intentionally in dedicated tests
LifecycleBackground, foreground, rotate, then assert state

Practice exercises

  1. Write a swipeUp helper using screen size percentages.
  2. Write scrollUntilVisible(selector, maxSwipes).
  3. Automate a long press on a list item.
  4. Background an app for 5 seconds and verify the screen state.
  5. Rotate a form screen and verify typed text remains.

Homework

Create a small mobile interaction suite:

  1. One test scrolls to an item and taps it.
  2. One test long-presses an item and verifies a context menu.
  3. One test backgrounds the app and verifies state preservation.
  4. One test rotates the screen and verifies layout/state.
  5. No test uses browser.pause() or absolute XPath.

Next: Lesson 06 — CI, cloud devices, and Appium maintenance (lab06.md).