From 4799e8f20b57f120965f8c16cd80dfd8b6d2493c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 13 Dec 2020 23:20:13 -0800 Subject: [PATCH] feat(adb): add screenshot (#4701) --- .github/workflows/tests.yml | 3 +-- android-types-internal.d.ts | 1 + package.json | 4 ++-- src/client/android.ts | 10 ++++++++++ src/dispatchers/androidDispatcher.ts | 4 ++++ src/protocol/channels.ts | 6 ++++++ src/protocol/protocol.yml | 4 ++++ src/protocol/validator.ts | 1 + src/server/android/android.ts | 4 ++++ test/android/browser.spec.ts | 4 ++-- test/android/device.spec.ts | 24 ++++++++++++++++++++++-- test/android/webview.spec.ts | 8 +++++--- utils/avd_recreate.sh | 2 +- 13 files changed, 63 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8022d2dd3..4367730a1a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -246,11 +246,10 @@ jobs: run: utils/avd_recreate.sh - name: Start Android Emulator run: utils/avd_start.sh - - run: npx folio test/android -p browserName=chromium --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json + - run: npx folio test/android -p browserName=chromium --workers=1 --forbid-only --timeout=60000 --global-timeout=5400000 --retries=3 --reporter=dot,json env: FOLIO_JSON_OUTPUT_NAME: "test-results/report.json" PW_ANDROID_TESTS: 1 - DEBUG: pw:api - run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json if: always() && github.ref == 'refs/heads/master' - uses: actions/upload-artifact@v1 diff --git a/android-types-internal.d.ts b/android-types-internal.d.ts index 7201b9eda4..675c7fee2c 100644 --- a/android-types-internal.d.ts +++ b/android-types-internal.d.ts @@ -47,6 +47,7 @@ export interface AndroidDevice exte swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise; info(selector: AndroidSelector): Promise; + screenshot(options?: { path?: string }): Promise; } export interface AndroidSocket extends EventEmitter { diff --git a/package.json b/package.json index f3ff2b26dd..83b266c265 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "ctest": "cross-env BROWSER=chromium folio test/", "ftest": "cross-env BROWSER=firefox folio test/", "wtest": "cross-env BROWSER=webkit folio test/", + "atest": "cross-env BROWSER=chromium PW_ANDROID_TESTS=1 npx folio test/android --workers=1 --reporter=list", "test": "folio test/", "eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts . || eslint --ext js,ts .", "tsc": "tsc -p .", @@ -31,8 +32,7 @@ "roll-browser": "node utils/roll_browser.js", "coverage": "node test/checkCoverage.js", "check-deps": "node utils/check_deps.js", - "build-android-driver": "./utils/build_android_driver.sh", - "test-android-driver": "PW_ANDROID_TESTS=1 npx folio test/android -p browserName=chromium --workers=1" + "build-android-driver": "./utils/build_android_driver.sh" }, "author": { "name": "Microsoft Corporation" diff --git a/src/client/android.ts b/src/client/android.ts index 3f68a75a4a..7dcb9501e6 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -191,6 +191,16 @@ export class AndroidDevice extends ChannelOwner { + return await this._wrapApiCall('androidDevice.screenshot', async () => { + const { binary } = await this._channel.screenshot(); + const buffer = Buffer.from(binary, 'base64'); + if (options.path) + await util.promisify(fs.writeFile)(options.path, buffer); + return buffer; + }); + } + async close() { return this._wrapApiCall('androidDevice.close', async () => { await this._channel.close(); diff --git a/src/dispatchers/androidDispatcher.ts b/src/dispatchers/androidDispatcher.ts index bbd826de6c..28172a188c 100644 --- a/src/dispatchers/androidDispatcher.ts +++ b/src/dispatchers/androidDispatcher.ts @@ -136,6 +136,10 @@ export class AndroidDeviceDispatcher extends Dispatcher { + return { binary: (await this._object.screenshot()).toString('base64') }; + } + async shell(params: channels.AndroidDeviceShellParams): Promise { return { result: (await this._object.shell(params.command)).toString('base64') }; } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 49302dd627..c10fdb9f89 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -2462,6 +2462,7 @@ export interface AndroidDeviceChannel extends Channel { swipe(params: AndroidDeviceSwipeParams, metadata?: Metadata): Promise; info(params: AndroidDeviceInfoParams, metadata?: Metadata): Promise; tree(params?: AndroidDeviceTreeParams, metadata?: Metadata): Promise; + screenshot(params?: AndroidDeviceScreenshotParams, metadata?: Metadata): Promise; inputType(params: AndroidDeviceInputTypeParams, metadata?: Metadata): Promise; inputPress(params: AndroidDeviceInputPressParams, metadata?: Metadata): Promise; inputTap(params: AndroidDeviceInputTapParams, metadata?: Metadata): Promise; @@ -2601,6 +2602,11 @@ export type AndroidDeviceTreeOptions = {}; export type AndroidDeviceTreeResult = { tree: AndroidElementInfo, }; +export type AndroidDeviceScreenshotParams = {}; +export type AndroidDeviceScreenshotOptions = {}; +export type AndroidDeviceScreenshotResult = { + binary: Binary, +}; export type AndroidDeviceInputTypeParams = { text: string, }; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 37a8a1eae6..c2ab226cf6 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -2204,6 +2204,10 @@ AndroidDevice: returns: tree: AndroidElementInfo + screenshot: + returns: + binary: binary + inputType: parameters: text: string diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 696956ea73..78cf140633 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -966,6 +966,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { selector: tType('AndroidSelector'), }); scheme.AndroidDeviceTreeParams = tOptional(tObject({})); + scheme.AndroidDeviceScreenshotParams = tOptional(tObject({})); scheme.AndroidDeviceInputTypeParams = tObject({ text: tString, }); diff --git a/src/server/android/android.ts b/src/server/android/android.ts index dcedd34b38..ba9c7f1430 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -148,6 +148,10 @@ export class AndroidDevice extends EventEmitter { return await this._backend.open(`${command}`); } + async screenshot(): Promise { + return await this._backend.runCommand(`shell:screencap -p`); + } + private async _driver(): Promise { if (this._driverPromise) return this._driverPromise; diff --git a/test/android/browser.spec.ts b/test/android/browser.spec.ts index 3079d0533c..33cb8f035b 100644 --- a/test/android/browser.spec.ts +++ b/test/android/browser.spec.ts @@ -18,11 +18,11 @@ import { folio } from './android.fixtures'; const { it, expect } = folio; if (process.env.PW_ANDROID_TESTS) { - it('should discover device', async function({ device }) { + it('androidDevice.model', async function({ device }) { expect(device.model()).toBe('sdk_gphone_x86_arm'); }); - it('should launch browser', async function({ device }) { + it('androidDevice.launchBrowser', async function({ device }) { const context = await device.launchBrowser(); const [page] = context.pages(); await page.goto('data:text/html,Hello world!'); diff --git a/test/android/device.spec.ts b/test/android/device.spec.ts index 9e72f5628e..1536e07da8 100644 --- a/test/android/device.spec.ts +++ b/test/android/device.spec.ts @@ -14,19 +14,39 @@ * limitations under the License. */ +import * as fs from 'fs'; +import { PNG } from 'pngjs'; + import { folio } from './android.fixtures'; const { it, expect } = folio; if (process.env.PW_ANDROID_TESTS) { - it('should run ADB shell commands', async function({ device }) { + it('androidDevice.shell', async function({ device }) { const output = await device.shell('echo 123'); expect(output.toString()).toBe('123\n'); }); - it('should open a ADB socket', async function({ device }) { + it('androidDevice.open', async function({ device }) { const socket = await device.open('shell:/bin/cat'); await socket.write(Buffer.from('321\n')); const output = await new Promise(resolve => socket.on('data', resolve)); expect(output.toString()).toBe('321\n'); }); + + it('androidDevice.screenshot', async function({ device, testInfo }) { + const path = testInfo.outputPath('screenshot.png'); + const result = await device.screenshot({ path }); + const buffer = fs.readFileSync(path); + expect(result.length).toBe(buffer.length); + const { width, height} = PNG.sync.read(result); + expect(width).toBe(1080); + expect(height).toBe(1920); + }); + + it('androidDevice.push', async function({ device, testInfo }) { + await device.shell('rm /data/local/tmp/hello-world'); + await device.push(Buffer.from('hello world'), '/data/local/tmp/hello-world'); + const data = await device.shell('cat /data/local/tmp/hello-world'); + expect(data).toEqual(Buffer.from('hello world')); + }); } diff --git a/test/android/webview.spec.ts b/test/android/webview.spec.ts index 2d909d94fc..184bc9ef20 100644 --- a/test/android/webview.spec.ts +++ b/test/android/webview.spec.ts @@ -18,7 +18,7 @@ import { folio } from './android.fixtures'; const { it, expect } = folio; if (process.env.PW_ANDROID_TESTS) { - it('should discover webviews', async function({ device }) { + it('androidDevice.webView', async function({ device }) { expect(device.webViews().length).toBe(0); await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); @@ -26,7 +26,7 @@ if (process.env.PW_ANDROID_TESTS) { expect(device.webViews().length).toBe(1); }); - it('should connect to page', async function({ device }) { + it('webView.page', async function({ device }) { expect(device.webViews().length).toBe(0); await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); @@ -43,7 +43,9 @@ if (process.env.PW_ANDROID_TESTS) { expect(await page.title()).toBe('Hello world!'); }); - it('should navigate page externally', async function({ device, server }) { + it('should navigate page externally', test => { + test.fixme(!!process.env.CI, 'Hangs on the bots'); + }, async function({ device, server }) { expect(device.webViews().length).toBe(0); await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); diff --git a/utils/avd_recreate.sh b/utils/avd_recreate.sh index 2538ad25ea..bdd99855b2 100755 --- a/utils/avd_recreate.sh +++ b/utils/avd_recreate.sh @@ -10,5 +10,5 @@ fi ${ANDROID_HOME}/tools/bin/avdmanager delete avd --name android30 || true echo "y" | ${ANDROID_HOME}/tools/bin/sdkmanager --install "system-images;android-30;google_apis;x86" -echo "no" | ${ANDROID_HOME}/tools/bin/avdmanager create avd --force --name android30 --device pixel_4 --package "system-images;android-30;google_apis;x86" +echo "no" | ${ANDROID_HOME}/tools/bin/avdmanager create avd --force --name android30 --device "Nexus 5X" --package "system-images;android-30;google_apis;x86" ${ANDROID_HOME}/emulator/emulator -list-avds