top of page
Writer's pictureMichelle Kidby

Mobile Test Automation for iOS & Android Native apps using Playwright

Updated: Nov 24, 2022

Why would I want to run WebdriverIO with Playwright Test runner?


There are many reasons why you might want to use Playwright, and there are several articles on the advantages of speed, ease of use, and their test runner Playwright Test. A glaring disadvantage of Playwright over other frameworks such as Selenium or WebdriverIO is that they will not support native iOS or Android apps. This has been talked about in depth on their GitHub and it has been stated that this is not within the scope of their project. Here is a solution for if you would want to run your mobile end-to-end tests along with your browser end-to-end tests.


For this test framework, Playwright Test is using WebdriverIO and Appium to run mobile tests within the same framework as browser tests.


The great thing about this is you can use the same language and test runner as you are using for both web and mobile test frameworks. They can be combined within one project to make your code more modular. Note that the following are setup steps for Mac, for windows the installation will be similar but iOS apps will not be available.


The full project can be found here in Github. https://github.com/strangetest/playwright-wdio-appium


#1 - Install Prerequisite Dependencies

  • Node.js

  • Java SDK

  • Android SDK & AVD Emulator

  • Appium

  • appium-doctor (for help with setup)

  • Xcode

  • carthage

#2 - Create a Project


Using an IDE or from within a terminal window create a project then run npm init. Then add the following files to to the root of project.


package.json


{
  "name": "playwright-wdio-appium",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "npx playwright test"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@playwright/test": "^1.28.1",
    "@types/node": "*",
    "appium": "^1.22.3",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.3",
    "webdriverio": "^7.27.0"
  }
}

Create a .gitignore file, there are multiple examples online, below is the one for this project.


.gitignore


# Logs
logs
*.log
npm-debug.log*
appium.log*

# Dependency directories
node_modules/*

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# System & IDE files
.idea/
.vscode/
.DS_Store

# app dist
frameworks/mobile/apps/*

Create a playwright config & tsconfig file, below are the ones I created for this example.


playwright.config.json

import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
  use: {
    headless: false,
    viewport: { width: 1280, height: 720 },
    ignoreHTTPSErrors: true,
    video: 'on',
    trace: 'on',
  },
  reporter: [
    ['html'],
    ['list'],
    ['json', {  outputFile: 'test-results.json' }]
  ],
};
export default config;

tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "types": [
      "node",
      "webdriverio/async",
    ]
  }
}

Within the root of the project in a terminal window run the following command:


npm install

#3 - Framework Design


Now we'll create the folders and files for the framework. Using Page Object Model and the fixture pattern.


  1. In the project root create the following folders:

- core

---- base-tests

-------- contactsBaseTest.ts

Note that the settings for the Android emulator are given in this file.

import { expect, test as base } from '@playwright/test';
import { Browser, remote, RemoteOptions } from 'webdriverio';
import { ContactsPage } from '../pages/ContactsPage';
import { ProjectCapabilities } from '../config/ProjectCapabilities';


type Fixtures = {
    driver: Browser<'async'>;
    contactsPage: ContactsPage;
}

const test = base.extend<Fixtures>({
    driver: async ({}, use) => {
        const remoteOptions: RemoteOptions = ProjectCapabilities
            .androidCapabilities(
                'com.android.contacts',
                'com.android.contacts.activities.PeopleActivity', 
                { device: 'Pixel4'}
            );
        const driver = await remote(remoteOptions);
        await use(driver);
        await driver.deleteSession();
    },
    contactsPage: async ({ driver }, use) => {
        const contactsPage = new ContactsPage(driver, 'ANDROID');
        await use(contactsPage);
    },
});

export { expect, test as contactsBaseTest };

---- config

-------- ProjectCapabilities.ts


Note that for iOS a bundleId for the AUT is required. That can be found by opening the mobile app code within XCODE. For Android the appPackage cam be found within the manifest.xml within the AUT code base.

import { RemoteOptions } from "webdriverio";

class ProjectCapabilities {
    private static webDriverPath: string = '/wd/hub';
    private static webDriverPort: number = 4723;
    
    static androidCapabilities(appPackage, appActivity, device, additionalCaps?: object): RemoteOptions {
        const desiredCapabilities = {
            platformName: "Android",
            deviceName: device.name,
            appPackage: appPackage,
            appActivity: appActivity,
            automationName: "UiAutomator2",
            ...additionalCaps
        };
        return {
            path: this.webDriverPath,
            port: this.webDriverPort,
            capabilities: desiredCapabilities
        }
    }
    static iosCapabilities(appPackage, appActivity, device, additionalCaps?: object): RemoteOptions {
        const desiredCapabilities = {
            platformName: "iOS",
            deviceName: device.name,
            bundleId: "",
            appPackage: appPackage,
            automationName: "XCUITest",
            ...additionalCaps
        };
        return {
            path: this.webDriverPath,
            port: this.webDriverPort,
            capabilities: desiredCapabilities
        }
    }
}
export { ProjectCapabilities };

---- pages

-------- BasePage.ts


import { Browser, Element } from 'webdriverio';

class BasePage {
    readonly driver: Browser<'async'>;
    constructor(driver: Browser<'async'>) {
        this.driver = driver;
    }
    async find(selector: string, timeout: number = 30 * 1000) {
        const element: Element<'async'> = await this.driver.$(selector);
        await element.waitForExist({
            timeout,
        });
        return element;
    }
    async tap(selector: string, timeout: number = 30 * 1000) {
        const element: Element<'async'> = await this.find(selector, timeout);
        return element.click();
    }
}

export { BasePage };

-------- ContactsPage.ts


import { BasePage } from "./BasePage"

class ContactsPage extends BasePage {
    readonly platform: string;
    readonly selectors: { [key: string]: string };
    readonly SELECTORS = {
        ANDROID: {
            ALLOW_BUTTON: 'id:com.android.permissioncontroller:id/permission_allow_button',
        },
        IOS: {
            ALLOW_BUTTON: '~Allow',
        }
    }
    constructor(driver, platform: string) {
        super(driver);
        this.platform = platform.toUpperCase();
        this.selectors = this.SELECTORS[this.platform]
    }
    async getAllowButton() {
        return this.find(this.selectors.ALLOW_BUTTON)
    }
    async tapAllowButton() {
        return this.tap(this.selectors.ALLOW_BUTTON)
    }
}
export { ContactsPage };

Within your IDE the structure should look similar to this:


#4 - Create a Test


At the root of the project create the tests folder and test file.


- tests

---- android.spec.ts


import { expect, contactsBaseTest as test } from '../core/base-tests/contactsBaseTest';

test.describe('run webdriver-io with playwright test', async () => {

    test('should navigate to contacts page', async ({ driver, contactsPage }) => {
        const allowButton = await contactsPage.getAllowButton();
        expect(allowButton).toBeTruthy();
        await contactsPage.tapAllowButton();
        await driver.pause(2 * 1000);
    });

});


Note that the Appium Inspector can be used for both Android & iOS. It's useful for finding or creating selectors to use in your test. For Android this example is using UIAutomator2 and for iOS XCUITest.


#5 - Run test and view the Playwright Test Report

Note that you will need to have already created or configured the Android emulator. For iOS you would need to use the XCODE simulator. A test for iOS is not included because you will need to have a mobile app already as an application under test to work with.


To run the test:

npm run test

To show the test report

npx playwright show-report


In conclusion


Playwright and the language of your choice can be used for both your mobile and web applications in test in one test automation framework. Using the same test runner and language for your web and mobile automation allows your test framework to be more modular without having to switch contexts.


684 views0 comments

Comments


bottom of page