appium-flutter-driver
Version:
Appium Flutter driver
195 lines (178 loc) • 6.91 kB
text/typescript
import { URL } from 'url';
import _ from 'lodash';
import type { FlutterDriver } from '../driver';
import { IsolateSocket } from './isolate_socket';
import { decode } from './base64url';
import type { LogEntry } from './log-monitor';
import { retryInterval } from 'asyncbox';
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=/'
export 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:/=_\\-.\\[\\]]+)`,
);
const moduleCheckIntervalCount = 30;
const moduleCheckIntervalMs = 500;
// SOCKETS
export async function connectSocket(
this: FlutterDriver,
dartObservatoryURL: string,
caps: Record<string, any>
): Promise<IsolateSocket> {
const isolateId = caps.isolateId;
this.log.debug(`Establishing a connection to the Dart Observatory`);
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) => {
this.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`, () => {
this.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 = () => {
this.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) {
this.log.errorWithException(new Error(JSON.stringify(e)));
}
};
this.log.info(`Connecting to Dart Observatory: ${dartObservatoryURL}`);
if (isolateId) {
this.log.info(`Listing the given isolate id: ${isolateId}`);
socket.isolateId = isolateId;
} else {
const vm = await socket.call(`getVM`) as {
isolates: [{
name: string,
id: number,
}],
};
this.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) {
this.log.error(`Cannot get Dart main isolate info`);
removeListenerAndResolve(null);
socket.close();
return;
}
// e.g. 'isolates/2978358234363215', '2978358234363215'
socket.isolateId = mainIsolateData.id;
}
// It could take time to load the expected module.
try {
await retryInterval(
moduleCheckIntervalCount,
moduleCheckIntervalMs,
async () => {
const isolate = await socket.call(`getIsolate`, {
isolateId: `${socket.isolateId}`,
}) as {
extensionRPCs: [string] | null,
} | null;
if (!isolate) {
throw new Error(`Cannot get main Dart Isolate`);
}
if (!Array.isArray(isolate.extensionRPCs)) {
throw new Error(`Cannot get Dart extensionRPCs from isolate ${JSON.stringify(isolate)}`);
}
if (isolate.extensionRPCs.indexOf(`ext.flutter.driver`) < 0) {
throw new Error(`"ext.flutter.driver" is not found in "extensionRPCs" ${JSON.stringify(isolate.extensionRPCs)}`);
}
}
);
} catch (e) {
this.log.error(e.message);
removeListenerAndResolve(null);
return;
}
removeListenerAndResolve(socket);
};
socket.on(`open`, onOpenListener);
});
const connectedSocket = await connectedPromise;
if (connectedSocket) {
return connectedSocket;
}
throw new Error(
`Cannot connect to the Dart Observatory URL ${dartObservatoryURL}. ` +
`Check the server log for more details`
);
}
export async function executeGetIsolateCommand(
this: FlutterDriver,
isolateId: string|number
) {
this.log.debug(`>>> getIsolate`);
const isolate = await (this.socket as IsolateSocket).call(`getIsolate`, { isolateId: `${isolateId}` });
this.log.debug(`<<< ${_.truncate(JSON.stringify(isolate), {'length': truncateLength})}`);
return isolate;
}
export async function executeGetVMCommand(this: FlutterDriver) {
this.log.debug(`>>> getVM`);
const vm = await (this.socket as IsolateSocket).call(`getVM`) as {
isolates: [{
name: string,
id: number,
}],
};
this.log.debug(`<<< ${_.truncate(JSON.stringify(vm), {'length': truncateLength})}`);
return vm;
}
export async function executeElementCommand(
this: FlutterDriver,
command: string,
elementBase64?: string,
extraArgs = {}
) {
const elementObject = elementBase64 ? JSON.parse(decode(elementBase64)) : {};
const serializedCommand = { command, ...elementObject, ...extraArgs };
this.log.debug(`>>> ${JSON.stringify(serializedCommand)}`);
const data = await (this.socket as IsolateSocket).executeSocketCommand(serializedCommand);
this.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 function extractObservatoryUrl(logEntry: LogEntry): URL | null {
const match = logEntry.message.match(OBSERVATORY_URL_PATTERN);
if (!match) {
return null;
}
try {
const result = new URL(match[2]);
result.protocol = `ws`;
result.pathname += `ws`;
return result;
} catch {
return null;
}
}