Skip to content

Commit

Permalink
feat: Refactor mixins and improve their typing (#392)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jul 25, 2024
1 parent a0d44b9 commit d1fda80
Show file tree
Hide file tree
Showing 14 changed files with 603 additions and 573 deletions.
166 changes: 61 additions & 105 deletions lib/mixins/connect.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import log from '../logger';
import {
appInfoFromDict, pageArrayFromDict, getDebuggerAppKey,
getPossibleDebuggerAppKeys, simpleStringify, deferredPromise
pageArrayFromDict,
getPossibleDebuggerAppKeys,
simpleStringify,
} from '../utils';
import events from './events';
import { timing } from '@appium/support';
Expand Down Expand Up @@ -32,62 +32,60 @@ const BLANK_PAGE_URL = 'about:blank';
/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @returns {Promise<void>}
*/
async function setConnectionKey () {
log.debug('Sending connection key request');
if (!this.rpcClient) {
throw new Error('rpcClient is undefined. Is the debugger connected?');
}
export async function setConnectionKey () {
this.log.debug('Sending connection key request');

// send but only wait to make sure the socket worked
// as response from Web Inspector can take a long time
await this.rpcClient.send('setConnectionKey', {}, false);
await this.requireRpcClient().send('setConnectionKey', {}, false);
}

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {number} [timeout=APP_CONNECT_TIMEOUT_MS]
* @returns {Promise<import('@appium/types').StringRecord>}
*/
async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
export async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
this.setup();

// initialize the rpc client
this.initRpcClient();

if (!this.rpcClient) {
throw new Error('rpcClient is undefined. Is the debugger connected?');
}
const rpcClient = this.requireRpcClient();

// listen for basic debugger-level events
this.rpcClient.on('_rpc_reportSetup:', _.noop);
this.rpcClient.on('_rpc_forwardGetListing:', this.onPageChange.bind(this));
this.rpcClient.on('_rpc_reportConnectedApplicationList:', this.onConnectedApplicationList.bind(this));
this.rpcClient.on('_rpc_applicationConnected:', this.onAppConnect.bind(this));
this.rpcClient.on('_rpc_applicationDisconnected:', this.onAppDisconnect.bind(this));
this.rpcClient.on('_rpc_applicationUpdated:', this.onAppUpdate.bind(this));
this.rpcClient.on('_rpc_reportConnectedDriverList:', this.onConnectedDriverList.bind(this));
this.rpcClient.on('_rpc_reportCurrentState:', this.onCurrentState.bind(this));
this.rpcClient.on('Page.frameDetached', this.frameDetached.bind(this));

await this.rpcClient.connect();
rpcClient.on('_rpc_reportSetup:', _.noop);
rpcClient.on('_rpc_forwardGetListing:', this.onPageChange.bind(this));
rpcClient.on('_rpc_reportConnectedApplicationList:', this.onConnectedApplicationList.bind(this));
rpcClient.on('_rpc_applicationConnected:', this.onAppConnect.bind(this));
rpcClient.on('_rpc_applicationDisconnected:', this.onAppDisconnect.bind(this));
rpcClient.on('_rpc_applicationUpdated:', this.onAppUpdate.bind(this));
rpcClient.on('_rpc_reportConnectedDriverList:', this.onConnectedDriverList.bind(this));
rpcClient.on('_rpc_reportCurrentState:', this.onCurrentState.bind(this));
rpcClient.on('Page.frameDetached', this.frameDetached.bind(this));

await rpcClient.connect();

// get the connection information about the app
try {
this.setConnectionKey();
if (timeout) {
log.debug(`Waiting up to ${timeout}ms for applications to be reported`);
this.log.debug(`Waiting up to ${timeout}ms for applications to be reported`);
try {
await waitForCondition(() => !_.isEmpty(this.appDict), {
waitMs: timeout,
intervalMs: APP_CONNECT_INTERVAL_MS,
});
} catch (err) {
log.debug(`Timed out waiting for applications to be reported`);
this.log.debug(`Timed out waiting for applications to be reported`);
}
}
return this.appDict || {};
} catch (err) {
log.error(`Error setting connection key: ${err.message}`);
this.log.error(`Error setting connection key: ${err.message}`);
await this.disconnect();
throw err;
}
Expand All @@ -98,7 +96,7 @@ async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
* @this {import('../remote-debugger').RemoteDebugger}
* @returns {Promise<void>}
*/
async function disconnect () {
export async function disconnect () {
if (this.rpcClient) {
await this.rpcClient.disconnect();
}
Expand All @@ -123,40 +121,38 @@ async function disconnect () {
* @param {boolean} [ignoreAboutBlankUrl]
* @returns {Promise<Page[]>}
*/
async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, ignoreAboutBlankUrl = false) {
log.debug('Selecting application');
if (!this.rpcClient) {
throw new Error('rpcClient is undefined. Is the debugger connected?');
}
export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, ignoreAboutBlankUrl = false) {
this.log.debug('Selecting application');
const rpcClient = this.requireRpcClient();

const shouldCheckForTarget = this.rpcClient.shouldCheckForTarget;
this.rpcClient.shouldCheckForTarget = false;
const shouldCheckForTarget = rpcClient.shouldCheckForTarget;
rpcClient.shouldCheckForTarget = false;
try {
const timer = new timing.Timer().start();
if (!this.appDict || _.isEmpty(this.appDict)) {
log.debug('No applications currently connected.');
this.log.debug('No applications currently connected.');
return [];
}

const {appIdKey, pageDict} = await this.searchForApp(currentUrl, maxTries, ignoreAboutBlankUrl);
const {appIdKey, pageDict} = await this.searchForApp(currentUrl, maxTries, ignoreAboutBlankUrl) ?? {};

// if, after all this, we have no dictionary, we have failed
if (!appIdKey || !pageDict) {
log.errorAndThrow(`Could not connect to a valid app after ${maxTries} tries.`);
throw this.log.errorWithException(`Could not connect to a valid app after ${maxTries} tries.`);
}

if (this.appIdKey !== appIdKey) {
log.debug(`Received altered app id, updating from '${this.appIdKey}' to '${appIdKey}'`);
this.log.debug(`Received altered app id, updating from '${this.appIdKey}' to '${appIdKey}'`);
this.appIdKey = appIdKey;
}

logApplicationDictionary(this.appDict);
logApplicationDictionary.bind(this)(this.appDict);

// translate the dictionary into a useful form, and return to sender
const pageArray = _.isEmpty(this.appDict[appIdKey].pageArray)
? pageArrayFromDict(pageDict)
: this.appDict[appIdKey].pageArray;
log.debug(`Finally selecting app ${this.appIdKey}: ${simpleStringify(pageArray)}`);
this.log.debug(`Finally selecting app ${this.appIdKey}: ${simpleStringify(pageArray)}`);

/** @type {Page[]} */
const fullPageArray = [];
Expand All @@ -175,10 +171,10 @@ async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, igno
}
}

log.debug(`Selected app after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
this.log.debug(`Selected app after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
return fullPageArray;
} finally {
this.rpcClient.shouldCheckForTarget = shouldCheckForTarget;
rpcClient.shouldCheckForTarget = shouldCheckForTarget;
}
}

Expand All @@ -190,32 +186,28 @@ async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, igno
* @param {boolean} ignoreAboutBlankUrl
* @returns {Promise<AppPages?>}
*/
async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
export async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
const bundleIds = this.includeSafari && !this.isSafari
? [this.bundleId, ...this.additionalBundleIds, SAFARI_BUNDLE_ID]
: [this.bundleId, ...this.additionalBundleIds];
let retryCount = 0;
try {
return await retryInterval(maxTries, SELECT_APP_RETRY_SLEEP_MS, async () => {
if (!this.rpcClient) {
throw new Error('rpcClient is undefined. Is the debugger connected?');
}

logApplicationDictionary(this.appDict);
logApplicationDictionary.bind(this)(this.appDict);
const possibleAppIds = getPossibleDebuggerAppKeys(/** @type {string[]} */ (bundleIds), this.appDict);
log.debug(`Trying out the possible app ids: ${possibleAppIds.join(', ')} (try #${retryCount + 1} of ${maxTries})`);
this.log.debug(`Trying out the possible app ids: ${possibleAppIds.join(', ')} (try #${retryCount + 1} of ${maxTries})`);
for (const attemptedAppIdKey of possibleAppIds) {
try {
if (!this.appDict[attemptedAppIdKey].isActive) {
log.debug(`Skipping app '${attemptedAppIdKey}' because it is not active`);
this.log.debug(`Skipping app '${attemptedAppIdKey}' because it is not active`);
continue;
}
log.debug(`Attempting app '${attemptedAppIdKey}'`);
const [appIdKey, pageDict] = await this.rpcClient.selectApp(attemptedAppIdKey);
this.log.debug(`Attempting app '${attemptedAppIdKey}'`);
const [appIdKey, pageDict] = await this.requireRpcClient().selectApp(attemptedAppIdKey);
// in iOS 8.2 the connect logic happens, but with an empty dictionary
// which leads to the remote debugger getting disconnected, and into a loop
if (_.isEmpty(pageDict)) {
log.debug('Empty page dictionary received. Trying again.');
this.log.debug('Empty page dictionary received. Trying again.');
continue;
}

Expand All @@ -231,19 +223,19 @@ async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
}

if (currentUrl) {
log.debug(`Received app, but expected url ('${currentUrl}') was not found. Trying again.`);
this.log.debug(`Received app, but expected url ('${currentUrl}') was not found. Trying again.`);
} else {
log.debug('Received app, but no match was found. Trying again.');
this.log.debug('Received app, but no match was found. Trying again.');
}
} catch (err) {
log.debug(`Error checking application: '${err.message}'. Retrying connection`);
this.log.debug(`Error checking application: '${err.message}'. Retrying connection`);
}
}
retryCount++;
throw new Error('Failed to find an app to select');
});
} catch (ign) {
log.errorAndThrow(`Could not connect to a valid app after ${maxTries} tries.`);
this.log.errorAndThrow(`Could not connect to a valid app after ${maxTries} tries.`);
}
return null;
}
Expand All @@ -256,7 +248,7 @@ async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
* @param {boolean} [ignoreAboutBlankUrl]
* @returns {AppPages?}
*/
function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false) {
export function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false) {
for (const appDict of _.values(appsDict)) {
if (!appDict || !appDict.isActive || !appDict.pageArray || appDict.pageArray.promise) {
continue;
Expand All @@ -280,29 +272,26 @@ function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false
* @param {boolean} [skipReadyCheck]
* @returns {Promise<void>}
*/
async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
this.appIdKey = _.startsWith(`${appIdKey}`, 'PID:') ? `${appIdKey}` : `PID:${appIdKey}`;
this.pageIdKey = pageIdKey;

log.debug(`Selecting page '${pageIdKey}' on app '${this.appIdKey}' and forwarding socket setup`);
if (!this.rpcClient) {
throw new Error('rpcClient is undefined. Is the debugger connected?');
}
this.log.debug(`Selecting page '${pageIdKey}' on app '${this.appIdKey}' and forwarding socket setup`);

const timer = new timing.Timer().start();

await this.rpcClient.selectPage(this.appIdKey, pageIdKey);
await this.requireRpcClient().selectPage(this.appIdKey, pageIdKey);

// make sure everything is ready to go
if (!skipReadyCheck && !await this.checkPageIsReady()) {
await this.waitForDom();
}

log.debug(`Selected page after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
this.log.debug(`Selected page after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
}

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {Record<string, any>} apps
* @returns {void}
*/
Expand All @@ -318,56 +307,23 @@ function logApplicationDictionary (apps) {
return JSON.stringify(value);
}

log.debug('Current applications available:');
this.log.debug('Current applications available:');
for (const [app, info] of _.toPairs(apps)) {
log.debug(` Application: "${app}"`);
this.log.debug(` Application: "${app}"`);
for (const [key, value] of _.toPairs(info)) {
if (key === 'pageArray' && Array.isArray(value) && value.length) {
log.debug(` ${key}:`);
this.log.debug(` ${key}:`);
for (const page of value) {
let prefix = '- ';
for (const [k, v] of _.toPairs(page)) {
log.debug(` ${prefix}${k}: ${JSON.stringify(v)}`);
this.log.debug(` ${prefix}${k}: ${JSON.stringify(v)}`);
prefix = ' ';
}
}
} else {
const valueString = getValueString(key, value);
log.debug(` ${key}: ${valueString}`);
this.log.debug(` ${key}: ${valueString}`);
}
}
}
}

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {Record<string, any>} dict
* @returns {void}
*/
function updateAppsWithDict (dict) {
// get the dictionary entry into a nice form, and add it to the
// application dictionary
this.appDict = this.appDict || {};
let [id, entry] = appInfoFromDict(dict);
if (this.appDict[id]) {
// preserve the page dictionary for this entry
entry.pageArray = this.appDict[id].pageArray;
}
this.appDict[id] = entry;

// add a promise to get the page dictionary
if (_.isUndefined(entry.pageArray)) {
entry.pageArray = deferredPromise();
}

// try to get the app id from our connected apps
if (!this.appIdKey) {
this.appIdKey = getDebuggerAppKey(/** @type {string} */ (this.bundleId), this.appDict);
}
}

export default {
setConnectionKey, connect, disconnect, selectApp,
searchForApp, searchForPage, selectPage, updateAppsWithDict
};
45 changes: 45 additions & 0 deletions lib/mixins/cookies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @returns {Promise<void>}
*/
export async function getCookies () {
this.log.debug('Getting cookies');
return await this.requireRpcClient().send('Page.getCookies', {
appIdKey: this.appIdKey,
pageIdKey: this.pageIdKey
});
}

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {any} cookie
* @returns {Promise<any>}
*/
export async function setCookie (cookie) {
this.log.debug('Setting cookie');
return await this.requireRpcClient().send('Page.setCookie', {
appIdKey: this.appIdKey,
pageIdKey: this.pageIdKey,
cookie
});
}

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {string} cookieName
* @param {string} url
* @returns {Promise<any>}
*/
export async function deleteCookie (cookieName, url) {
this.log.debug(`Deleting cookie '${cookieName}' on '${url}'`);
return await this.requireRpcClient().send('Page.deleteCookie', {
appIdKey: this.appIdKey,
pageIdKey: this.pageIdKey,
cookieName,
url,
});
}
Loading

0 comments on commit d1fda80

Please sign in to comment.