-
-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add javascript wrappers over Settings App APIs (#131)
- Loading branch information
1 parent
a5fddd8
commit bd56d5e
Showing
21 changed files
with
1,127 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
coverage | ||
build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
name: Unit Tests | ||
|
||
on: [pull_request, push] | ||
|
||
|
||
jobs: | ||
prepare_matrix: | ||
runs-on: ubuntu-latest | ||
outputs: | ||
versions: ${{ steps.generate-matrix.outputs.versions }} | ||
steps: | ||
- name: Select 3 most recent LTS versions of Node.js | ||
id: generate-matrix | ||
run: echo "versions=$(curl -s https://endoflife.date/api/nodejs.json | jq -c '[[.[] | select(.lts != false)][:3] | .[].cycle | tonumber]')" >> "$GITHUB_OUTPUT" | ||
|
||
|
||
test: | ||
needs: | ||
- prepare_matrix | ||
strategy: | ||
matrix: | ||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }} | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: ${{ matrix.node-version }} | ||
- name: Use Java 17 | ||
uses: actions/setup-java@v3 | ||
with: | ||
distribution: 'temurin' | ||
java-version: '17' | ||
- run: npm install --no-package-lock | ||
name: Install dev dependencies | ||
- run: npm run build | ||
name: Build | ||
- run: npm run js:lint | ||
name: Run linter | ||
- run: npm run js:test | ||
name: Run unit tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
require: ['ts-node/register'], | ||
forbidOnly: Boolean(process.env.CI), | ||
color: true | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
const path = require('path'); | ||
const path = require('node:path'); | ||
const { SettingsApp } = require('./build/lib/client'); | ||
const constants = require('./build/lib/constants'); | ||
|
||
module.exports = { | ||
path: path.resolve(__dirname, 'apks', 'settings_apk-debug.apk') | ||
path: path.resolve(__dirname, 'apks', 'settings_apk-debug.apk'), | ||
SettingsApp, | ||
...constants, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { log, LOG_PREFIX } from './logger'; | ||
import _ from 'lodash'; | ||
import { waitForCondition } from 'asyncbox'; | ||
import { SETTINGS_HELPER_ID, SETTINGS_HELPER_MAIN_ACTIVITY } from './constants.js'; | ||
import { setAnimationState } from './commands/animation'; | ||
import { getClipboard } from './commands/clipboard'; | ||
import { setGeoLocation, getGeoLocation, refreshGeoLocationCache } from './commands/geolocation'; | ||
import { setDeviceSysLocale } from './commands/locale'; | ||
import { scanMedia } from './commands/media'; | ||
import { setDataState, setWifiState } from './commands/network'; | ||
import { getNotifications } from './commands/notifications'; | ||
import { getSmsList } from './commands/sms'; | ||
import { performEditorAction, typeUnicode } from './commands/typing'; | ||
|
||
/** | ||
* @typedef {Object} SettingsAppOpts | ||
* @property {import('appium-adb').ADB} adb | ||
*/ | ||
|
||
|
||
export class SettingsApp { | ||
/** @type {import('appium-adb').ADB} */ | ||
adb; | ||
|
||
/** @type {import('npmlog').Logger} */ | ||
log; | ||
|
||
/** | ||
* @param {SettingsAppOpts} opts | ||
*/ | ||
constructor (opts) { | ||
this.adb = opts.adb; | ||
this.log = log; | ||
} | ||
|
||
/** | ||
* @typedef {Object} SettingsAppStartupOptions | ||
* @property {number} [timeout=5000] The maximum number of milliseconds | ||
* to wait until the app has started | ||
* @property {boolean} [shouldRestoreCurrentApp=false] Whether to restore | ||
* the activity which was the current one before Settings startup | ||
*/ | ||
|
||
/** | ||
* Ensures that Appium Settings helper application is running | ||
* and starts it if necessary | ||
* | ||
* @param {SettingsAppStartupOptions} [opts={}] | ||
* @throws {Error} If Appium Settings has failed to start | ||
* @returns {Promise<SettingsApp>} self instance for chaining | ||
*/ | ||
async requireRunning (opts = {}) { | ||
if (await this.adb.processExists(SETTINGS_HELPER_ID)) { | ||
return this; | ||
} | ||
|
||
this.log.debug(LOG_PREFIX, 'Starting Appium Settings app'); | ||
const { | ||
timeout = 5000, | ||
shouldRestoreCurrentApp = false, | ||
} = opts; | ||
let appPackage; | ||
if (shouldRestoreCurrentApp) { | ||
try { | ||
({appPackage} = await this.adb.getFocusedPackageAndActivity()); | ||
} catch (e) { | ||
this.log.warn(LOG_PREFIX, `The current application can not be restored: ${e.message}`); | ||
} | ||
} | ||
await this.adb.startApp({ | ||
pkg: SETTINGS_HELPER_ID, | ||
activity: SETTINGS_HELPER_MAIN_ACTIVITY, | ||
action: 'android.intent.action.MAIN', | ||
category: 'android.intent.category.LAUNCHER', | ||
stopApp: false, | ||
waitForLaunch: false, | ||
}); | ||
try { | ||
await waitForCondition(async () => await this.adb.processExists(SETTINGS_HELPER_ID), { | ||
waitMs: timeout, | ||
intervalMs: 300, | ||
}); | ||
if (shouldRestoreCurrentApp && appPackage) { | ||
try { | ||
await this.adb.activateApp(appPackage); | ||
} catch (e) { | ||
log.warn(`The current application can not be restored: ${e.message}`); | ||
} | ||
} | ||
return this; | ||
} catch (err) { | ||
throw new Error(`Appium Settings app is not running after ${timeout}ms`); | ||
} | ||
} | ||
|
||
/** | ||
* Parses the output in JSON format retrieved from | ||
* the corresponding Appium Settings broadcast calls | ||
* | ||
* @param {string} output The actual command output | ||
* @param {string} entityName The name of the entity which is | ||
* going to be parsed | ||
* @returns {Object} The parsed JSON object | ||
* @throws {Error} If the output cannot be parsed | ||
* as a valid JSON | ||
*/ | ||
_parseJsonData (output, entityName) { | ||
if (!/\bresult=-1\b/.test(output) || !/\bdata="/.test(output)) { | ||
this.log.debug(LOG_PREFIX, output); | ||
throw new Error( | ||
`Cannot retrieve ${entityName} from the device. ` + | ||
'Check the server log for more details' | ||
); | ||
} | ||
const match = /\bdata="(.+)",?/.exec(output); | ||
if (!match) { | ||
this.log.debug(LOG_PREFIX, output); | ||
throw new Error( | ||
`Cannot parse ${entityName} from the command output. ` + | ||
'Check the server log for more details' | ||
); | ||
} | ||
const jsonStr = _.trim(match[1]); | ||
try { | ||
return JSON.parse(jsonStr); | ||
} catch (e) { | ||
log.debug(jsonStr); | ||
throw new Error( | ||
`Cannot parse ${entityName} from the resulting data string. ` + | ||
'Check the server log for more details' | ||
); | ||
} | ||
} | ||
|
||
setAnimationState = setAnimationState; | ||
|
||
getClipboard = getClipboard; | ||
|
||
setGeoLocation = setGeoLocation; | ||
getGeoLocation = getGeoLocation; | ||
refreshGeoLocationCache = refreshGeoLocationCache; | ||
|
||
setDeviceSysLocale = setDeviceSysLocale; | ||
|
||
scanMedia = scanMedia; | ||
|
||
setDataState = setDataState; | ||
setWifiState = setWifiState; | ||
|
||
getNotifications = getNotifications; | ||
getSmsList = getSmsList; | ||
|
||
performEditorAction = performEditorAction; | ||
typeUnicode = typeUnicode; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { ANIMATION_SETTING_ACTION, ANIMATION_SETTING_RECEIVER } from '../constants.js'; | ||
|
||
/** | ||
* Change the state of animation on the device under test. | ||
* Animation on the device is controlled by the following global properties: | ||
* [ANIMATOR_DURATION_SCALE]{@link https://developer.android.com/reference/android/provider/Settings.Global.html#ANIMATOR_DURATION_SCALE}, | ||
* [TRANSITION_ANIMATION_SCALE]{@link https://developer.android.com/reference/android/provider/Settings.Global.html#TRANSITION_ANIMATION_SCALE}, | ||
* [WINDOW_ANIMATION_SCALE]{@link https://developer.android.com/reference/android/provider/Settings.Global.html#WINDOW_ANIMATION_SCALE}. | ||
* This method sets all this properties to 0.0 to disable (1.0 to enable) animation. | ||
* | ||
* Turning off animation might be useful to improve stability | ||
* and reduce tests execution time. | ||
* | ||
* @this {import('../client').SettingsApp} | ||
* @param {boolean} on - True to enable and false to disable it. | ||
*/ | ||
export async function setAnimationState (on) { | ||
await this.adb.shell([ | ||
'am', 'broadcast', | ||
'-a', ANIMATION_SETTING_ACTION, | ||
'-n', ANIMATION_SETTING_RECEIVER, | ||
'--es', 'setstatus', on ? 'enable' : 'disable' | ||
]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import _ from 'lodash'; | ||
import { LOG_PREFIX } from '../logger'; | ||
import { | ||
CLIPBOARD_RECEIVER, | ||
CLIPBOARD_RETRIEVAL_ACTION, | ||
APPIUM_IME | ||
} from '../constants'; | ||
|
||
/** | ||
* Retrieves the text content of the device's clipboard. | ||
* The method works for Android below and above 29. | ||
* It temorarily enforces the IME setting in order to workaround | ||
* security limitations if needed. | ||
* This method only works if Appium Settings v. 2.15+ is installed | ||
* on the device under test | ||
* | ||
* @this {import('../client').SettingsApp} | ||
* @returns {Promise<string>} The actual content of the main clipboard as | ||
* base64-encoded string or an empty string if the clipboard is empty | ||
* @throws {Error} If there was a problem while getting the | ||
* clipboard contant | ||
*/ | ||
export async function getClipboard () { | ||
this.log.debug(LOG_PREFIX, 'Getting the clipboard content'); | ||
await this.requireRunning({shouldRestoreCurrentApp: true}); | ||
const retrieveClipboard = async () => await this.adb.shell([ | ||
'am', 'broadcast', | ||
'-n', CLIPBOARD_RECEIVER, | ||
'-a', CLIPBOARD_RETRIEVAL_ACTION, | ||
]); | ||
let output; | ||
try { | ||
output = (await this.adb.getApiLevel() >= 29) | ||
? (await this.adb.runInImeContext(APPIUM_IME, retrieveClipboard)) | ||
: (await retrieveClipboard()); | ||
} catch (err) { | ||
throw new Error(`Cannot retrieve the current clipboard content from the device. ` + | ||
`Make sure the Appium Settings application is up to date. ` + | ||
`Original error: ${err.message}`); | ||
} | ||
|
||
const match = /data="([^"]*)"/.exec(output); | ||
if (!match) { | ||
throw new Error(`Cannot parse the actual cliboard content from the command output: ${output}`); | ||
} | ||
return _.trim(match[1]); | ||
}; |
Oops, something went wrong.