feat(adb): add screenshot (#4701)
This commit is contained in:
		
							parent
							
								
									1596b53da2
								
							
						
					
					
						commit
						4799e8f20b
					
				|  | @ -246,11 +246,10 @@ jobs: | ||||||
|       run: utils/avd_recreate.sh |       run: utils/avd_recreate.sh | ||||||
|     - name: Start Android Emulator |     - name: Start Android Emulator | ||||||
|       run: utils/avd_start.sh |       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: |       env: | ||||||
|         FOLIO_JSON_OUTPUT_NAME: "test-results/report.json" |         FOLIO_JSON_OUTPUT_NAME: "test-results/report.json" | ||||||
|         PW_ANDROID_TESTS: 1 |         PW_ANDROID_TESTS: 1 | ||||||
|         DEBUG: pw:api |  | ||||||
|     - run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json |     - run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json | ||||||
|       if: always() && github.ref == 'refs/heads/master' |       if: always() && github.ref == 'refs/heads/master' | ||||||
|     - uses: actions/upload-artifact@v1 |     - uses: actions/upload-artifact@v1 | ||||||
|  |  | ||||||
|  | @ -47,6 +47,7 @@ export interface AndroidDevice<BrowserContextOptions, BrowserContext, Page> exte | ||||||
|   swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>; |   swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>; | ||||||
| 
 | 
 | ||||||
|   info(selector: AndroidSelector): Promise<AndroidElementInfo>; |   info(selector: AndroidSelector): Promise<AndroidElementInfo>; | ||||||
|  |   screenshot(options?: { path?: string }): Promise<Buffer>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface AndroidSocket extends EventEmitter { | export interface AndroidSocket extends EventEmitter { | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ | ||||||
|     "ctest": "cross-env BROWSER=chromium folio test/", |     "ctest": "cross-env BROWSER=chromium folio test/", | ||||||
|     "ftest": "cross-env BROWSER=firefox folio test/", |     "ftest": "cross-env BROWSER=firefox folio test/", | ||||||
|     "wtest": "cross-env BROWSER=webkit 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/", |     "test": "folio test/", | ||||||
|     "eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts . || eslint --ext js,ts .", |     "eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts . || eslint --ext js,ts .", | ||||||
|     "tsc": "tsc -p .", |     "tsc": "tsc -p .", | ||||||
|  | @ -31,8 +32,7 @@ | ||||||
|     "roll-browser": "node utils/roll_browser.js", |     "roll-browser": "node utils/roll_browser.js", | ||||||
|     "coverage": "node test/checkCoverage.js", |     "coverage": "node test/checkCoverage.js", | ||||||
|     "check-deps": "node utils/check_deps.js", |     "check-deps": "node utils/check_deps.js", | ||||||
|     "build-android-driver": "./utils/build_android_driver.sh", |     "build-android-driver": "./utils/build_android_driver.sh" | ||||||
|     "test-android-driver": "PW_ANDROID_TESTS=1 npx folio test/android  -p browserName=chromium --workers=1" |  | ||||||
|   }, |   }, | ||||||
|   "author": { |   "author": { | ||||||
|     "name": "Microsoft Corporation" |     "name": "Microsoft Corporation" | ||||||
|  |  | ||||||
|  | @ -191,6 +191,16 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async screenshot(options: { path?: string } = {}): Promise<Buffer> { | ||||||
|  |     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() { |   async close() { | ||||||
|     return this._wrapApiCall('androidDevice.close', async () => { |     return this._wrapApiCall('androidDevice.close', async () => { | ||||||
|       await this._channel.close(); |       await this._channel.close(); | ||||||
|  |  | ||||||
|  | @ -136,6 +136,10 @@ export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels. | ||||||
|     await this._object.send('inputDrag', params); |     await this._object.send('inputDrag', params); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async screenshot(params: channels.AndroidDeviceScreenshotParams): Promise<channels.AndroidDeviceScreenshotResult> { | ||||||
|  |     return { binary: (await this._object.screenshot()).toString('base64') }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async shell(params: channels.AndroidDeviceShellParams): Promise<channels.AndroidDeviceShellResult> { |   async shell(params: channels.AndroidDeviceShellParams): Promise<channels.AndroidDeviceShellResult> { | ||||||
|     return { result: (await this._object.shell(params.command)).toString('base64') }; |     return { result: (await this._object.shell(params.command)).toString('base64') }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -2462,6 +2462,7 @@ export interface AndroidDeviceChannel extends Channel { | ||||||
|   swipe(params: AndroidDeviceSwipeParams, metadata?: Metadata): Promise<AndroidDeviceSwipeResult>; |   swipe(params: AndroidDeviceSwipeParams, metadata?: Metadata): Promise<AndroidDeviceSwipeResult>; | ||||||
|   info(params: AndroidDeviceInfoParams, metadata?: Metadata): Promise<AndroidDeviceInfoResult>; |   info(params: AndroidDeviceInfoParams, metadata?: Metadata): Promise<AndroidDeviceInfoResult>; | ||||||
|   tree(params?: AndroidDeviceTreeParams, metadata?: Metadata): Promise<AndroidDeviceTreeResult>; |   tree(params?: AndroidDeviceTreeParams, metadata?: Metadata): Promise<AndroidDeviceTreeResult>; | ||||||
|  |   screenshot(params?: AndroidDeviceScreenshotParams, metadata?: Metadata): Promise<AndroidDeviceScreenshotResult>; | ||||||
|   inputType(params: AndroidDeviceInputTypeParams, metadata?: Metadata): Promise<AndroidDeviceInputTypeResult>; |   inputType(params: AndroidDeviceInputTypeParams, metadata?: Metadata): Promise<AndroidDeviceInputTypeResult>; | ||||||
|   inputPress(params: AndroidDeviceInputPressParams, metadata?: Metadata): Promise<AndroidDeviceInputPressResult>; |   inputPress(params: AndroidDeviceInputPressParams, metadata?: Metadata): Promise<AndroidDeviceInputPressResult>; | ||||||
|   inputTap(params: AndroidDeviceInputTapParams, metadata?: Metadata): Promise<AndroidDeviceInputTapResult>; |   inputTap(params: AndroidDeviceInputTapParams, metadata?: Metadata): Promise<AndroidDeviceInputTapResult>; | ||||||
|  | @ -2601,6 +2602,11 @@ export type AndroidDeviceTreeOptions = {}; | ||||||
| export type AndroidDeviceTreeResult = { | export type AndroidDeviceTreeResult = { | ||||||
|   tree: AndroidElementInfo, |   tree: AndroidElementInfo, | ||||||
| }; | }; | ||||||
|  | export type AndroidDeviceScreenshotParams = {}; | ||||||
|  | export type AndroidDeviceScreenshotOptions = {}; | ||||||
|  | export type AndroidDeviceScreenshotResult = { | ||||||
|  |   binary: Binary, | ||||||
|  | }; | ||||||
| export type AndroidDeviceInputTypeParams = { | export type AndroidDeviceInputTypeParams = { | ||||||
|   text: string, |   text: string, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -2204,6 +2204,10 @@ AndroidDevice: | ||||||
|       returns: |       returns: | ||||||
|         tree: AndroidElementInfo |         tree: AndroidElementInfo | ||||||
| 
 | 
 | ||||||
|  |     screenshot: | ||||||
|  |       returns: | ||||||
|  |         binary: binary | ||||||
|  | 
 | ||||||
|     inputType: |     inputType: | ||||||
|       parameters: |       parameters: | ||||||
|         text: string |         text: string | ||||||
|  |  | ||||||
|  | @ -966,6 +966,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { | ||||||
|     selector: tType('AndroidSelector'), |     selector: tType('AndroidSelector'), | ||||||
|   }); |   }); | ||||||
|   scheme.AndroidDeviceTreeParams = tOptional(tObject({})); |   scheme.AndroidDeviceTreeParams = tOptional(tObject({})); | ||||||
|  |   scheme.AndroidDeviceScreenshotParams = tOptional(tObject({})); | ||||||
|   scheme.AndroidDeviceInputTypeParams = tObject({ |   scheme.AndroidDeviceInputTypeParams = tObject({ | ||||||
|     text: tString, |     text: tString, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | @ -148,6 +148,10 @@ export class AndroidDevice extends EventEmitter { | ||||||
|     return await this._backend.open(`${command}`); |     return await this._backend.open(`${command}`); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async screenshot(): Promise<Buffer> { | ||||||
|  |     return await this._backend.runCommand(`shell:screencap -p`); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private async _driver(): Promise<Transport> { |   private async _driver(): Promise<Transport> { | ||||||
|     if (this._driverPromise) |     if (this._driverPromise) | ||||||
|       return this._driverPromise; |       return this._driverPromise; | ||||||
|  |  | ||||||
|  | @ -18,11 +18,11 @@ import { folio } from './android.fixtures'; | ||||||
| const { it, expect } = folio; | const { it, expect } = folio; | ||||||
| 
 | 
 | ||||||
| if (process.env.PW_ANDROID_TESTS) { | 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'); |     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 context = await device.launchBrowser(); | ||||||
|     const [page] = context.pages(); |     const [page] = context.pages(); | ||||||
|     await page.goto('data:text/html,<title>Hello world!</title>'); |     await page.goto('data:text/html,<title>Hello world!</title>'); | ||||||
|  |  | ||||||
|  | @ -14,19 +14,39 @@ | ||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | import * as fs from 'fs'; | ||||||
|  | import { PNG } from 'pngjs'; | ||||||
|  | 
 | ||||||
| import { folio } from './android.fixtures'; | import { folio } from './android.fixtures'; | ||||||
| const { it, expect } = folio; | const { it, expect } = folio; | ||||||
| 
 | 
 | ||||||
| if (process.env.PW_ANDROID_TESTS) { | 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'); |     const output = await device.shell('echo 123'); | ||||||
|     expect(output.toString()).toBe('123\n'); |     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'); |     const socket = await device.open('shell:/bin/cat'); | ||||||
|     await socket.write(Buffer.from('321\n')); |     await socket.write(Buffer.from('321\n')); | ||||||
|     const output = await new Promise(resolve => socket.on('data', resolve)); |     const output = await new Promise(resolve => socket.on('data', resolve)); | ||||||
|     expect(output.toString()).toBe('321\n'); |     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')); | ||||||
|  |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ import { folio } from './android.fixtures'; | ||||||
| const { it, expect } = folio; | const { it, expect } = folio; | ||||||
| 
 | 
 | ||||||
| if (process.env.PW_ANDROID_TESTS) { | 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); |     expect(device.webViews().length).toBe(0); | ||||||
|     await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); |     await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); | ||||||
|     const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); |     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); |     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); |     expect(device.webViews().length).toBe(0); | ||||||
|     await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); |     await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); | ||||||
|     const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); |     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!'); |     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); |     expect(device.webViews().length).toBe(0); | ||||||
|     await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); |     await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity'); | ||||||
|     const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); |     const webview = await device.webView({ pkg: 'org.chromium.webview_shell' }); | ||||||
|  |  | ||||||
|  | @ -10,5 +10,5 @@ fi | ||||||
| 
 | 
 | ||||||
| ${ANDROID_HOME}/tools/bin/avdmanager delete avd --name android30 || true | ${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 "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 | ${ANDROID_HOME}/emulator/emulator -list-avds | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue