appium-flutter-driver
Version:
239 lines (221 loc) • 9.24 kB
text/typescript
import { URL } from 'url';
import _ from 'lodash';
import type { FlutterDriver } from '../driver';
import { log } from '../logger';
import { IsolateSocket } from './isolate_socket';
import { decode } from './base64url';
import B from 'bluebird';
import type XCUITestDriver from 'appium-xcuitest-driver';
import type AndroidUiautomator2Driver from 'appium-uiautomator2-driver';
import { PLATFORM } from '../platform';
const truncateLength = 500;
// https://github.com/flutter/flutter/blob/f90b019c68edf4541a4c8273865a2b40c2c01eb3/dev/devicelab/lib/framework/runner.dart#L183
// e.g. 'Observatory listening on http://127.0.0.1:52817/_w_SwaKs9-g=/'
// https://github.com/flutter/flutter/blob/52ae102f182afaa0524d0d01d21b2d86d15a11dc/packages/flutter_tools/lib/src/resident_runner.dart#L1386-L1389
// e.g. 'An Observatory debugger and profiler on ${device.device.name} is available at: http://127.0.0.1:52817/_w_SwaKs9-g=/'
const OBSERVATORY_URL_PATTERN = new RegExp(
`(Observatory listening on |` +
`An Observatory debugger and profiler on\\s.+\\sis available at: |` +
`The Dart VM service is listening on )` +
`((http|//)[a-zA-Z0-9:/=_\\-.\\[\\]]+)`,
);
type AnyDriver = XCUITestDriver | AndroidUiautomator2Driver;
// SOCKETS
export const connectSocket = async (
getObservatoryWsUri: (flutterDriver: FlutterDriver, driver: AnyDriver, caps: any) => Promise<string>,
flutterDriver: FlutterDriver,
driver: AnyDriver,
caps: Record<string, any>
): Promise<IsolateSocket> => {
const retryBackoff = caps.retryBackoffTime || 3000;
const maxRetryCount = caps.maxRetryCount || 10;
const isolateId = caps.isolateId;
let retryCount = 0;
let urlFetchError: Error|undefined;
let dartObservatoryURL: string|undefined;
log.debug(
`Establishing a connection to the Dart Observatory. ` +
`Will retry ${maxRetryCount} times with ${retryBackoff}ms delay between retries. ` +
`These values could be customized by changing 'maxRetryCount' and 'retryBackoffTime' capabilities.`
);
while (retryCount < maxRetryCount) {
if (retryCount > 0 && retryBackoff > 0) {
log.info(`Waiting ${retryBackoff}ms before retrying`);
await B.delay(retryBackoff);
}
log.info(`Attempt #${(retryCount + 1)} of ${maxRetryCount}`);
// Every attempt gets the latest observatory url
try {
dartObservatoryURL = await getObservatoryWsUri(flutterDriver, driver, caps);
urlFetchError = undefined;
} catch (e) {
urlFetchError = e;
log.debug(`Got an error while finding an observatory url. Original error: ${e.message}`);
}
if (!urlFetchError) {
const connectedPromise = new Promise<IsolateSocket | null>((resolve) => {
const socket = new IsolateSocket(dartObservatoryURL);
const removeListenerAndResolve = (r: IsolateSocket | null) => {
socket.removeListener(`error`, onErrorListener);
socket.removeListener(`timeout`, onTimeoutListener);
socket.removeListener(`open`, onOpenListener);
resolve(r);
};
// Add an 'error' event handler for the client socket
const onErrorListener = (ex: Error) => {
log.error(`Connection to ${dartObservatoryURL} got an error: ${ex.message}`);
removeListenerAndResolve(null);
};
socket.on(`error`, onErrorListener);
// Add a 'close' event handler for the client socket
socket.on(`close`, () => {
log.info(`Connection to ${dartObservatoryURL} closed`);
// @todo do we need to set this.socket = null?
});
// Add a 'timeout' event handler for the client socket
const onTimeoutListener = () => {
log.error(`Connection to ${dartObservatoryURL} timed out`);
removeListenerAndResolve(null);
};
socket.on(`timeout`, onTimeoutListener);
const onOpenListener = async () => {
const originalSocketCall = socket.call;
socket.call = async (...args: any) => {
try {
// `await` is needed so that rejected promise will be thrown and caught
return await originalSocketCall.apply(socket, args);
} catch (e) {
log.errorAndThrow(JSON.stringify(e));
}
};
log.info(`Connecting to Dart Observatory: ${dartObservatoryURL}`);
if (isolateId) {
log.info(`Listing the given isolate id: ${isolateId}`);
socket.isolateId = isolateId;
} else {
const vm = await socket.call(`getVM`) as {
isolates: [{
name: string,
id: number,
}],
};
log.info(`Listing all isolates: ${JSON.stringify(vm.isolates)}`);
// To accept 'main.dart:main()' and 'main'
const mainIsolateData = vm.isolates.find((e) => e.name.includes(`main`));
if (!mainIsolateData) {
log.error(`Cannot get Dart main isolate info`);
removeListenerAndResolve(null);
socket.close();
return;
}
// e.g. 'isolates/2978358234363215', '2978358234363215'
socket.isolateId = mainIsolateData.id;
}
// @todo check extension and do health check
const isolate = await socket.call(`getIsolate`, {
isolateId: `${socket.isolateId}`,
}) as {
extensionRPCs: [string] | null,
} | null;
if (!isolate) {
log.error(`Cannot get main Dart Isolate`);
removeListenerAndResolve(null);
return;
}
if (!Array.isArray(isolate.extensionRPCs)) {
log.error(`Cannot get Dart extensionRPCs from isolate ${JSON.stringify(isolate)}`);
removeListenerAndResolve(null);
return;
}
if (isolate.extensionRPCs.indexOf(`ext.flutter.driver`) < 0) {
log.error(
`"ext.flutter.driver" is not found in "extensionRPCs" ${JSON.stringify(isolate.extensionRPCs)}`
);
removeListenerAndResolve(null);
return;
}
removeListenerAndResolve(socket);
};
socket.on(`open`, onOpenListener);
});
const connectedSocket = await connectedPromise;
if (connectedSocket) {
return connectedSocket;
}
}
// re-create the port forward
switch (_.toLower(caps.platformName)) {
case PLATFORM.IOS:
flutterDriver.localServer?.close();
break;
case PLATFORM.ANDROID:
if (flutterDriver.portForwardLocalPort) {
await driver.adb.removePortForward(flutterDriver.portForwardLocalPort);
}
break;
}
retryCount++;
}
throw new Error(
urlFetchError
? (`Cannot determine the Dart Observatory URL after ${maxRetryCount} retries. ` +
`Original error: ${urlFetchError.message}`)
: (`Cannot connect to the Dart Observatory URL ${dartObservatoryURL} after ` +
`${maxRetryCount} retries. Check the server log for more details`)
);
};
export const executeGetIsolateCommand = async function(
this: FlutterDriver,
isolateId: string|number
) {
log.debug(`>>> getIsolate`);
const isolate = await this.socket!.call(`getIsolate`, { isolateId: `${isolateId}` });
log.debug(`<<< ${_.truncate(JSON.stringify(isolate), {'length': truncateLength})}`);
return isolate;
};
export const executeGetVMCommand = async function(this: FlutterDriver) {
log.debug(`>>> getVM`);
const vm = await this.socket!.call(`getVM`) as {
isolates: [{
name: string,
id: number,
}],
};
log.debug(`<<< ${_.truncate(JSON.stringify(vm), {'length': truncateLength})}`);
return vm;
};
export const executeElementCommand = async function(
this: FlutterDriver,
command: string,
elementBase64?: string,
extraArgs = {}) {
const elementObject = elementBase64 ? JSON.parse(decode(elementBase64)) : {};
const serializedCommand = { command, ...elementObject, ...extraArgs };
log.debug(`>>> ${JSON.stringify(serializedCommand)}`);
const data = await this.socket!.executeSocketCommand(serializedCommand);
log.debug(`<<< ${JSON.stringify(data)} | previous command ${command}`);
if (data.isError) {
throw new Error(
`Cannot execute command ${command}, server response ${JSON.stringify(data, null, 2)}`,
);
}
return data.response;
};
export const fetchObservatoryUrl = (deviceLogs: [{ message: string }]): URL => {
let dartObservatoryURL: URL|undefined;
for (const line of deviceLogs.map((e) => e.message).reverse()) {
const match = line.match(OBSERVATORY_URL_PATTERN);
if (match) {
dartObservatoryURL = new URL(match[2]);
break;
}
}
if (!dartObservatoryURL) {
throw new Error(`No observatory URL matching to '${OBSERVATORY_URL_PATTERN}' was found in the device log. ` +
`Please make sure the application under test is configured properly according to ` +
`https://github.com/appium-userland/appium-flutter-driver#usage and that it does not crash on startup.`);
}
dartObservatoryURL.protocol = `ws`;
dartObservatoryURL.pathname += `ws`;
return dartObservatoryURL;
};