Skip to content

Commit

Permalink
feat: Rewrite the RemoteDebugger class into typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Aug 3, 2024
1 parent 0f240fa commit e086a27
Show file tree
Hide file tree
Showing 17 changed files with 598 additions and 544 deletions.
116 changes: 65 additions & 51 deletions lib/mixins/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,19 @@ export async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
const timer = new timing.Timer().start();
this.log.debug(`Waiting up to ${timeout}ms for applications to be reported`);
try {
await waitForCondition(() => !_.isEmpty(this.appDict), {
await waitForCondition(() => !_.isEmpty(this._appDict), {
waitMs: timeout,
intervalMs: APP_CONNECT_INTERVAL_MS,
});
this.log.debug(
`Retrieved ${util.pluralize('application', _.size(this.appDict), true)} ` +
`Retrieved ${util.pluralize('application', _.size(this._appDict), true)} ` +
`within ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
);
} catch (err) {
this.log.debug(`Timed out waiting for applications to be reported`);
}
}
return this.appDict || {};
return this._appDict || {};
} catch (err) {
this.log.error(`Error setting connection key: ${err.message}`);
await this.disconnect();
Expand All @@ -92,8 +92,8 @@ export async function connect (timeout = APP_CONNECT_TIMEOUT_MS) {
* @returns {Promise<void>}
*/
export async function disconnect () {
if (this.rpcClient) {
await this.rpcClient.disconnect();
if (this._rpcClient) {
await this._rpcClient.disconnect();
}
this.emit(events.EVENT_DISCONNECT, true);
this.teardown();
Expand All @@ -115,23 +115,23 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
rpcClient.shouldCheckForTarget = false;
try {
const timer = new timing.Timer().start();
if (!this.appDict || _.isEmpty(this.appDict)) {
if (!this._appDict || _.isEmpty(this._appDict)) {
this.log.debug('No applications currently connected.');
return [];
}

const { appIdKey } = await this.searchForApp(currentUrl, maxTries, ignoreAboutBlankUrl);
if (this.appIdKey !== appIdKey) {
this.log.debug(`Received altered app id, updating from '${this.appIdKey}' to '${appIdKey}'`);
this.appIdKey = appIdKey;
const { appIdKey } = await searchForApp.bind(this)(currentUrl, maxTries, ignoreAboutBlankUrl);
if (this._appIdKey !== appIdKey) {
this.log.debug(`Received altered app id, updating from '${this._appIdKey}' to '${appIdKey}'`);
this._appIdKey = appIdKey;
}
logApplicationDictionary.bind(this)();
// translate the dictionary into a useful form, and return to sender
this.log.debug(`Finally selecting app ${this.appIdKey}`);
this.log.debug(`Finally selecting app ${this._appIdKey}`);

/** @type {import('../types').Page[]} */
const fullPageArray = [];
for (const [app, info] of _.toPairs(this.appDict)) {
for (const [app, info] of _.toPairs(this._appDict)) {
if (!_.isArray(info.pageArray) || !info.isActive) {
continue;
}
Expand All @@ -153,6 +153,32 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
}
}

/**
*
* @this {RemoteDebugger}
* @param {string|number} appIdKey
* @param {string|number} pageIdKey
* @param {boolean} [skipReadyCheck]
* @returns {Promise<void>}
*/
export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
this._appIdKey = _.startsWith(`${appIdKey}`, 'PID:') ? `${appIdKey}` : `PID:${appIdKey}`;
this._pageIdKey = pageIdKey;

this.log.debug(`Selecting page '${pageIdKey}' on app '${this._appIdKey}' and forwarding socket setup`);

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

await this.requireRpcClient().selectPage(this._appIdKey, pageIdKey);

if (!skipReadyCheck && !await this.checkPageIsReady()) {
await this.waitForDom();
}

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


/**
*
* @this {RemoteDebugger}
Expand All @@ -161,18 +187,18 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
* @param {boolean} ignoreAboutBlankUrl
* @returns {Promise<import('../types').AppPage>}
*/
export async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
const bundleIds = this.includeSafari && !this.isSafari
? [this.bundleId, ...this.additionalBundleIds, SAFARI_BUNDLE_ID]
: [this.bundleId, ...this.additionalBundleIds];
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;
return /** @type {import('../types').AppPage} */ (await retryInterval(maxTries, SELECT_APP_RETRY_SLEEP_MS, async () => {
logApplicationDictionary.bind(this)();
const possibleAppIds = getPossibleDebuggerAppKeys.bind(this)(/** @type {string[]} */ (bundleIds));
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) {
if (!this._appDict[attemptedAppIdKey].isActive) {
this.log.debug(`Skipping app '${attemptedAppIdKey}' because it is not active`);
continue;
}
Expand All @@ -190,12 +216,12 @@ export async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
}

// save the page array for this app
this.appDict[appIdKey].pageArray = pageArrayFromDict(pageDict);
this._appDict[appIdKey].pageArray = pageArrayFromDict(pageDict);

// if we are looking for a particular url, make sure we
// have the right page. Ignore empty or undefined urls.
// Ignore about:blank if requested.
const result = this.searchForPage(this.appDict, currentUrl, ignoreAboutBlankUrl);
const result = searchForPage.bind(this)(this._appDict, currentUrl, ignoreAboutBlankUrl);
if (result) {
return result;
}
Expand Down Expand Up @@ -225,7 +251,7 @@ export async function searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
* @param {boolean} [ignoreAboutBlankUrl]
* @returns {import('../types').AppPage?}
*/
export function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false) {
function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false) {
for (const appDict of _.values(appsDict)) {
if (!appDict || !appDict.isActive || !appDict.pageArray || _.isEmpty(appDict.pageArray)) {
continue;
Expand All @@ -244,39 +270,13 @@ export function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl
return null;
}

/**
*
* @this {RemoteDebugger}
* @param {string|number} appIdKey
* @param {string|number} pageIdKey
* @param {boolean} [skipReadyCheck]
* @returns {Promise<void>}
*/
export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {
this.appIdKey = _.startsWith(`${appIdKey}`, 'PID:') ? `${appIdKey}` : `PID:${appIdKey}`;
this.pageIdKey = pageIdKey;

this.log.debug(`Selecting page '${pageIdKey}' on app '${this.appIdKey}' and forwarding socket setup`);

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

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

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

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

/**
* @this {RemoteDebugger}
* @returns {void}
*/
function logApplicationDictionary () {
this.log.debug('Current applications available:');
for (const [app, info] of _.toPairs(this.appDict)) {
for (const [app, info] of _.toPairs(this._appDict)) {
this.log.debug(` Application: "${app}"`);
for (const [key, value] of _.toPairs(info)) {
if (key === 'pageArray' && Array.isArray(value) && value.length) {
Expand Down Expand Up @@ -307,7 +307,7 @@ function logApplicationDictionary () {
export function getPossibleDebuggerAppKeys(bundleIds) {
if (bundleIds.includes(WILDCARD_BUNDLE_ID)) {
this.log.debug('Skip checking bundle identifiers because the bundleIds includes a wildcard');
return _.uniq(Object.keys(this.appDict));
return _.uniq(Object.keys(this._appDict));
}

// go through the possible bundle identifiers
Expand All @@ -324,10 +324,10 @@ export function getPossibleDebuggerAppKeys(bundleIds) {
const proxiedAppIds = new Set();
for (const bundleId of possibleBundleIds) {
// now we need to determine if we should pick a proxy for this instead
for (const appId of appIdsForBundle(bundleId, this.appDict)) {
for (const appId of appIdsForBundle(bundleId, this._appDict)) {
proxiedAppIds.add(appId);
this.log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`);
for (const [key, data] of _.toPairs(this.appDict)) {
for (const [key, data] of _.toPairs(this._appDict)) {
if (data.isProxy && data.hostId === appId) {
this.log.debug(
`Found separate bundleId '${data.bundleId}' ` +
Expand All @@ -343,5 +343,19 @@ export function getPossibleDebuggerAppKeys(bundleIds) {
}

/**
* @typedef {import('../remote-debugger').RemoteDebugger} RemoteDebugger
* @typedef {Object} HasConnectionRelatedProperties
* @property {string | null | undefined} _appIdKey
* @property {string | number | null | undefined} _pageIdKey
* @property {import('../types').AppDict} _appDict
* @property {string | undefined} _bundleId
* @property {import('../rpc/rpc-client').RpcClient | undefined} _rpcClient
* @property {boolean} _includeSafari
* @property {boolean} _isSafari
* @property {string[]} _additionalBundleIds
* @property {(this: RemoteDebugger, timeoutMs?: number | undefined) => Promise<boolean>} checkPageIsReady:
* @property {(this: RemoteDebugger, startPageLoadTimer?: timing.Timer | null | undefined) => Promise<void>} waitForDom:
*/

/**
* @typedef {import('../remote-debugger').RemoteDebugger & HasConnectionRelatedProperties} RemoteDebugger
*/
20 changes: 13 additions & 7 deletions lib/mixins/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
export async function getCookies () {
this.log.debug('Getting cookies');
return await this.requireRpcClient().send('Page.getCookies', {
appIdKey: this.appIdKey,
pageIdKey: this.pageIdKey
appIdKey: this._appIdKey,
pageIdKey: this._pageIdKey
});
}

Expand All @@ -21,8 +21,8 @@ export async function getCookies () {
export async function setCookie (cookie) {
this.log.debug('Setting cookie');
return await this.requireRpcClient().send('Page.setCookie', {
appIdKey: this.appIdKey,
pageIdKey: this.pageIdKey,
appIdKey: this._appIdKey,
pageIdKey: this._pageIdKey,
cookie
});
}
Expand All @@ -37,13 +37,19 @@ export async function setCookie (cookie) {
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,
appIdKey: this._appIdKey,
pageIdKey: this._pageIdKey,
cookieName,
url,
});
}

/**
* @typedef {import('../remote-debugger').RemoteDebugger} RemoteDebugger
* @typedef {Object} HasCookiesRelatedProperties
* @property {string | null | undefined} _appIdKey
* @property {string | number | null | undefined} _pageIdKey
*/

/**
* @typedef {import('../remote-debugger').RemoteDebugger & HasCookiesRelatedProperties} RemoteDebugger
*/
49 changes: 47 additions & 2 deletions lib/mixins/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const events = {
*
* @this {RemoteDebugger}
* @param {string} eventName
* @param {(event: import('@appium/types').StringRecord) => any} listener
* @param {import('../types').EventListener} listener
* @returns {void}
*/
export function addClientEventListener (eventName, listener) {
Expand All @@ -30,8 +30,53 @@ export function removeClientEventListener (eventName) {
}
}

/**
* @this {RemoteDebugger}
* @param {import('../types').EventListener} listener
* @returns {void}
*/
export function startConsole (listener) {
this.log.debug('Starting to listen for JavaScript console');
this.addClientEventListener('Console.messageAdded', listener);
this.addClientEventListener('Console.messageRepeatCountUpdated', listener);
}

/**
* @this {RemoteDebugger}
* @returns {void}
*/
export function stopConsole () {
this.log.debug('Stopping to listen for JavaScript console');
this.removeClientEventListener('Console.messageAdded');
this.removeClientEventListener('Console.messageRepeatCountUpdated');
}

/**
* @this {RemoteDebugger}
* @param {import('../types').EventListener} listener
* @returns {void}
*/
export function startNetwork (listener) {
this.log.debug('Starting to listen for network events');
this.addClientEventListener('NetworkEvent', listener);
}

/**
* @this {RemoteDebugger}
* @returns {void}
*/
export function stopNetwork () {
this.log.debug('Stopping to listen for network events');
this.removeClientEventListener('NetworkEvent');
}

export default events;

/**
* @typedef {import('../remote-debugger').RemoteDebugger} RemoteDebugger
* @typedef {Object} HasEventsRelatedProperties
* @property {import('@appium/types').StringRecord<import('../types').EventListener[]>} _clientEventListeners:
*/

/**
* @typedef {import('../remote-debugger').RemoteDebugger & HasEventsRelatedProperties} RemoteDebugger
*/
Loading

0 comments on commit e086a27

Please sign in to comment.