Skip to content

Commit

Permalink
feat: Rewrite the RemoteDebugger class into typescript (#400)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The following class properties have been renamed (got a leading underscore) and made protected:
skippedApps
clientEventListeners
appDict
appIdKey
pageIdKey
connectedDrivers
currentState
pageLoadDelay
rpcClient
pageLoading
navigatingToPage
pageLoadStrategy
bundleId
additionalBundleIds
platformVersion
isSafari
includeSafari
useNewSafari
pageLoadMs
garbageCollectOnExecute
host
port
socketPath
remoteDebugProxy
pageReadyTimeout
logAllCommunication
logAllCommunicationHexDump
socketChunkSize
webInspectorMaxFrameLength
fullPageInitialization
udid

BREAKING CHANGE: The following class methods have been made private:
searchForApp
searchForPage

BREAKING CHANGE: Remove the unused RPC_RESPONSE_TIMEOUT_MS export
  • Loading branch information
mykola-mokhnach authored Aug 4, 2024
1 parent 0f240fa commit ef277b3
Show file tree
Hide file tree
Showing 18 changed files with 602 additions and 544 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: Functional Tests

on: [pull_request]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
e2e:
strategy:
Expand Down
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 ef277b3

Please sign in to comment.