mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(adb): add screenshot (#4701)
This commit is contained in:
parent
1596b53da2
commit
4799e8f20b
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -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
|
||||||
|
1
android-types-internal.d.ts
vendored
1
android-types-internal.d.ts
vendored
@ -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…
x
Reference in New Issue
Block a user