Skip to content

sharisroy/mobilewright-testapp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MobileWright — Android E2E Test Suite

End-to-end tests for com.haris.testapp using MobileWright, a Playwright-style framework for Android and iOS automation.


Table of Contents


Project Overview

This project tests the full user lifecycle of a personal-information Android app:

  1. Home screen — verify all UI elements before any interaction
  2. Registration — fill and submit the sign-up form
  3. Login — authenticate with the registered credentials
  4. Profile screen — verify the initial empty profile state
  5. Update profile — fill gender, date of birth, marital status, address, and mobile
  6. Verify updated profile — assert every row shows the saved values
  7. Change password — update to a new password
  8. Logout — sign out and return to the home screen
  9. Re-login — authenticate with the new password
  10. Final verification — confirm the profile header is correct after re-login

App ID: com.haris.testapp
Platform: Android
Test timeout: 600 s (the full lifecycle takes ~5 minutes on a real device)


Setup

npm install

Configure mobilewright.config.ts (already committed):

import { defineConfig } from 'mobilewright';

export default defineConfig({
  testDir: './tests',
  reporter: 'html',
  platform: 'android',
  timeout: 30_000,
});

Connect an Android device or start an emulator before running.


Running Tests

# Run all tests
npm test

# Run only the lifecycle test
npm run test:file

Open the HTML report after a run:

npx mobilewright show-report

Test Coverage

File Description
tests/testApp.spec.ts Full user lifecycle: register → login → update profile → change password → re-login
tests/example.spec.ts Minimal smoke test
tests/full_testApp.spec.ts Extended variant

Key Patterns

Locator Strategies

Each element is located with the most stable available locator — in priority order:

// 1. Accessibility label (most stable)
await screen.getByLabel('Full Name Edit Text').fill('Haris Roy');

// 2. Android resource ID
await screen.getByTestId('com.haris.testapp:id/btn_login').tap();

// 3. Unique visible text (use when no label or ID exists)
await screen.getByText('OR').getText();

The id() helper keeps resource IDs DRY:

const id = (value: string) => `${APP_ID}:id/${value}`;

await screen.getByTestId(id('et_login_email')).fill('haris@mail.com');
// expands to: screen.getByTestId('com.haris.testapp:id/et_login_email')

Reading Off-Screen Values

getText() requires the element to be visible. After scrolling down, fields near the top of a form are off-screen and will timeout. Use viewTree() instead — it returns all nodes regardless of scroll position:

const formNodes = collectAll(await screen.viewTree());
const getFormValue = (resourceId: string): string =>
    formNodes.find((n: any) =>
        n.identifier === id(resourceId) || n.resourceId === id(resourceId)
    )?.text || '';

const fullName = getFormValue('et_full_name');   // works even if scrolled off screen
const address  = getFormValue('et_address');

The same technique is used to read profile row values, where the row container (LinearLayout) returns "" from getText() because the text lives in a child TextView. The getRowValue helper finds the tv_row_value child by matching Y coordinates within the row's bounds:

const profileNodes = collectAll(await screen.viewTree());
const getRowValue = (rowResourceId: string): string => {
    const row = profileNodes.find((n: any) =>
        n.identifier === rowResourceId || n.resourceId === rowResourceId
    );
    if (!row) return '';
    const { y: ry, height: rh } = row.bounds;
    return profileNodes.find((n: any) =>
        n.isVisible &&
        (n.identifier === id('tv_row_value') || n.resourceId === id('tv_row_value')) &&
        n.bounds.y >= ry && n.bounds.y < ry + rh
    )?.text || '';
};

expect(getRowValue(id('row_full_name'))).toBe('Haris Roy');
expect(getRowValue(id('row_gender'))).toBe('N/A');

Handling Dropdowns (AutoCompleteView)

Android AutoCompleteView popups are not in the accessibility treegetByText('Male') will not find the dropdown option. The fix is to tap at a calculated coordinate below the field's bottom edge:

await screen.getByTestId(id('acv_gender')).tap();
await sleep(800); // wait for popup to render

const nodes = collectAll(await screen.viewTree());
const genderNode = nodes.find((n: any) =>
    n.identifier === id('acv_gender') || n.resourceId === id('acv_gender')
);
if (!genderNode) throw new Error('Could not find gender dropdown field');

const { x: gx, y: gy, width: gw, height: gh } = genderNode.bounds;
await device.driver.tap(Math.round(gx + gw / 2), Math.round(gy + gh + 50));

Scrolling to Hidden Fields

Some form fields (mobile number, terms checkbox, save button) are below the fold. findVisibleElement scrolls up automatically until the element appears:

const mobileField = await findVisibleElement([
    { name: 'et_mobile full id',          locator: screen.getByTestId(id('et_mobile')) },
    { name: 'Mobile Number Edit Text',    locator: screen.getByLabel('Mobile Number Edit Text') },
], 5); // up to 5 swipes

await mobileField.fill('01712345678');

The helper:

const findVisibleElement = async (locators: LocatorItem[], maxSwipes = 0) => {
    for (let i = 0; i <= maxSwipes; i++) {
        for (const item of locators) {
            if (await item.locator.isVisible().catch(() => false)) {
                return item.locator;
            }
        }
        if (i < maxSwipes) {
            await device.driver.swipe('up', { distance: 650 });
            await sleep(500);
        }
    }
    throw new Error(`Could not find visible element: ${locators.map(i => i.name).join(', ')}`);
};

WebSocket Stability

MobileWright communicates with the device over a WebSocket. On a real device under load (especially after a ~5-minute test run), rapid interactions can drop the connection with code=1005. Rules that keep the session alive:

  1. Wait for the screen section before filling fields — never fill immediately after a screen transition.
  2. toBeVisible() before each fill() — ensures the field is ready.
  3. dismissKeyboard() between fields — closes the IME so its animation doesn't interfere with the next interaction.
  4. sleep() after taps that trigger network calls — password update and login tap need ~3 s to complete before the next assertion.
// Good pattern (Change Password section):
await changePasswordButton.tap();
await expect(screen.getByLabel('Change Password Section')).toBeVisible({ timeout: 20_000 });
await sleep(1000);

await expect(screen.getByTestId(id('et_current_password'))).toBeVisible({ timeout: 10_000 });
await screen.getByTestId(id('et_current_password')).fill('Password123');

await expect(screen.getByTestId(id('et_new_password'))).toBeVisible({ timeout: 10_000 });
await screen.getByTestId(id('et_new_password')).fill('Password456');
await dismissKeyboard();

await screen.getByTestId(id('btn_update_password')).tap();
await sleep(3000); // wait for API call before polling the screen
await waitForProfileScreen(25_000);

Helper Utilities

Helper Purpose
collectAll(nodes) Recursively flattens viewTree() into a single array for searching
dismissKeyboard() Presses BACK to close the soft keyboard, then waits 500 ms
findVisibleElement(locators, maxSwipes) Swipes up until one of the candidate locators becomes visible
dumpVisibleUi(title) Prints all visible nodes to the console — useful for debugging unexpected screens
waitForProfileScreen(timeout) Waits for My Profile text; dumps the UI and throws a clear error if it never appears
assertProfileHeader(name, email) Asserts the profile page title, name, and email from the view tree

Locator Reference

getByLabel

Finds an element by its accessibility label — the most stable locator because labels are set by developers explicitly.

const text = await screen.getByLabel('App Name').getText();
await expect(screen.getByLabel('Login Button')).toBeVisible();

Best for: any element with an explicit accessibility label.


getByText

Finds an element by its visible text content.

const text = await screen.getByText('Sign In').getText();
const text = await screen.getByText('Sign in', { exact: false }).getText();
const text = await screen.getByText(/manage your/i).getText();

⚠️ Breaks if copy changes (translations, A/B tests). Prefer getByLabel.


getByTestId

Finds an element by its Android resource ID or iOS accessibility identifier.

const email = await screen.getByTestId('com.haris.testapp:id/et_login_email').getText();
await expect(screen.getByTestId('com.haris.testapp:id/btn_login')).toBeVisible();

Best for: form fields and buttons with resource IDs — very reliable on Android.


getByType

Finds elements by their native component type. Returns a collection.

const first = await screen.getByType('android.widget.TextView').first().getText();
const second = await screen.getByType('android.widget.EditText').nth(1).getText();
const count = await screen.getByType('android.widget.Button').count();

Common Android types:

Type Description
android.widget.TextView Text label
android.widget.EditText Input field
android.widget.Button Button
android.widget.CheckBox Checkbox
android.widget.RadioButton Radio button
android.widget.AutoCompleteTextView Dropdown text field
android.widget.LinearLayout Layout container (returns "" from getText)
android.widget.ScrollView Scrollable container
androidx.cardview.widget.CardView Card container

⚠️ .nth() is order-dependent — fragile if layout changes. Use as last resort.


getByRole

Cross-platform semantic role locator — same code works on iOS and Android.

const loginText = await screen.getByRole('button', { name: 'Login' }).getText();
const email = await screen.getByRole('textfield').first().getText();
Role Android iOS
button Button, ImageButton XCUIElementTypeButton
textfield EditText XCUIElementTypeTextField
text TextView XCUIElementTypeStaticText
image ImageView XCUIElementTypeImage
switch Switch XCUIElementTypeSwitch
checkbox CheckBox

getByPlaceholder

Finds an input by its placeholder text.

const field = await screen.getByPlaceholder('Enter your email').getText();
await expect(screen.getByPlaceholder('Password')).toBeVisible();

Assertions

All assertions auto-retry until the condition is met or timeout is reached.

await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();
await expect(locator).toBeEnabled();
await expect(locator).toHaveText('Test Application');  // exact
await expect(locator).toContainText('Application');    // substring
await expect(locator).toHaveText(/test/i);             // regex

// Custom timeout
await expect(locator).toBeVisible({ timeout: 15_000 });

Inspecting the Screen

When locators are unknown, dump the full accessibility tree:

test('inspect screen', async ({ screen, device }) => {
    await device.launchApp('com.your.app');

    const tree = await screen.viewTree();

    function printNodes(nodes: typeof tree, depth = 0) {
        const indent = '  '.repeat(depth);
        for (const node of nodes) {
            console.log(
                `${indent}type=${node.type}` +
                (node.label      ? ` label="${node.label}"`           : '') +
                (node.text       ? ` text="${node.text}"`             : '') +
                (node.resourceId ? ` resourceId="${node.resourceId}"` : '') +
                ` visible=${node.isVisible}`
            );
            if (node.children?.length) printNodes(node.children, depth + 1);
        }
    }

    printNodes(tree);
});

Pick your locator from the output:

Output field Locator
label="..." screen.getByLabel('...')
text="..." screen.getByText('...')
resourceId="..." screen.getByTestId('...')
type="..." screen.getByType('...')

Which Locator Should I Use?

Does the element have an accessibility label?
  └─ YES → getByLabel('...')          ✅ most stable

Does it have a resource ID / test ID?
  └─ YES → getByTestId('...')         ✅ reliable on Android

Is it a button or input and you want cross-platform code?
  └─ YES → getByRole('button' / 'textfield', { name: '...' })

Is the visible text unique on screen?
  └─ YES → getByText('...')

None of the above?
  └─ getByType('...').nth(n)          ⚠️ last resort only

Priority: getByLabel > getByTestId > getByRole > getByText > getByType

About

Sample Mobilewright project demonstrating Android/iOS automation, form validation, and complete user flows.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors