Go beyond the software, automate hardware scenarios on

Android Emulators

About me

🇦🇷Bruno Alassia 🇨🇦

Senior Software Engineer at Visual testing team @Saucelabs

Former Emulator/Simulator team

 

Python developer, NodeJS enthusiast,

mobile development. Open source lover.

Contributor to Appium

@vrunoa

vrunoa

vruno@saucelabs.com

Usual case scenario

describe('Android Conference login tests', async () => {
    before(async () => {
        driver = await wd.promiseChainRemote(endpoint)
        res = await driver.init(caps)
    });
    after(async () => {
        await driver.quit()
    });
    it('Test login elements are disaplyed', async () => {
        let el = await driver.elementById("textUsername");
        expect(await el.isDisplayed()).to.equal(true);
        expect(await el.text()).to.equal('Username');
        el = await driver.elementById("userEditText");
        expect(await el.isDisplayed()).to.equal(true);
        el = await driver.elementById("textPassword");
        expect(await el.isDisplayed()).to.equal(true);
        expect(await el.text()).to.equal('Password');
        el = await driver.elementById("pwdEditText");
        expect(await el.isDisplayed()).to.equal(true);
        el = await driver.elementById("enterBtt");
        expect(await el.isDisplayed()).to.equal(true);
    });
...

But what if we want to go beyond the user interaction with our app UI?

Inconming SMS

Phone Call is accepted

Signal strength changes

Network drops

Battery is less than 5%

Charger is disconnected

Fingerprint is used

Device is rotated

Light sensor found is too dark

...and more

Android Emulator to the rescue

Wait a minute! I have to do all this manually?

  • driver.fingerprint()
  • driver.sendSms(phoneNumber, message)
  • driver.gsmCall(phoneNumber, action)
  • driver.gsmSignal(signalStrength)
  • driver.gsmVoice(state)
  • driver.powerCapacity(percent)
  • driver.powerAC(state)
  • driver.networkSpeed(netspeed)

Let's see some of this in action

sending and reading incoming sms

it('test code is set on SMS received', async () => {
  let phoneContainer = await driver.elementById("enterPhoneContainer");
  expect(await phoneContainer.isDisplayed()).to.equal(true);
  let el = await driver.elementById("phoneEditText");
  expect(await el.isDisplayed()).to.equal(true);
  await el.sendKeys("60512345678")
  el = await driver.elementById("confirmBtt");
  await el.click();
  let codeContainer = await driver.elementById("validCodeContainer");
  expect(await codeContainer.isDisplayed()).to.equal(true);
  await driver.sendSms('2020', '1234');
  await sleep(500);
  el = await driver.elementById('smsCode1');
  expect(await el.text()).to.equal("1");
  el = await driver.elementById('smsCode2');
  expect(await el.text()).to.equal("2");
  el = await driver.elementById('smsCode3');
  expect(await el.text()).to.equal("3");
  el = await driver.elementById('smsCode4');
  expect(await el.text()).to.equal("4");
});

handle login using the fingerprint

it('test shows correct message on failed fingerprint', async () => {
  await driver.fingerprint(1112);
  let el = await driver.elementById("fingerprintMessage");
  expect(await el.isDisplayed()).to.equal(true);
  expect(await el.text()).to.equal("Failed to authenticate!");     
  await sleep(1500);   
});
it('test shows correct message on success fingerprint', async () => {
  await driver.fingerprint(1111);
  let el = await driver.elementById("fingerprintMessage");
  expect(await el.isDisplayed()).to.equal(true);
  expect(await el.text()).to.equal("Welcome you\'re authenticated");
  await sleep(1500);
});

pause task on incoming phone call

it('test video is paused on incoming call', async () => {
  await sleep(2500);
  await driver.gsmCall('6505551212', 'call');
  await sleep(8000);
  let el = await driver.elementById('android:id/pause');
  expect(await el.isDisplayed()).to.equal(true);
});
it('test video continues after call ended', async () => {
  let el = await driver.elementById('android:id/pause');
  await driver.gsmCall('6505551212', 'cancel');
  await sleep(5000);
  expect(await el.isDisplayed()).to.equal(false);
  await sleep(3000);
});

checking battery state

it('test battery level text is changed', async () => {
  await driver.powerCapacity(52);
  let el = await driver.elementById("batteryPercent");
  expect(await el.isDisplayed()).to.equal(true);
  expect(await el.text()).to.equal("52%");
  await sleep(1000);
});
it('test alarm starts on battery charged', async () => {
  await driver.powerCapacity(100);
  let el = await driver.elementById("batteryPercent");
  expect(await el.isDisplayed()).to.equal(true);
  expect(await el.text()).to.equal("100%");
  el = await driver.elementById("batteryAlarmText");
  expect(await el.isDisplayed()).to.equal(true);
  el = await driver.elementById("stopBtt");
  expect(await el.isDisplayed()).to.equal(true);
  await sleep(2500);
});

How all this magic works?

adb emu sms send '6505551212' 'Hello from adb'

...become a new appium feature

adb emu sms send '6505551212' 'Hello from adb'
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

Inconming SMS

Phone Call is accepted

Signal strength changes

Network drops

Battery is less than 5%

Charger is disconnected

Fingerprint is used

Device is rotated

Light sensor found is too dark

...and more

not supported in Appium

...until now

let's add light sensor support to Appium, right now

Light sensor found is too dark

Our light sensor application

Road to an Appium feature

adb emu ...
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

adb emu ... ?

# inside telnet
sensor set light 6000

adb emu sensor set light X

# using adb
adb emu sensor set light 6000

Road to an Appium feature

adb emu sensor set light 6000
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

Calling the new command from appium-adb

/**
 * Emulate light sensor on the connected emulator.
 *
 * @param {float} light 
 */
emuMethods.lightSensor = async function lightSensor (light = 0) {
  // light sensor <light> allows you to set the ligth sensor values on the emulator.
  await this.adbExecEmu(['sensor', 'set', 'light', light]);
};

Our first step is to implement our know adb emu command in the appium-adb package. All commands for the emulator are in /lib/tools/adb-emu-commands.js

To quickly test this new appium-adb command we're going to use appium-adb-repl, a simple REPL package for appium-adb

npm link

On the appium-adb folder we run

and in the appium-adb-repl folder we link the package

npm link appium-adb
adb.lightSensor(0);

Now that we're set, let's watch it working

Road to an Appium feature

adb emu sensor set light 6000
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

updating the driver

commands.lightSensor = async function lightSensor (light) {
  if (!this.isEmulator()) {
    log.errorAndThrow('lightSensor method is only available for emulators');
  }
  await this.adb.lightSensor(light);
};

With the method implement in appium-adb we can now call this from the driver. In the appium-android-driver we'll add a method in /lib/command/actions.js  calling the lightSensor

and npm link it so it can be used in appium

Road to an Appium feature

adb emu sensor set light 6000
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

'/wd/hub/session/:sessionId/appium/device/light_sensor': {
    POST: {command: 'lightSensor', payloadParams: {required: ['light']}}
},

On the appium-base-driver we are going to add the new endpoint for this new feature. All endpoints in the base driver are defined in /lib/protocol/routes.js; using finger_print method as example;

adding a new endpoint

'/wd/hub/session/:sessionId/appium/device/finger_print': {
    POST: {command: 'fingerprint', payloadParams: {required: ['fingerprintId']}}
},

we'll add a new light_sensor endpoint with light  as a required parameter

and npm link it so it can be used in appium

Road to an Appium feature

adb emu sensor set light 6000
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

Baking it all together into Appium

With all the changes applied to the appium dependencies, our text step is to put all this together in the appium server. In order to do this, we're going to link all the edited packages from the appium project

npm link appium-adb
npm link appium-android-driver
npm link appium-base-driver

Once appium is using all the new dependencies, let's start the appium server from the source code

node .
[Appium] Welcome to Appium v1.13.0-beta.3-light-sensor (REV 783252b09c5f932bd094b78c773da0865995f370)
[Appium] Appium REST http interface listener started on 0.0.0.0:4723

Road to an Appium feature

adb emu sensor set light 6000
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

Updating the appium client

In our case we're going to update the Appium nodeJS client by adding a new method in lib/commands.js script.

/**
 * lightSensor(light, cb) -> cb(err)
 *
 * @jsonWire POST /session/:sessionId/appium/device/light_sensor
 */
commands.lightSensor = function() {
  var fargs = utils.varargs(arguments);
  var cb = fargs.callback,
      light = fargs.all[0];
  var data = {light: light};
  this._jsonWireCall({
    method: 'POST'
    , relPath: '/appium/device/light_sensor'
    , data: data
    , cb: simpleCallback(cb)
  });
};

and npm link it so it can be used in the tests we're about to create

Road to an Appium feature

adb emu sensor set light 6000
appium-adb
appium-android-driver
appium-base-driver
appium
appium clients

implement adb emu command in

appium-adb package

add a call to appium-adb if the device in test is an emulator

add a new endpoint to call the new method

implemented in the android driver

integrate all the new dependencies

containing the emulator methods

add a new method calling the new endpoint implemented in appium

Building our light sensor test

Appium is running, the nodeJS client has been updated adding the new lightSensor method, so the only thing left is to create tests using this new feature. Let's do it. Let's create to handle this 3 case scenarios for our application:

const wd = require('wd');
...
describe('Test light sensor', async () => {
...
  it('uses light mode on high luminance', async () => {
    await driver.lightSensor(40000);
    let container = await driver.elementById("sensorContainer");
    expect(await container.getAttribute('content-desc')).to.equal("#FFFFFF");
    let textEl = await driver.elementById("sensorText");
    expect(await textEl.getAttribute('content-desc')).to.equal("#000000");
  });
  it ('uses dark mode on Zero luminance', async () => {
    await driver.lightSensor(0);
    await sleep(500);
    let container = await driver.elementById("sensorContainer");
    expect(await container.getAttribute('content-desc')).to.equal("#000000");
    let textEl = await driver.elementById("sensorText");
    expect(await textEl.getAttribute('content-desc')).to.equal("#FFFFFF");
  });
...
});
  • high luminance uses light mode
  • no luminance uses dark mode
  • minimal luminance uses light mode
...
  it('uses light mode when luminance is above the minimum', async () => {
    await driver.lightSensor(5001);
    await sleep(500);
    let container = await driver.elementById("sensorContainer");
    expect(await container.getAttribute('content-desc')).to.equal("#FFFFFF");
    let textEl = await driver.elementById("sensorText");
    expect(await textEl.getAttribute('content-desc')).to.equal("#000000");
  });
});

Thank you!

@vrunoa

vrunoa

Q&A

Go beyond the software, automate hardware scenarios on Android Emulators

By Bruno Alassia

Go beyond the software, automate hardware scenarios on Android Emulators

  • 14,279