Skip to content

Commit

Permalink
fix: Tune page array retrieval (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jul 31, 2024
1 parent 84addf1 commit 8bb5ce3
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 220 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ jobs:
strategy:
matrix:
include:
- os: macos-13
- os: macos-14
xcode: '15.1'
ios: '17.2'
device: iPhone 14
device: iPhone 15
- os: macos-13
xcode: '14.3.1'
ios: '16.4'
Expand All @@ -22,7 +22,7 @@ jobs:
DEVICE_NAME: ${{ matrix.device }}
CI: true
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.XCODE_VERSION }}
Expand All @@ -31,6 +31,13 @@ jobs:
with:
node-version: lts/*
check-latest: true
- name: Prepare iOS simulator
run: |
open -Fn "$(xcode-select -p)/Applications/Simulator.app"
udid=$(xcrun simctl list devices available -j | \
node -p "Object.entries(JSON.parse(fs.readFileSync(0)).devices).filter((x) => x[0].includes('$PLATFORM_VERSION'.replace('.', '-'))).reduce((acc, x) => [...acc, ...x[1]], []).find(({name}) => name === '$DEVICE_NAME').udid")
xcrun simctl bootstatus $udid -b
xcrun simctl openurl $udid "https://google.com"
- run: npm install
- run: |
export PATH="${PATH}:$(python -c 'import site; print(site.USER_BASE)')/bin"
Expand Down
189 changes: 84 additions & 105 deletions lib/mixins/connect.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
import {
pageArrayFromDict,
getPossibleDebuggerAppKeys,
simpleStringify,
} from '../utils';
import events from './events';
import { timing } from '@appium/support';
import { retryInterval, waitForCondition } from 'asyncbox';
import _ from 'lodash';


const APP_CONNECT_TIMEOUT_MS = 0;
const APP_CONNECT_INTERVAL_MS = 100;
const SELECT_APP_RETRIES = 20;
const SELECT_APP_RETRY_SLEEP_MS = 500;
const SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
const BLANK_PAGE_URL = 'about:blank';

/**
* @typedef {Object} AppPages
* @property {string} appIdKey
* @property {Record<string, any>} pageDict
*/

/**
* @typedef {Object} App
* @property {string} id
* @property {string} bundleId
*/


/**
*
* @this {import('../remote-debugger').RemoteDebugger}
Expand Down Expand Up @@ -104,21 +89,12 @@ export async function disconnect () {
this.teardown();
}

/**
* @typedef {Object} Page
* @property {string} url
* @property {string} title
* @property {number} id
* @property {boolean} isKey
* @property {string} [bundleId]
*/

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {string?} currentUrl
* @param {number} [maxTries]
* @param {boolean} [ignoreAboutBlankUrl]
* @param {string?} [currentUrl=null]
* @param {number} [maxTries=SELECT_APP_RETRIES]
* @param {boolean} [ignoreAboutBlankUrl=false]
* @returns {Promise<Page[]>}
*/
export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, ignoreAboutBlankUrl = false) {
Expand All @@ -134,25 +110,14 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
return [];
}

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

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

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;
}

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

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

/** @type {Page[]} */
const fullPageArray = [];
Expand Down Expand Up @@ -184,80 +149,85 @@ export async function selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIE
* @param {string?} currentUrl
* @param {number} maxTries
* @param {boolean} ignoreAboutBlankUrl
* @returns {Promise<AppPages?>}
* @returns {Promise<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];
let retryCount = 0;
try {
return await retryInterval(maxTries, SELECT_APP_RETRY_SLEEP_MS, async () => {
logApplicationDictionary.bind(this)(this.appDict);
const possibleAppIds = getPossibleDebuggerAppKeys(/** @type {string[]} */ (bundleIds), this.appDict);
this.log.debug(`Trying out the possible app ids: ${possibleAppIds.join(', ')} (try #${retryCount + 1} of ${maxTries})`);
for (const attemptedAppIdKey of possibleAppIds) {
return /** @type {AppPage} */ (await retryInterval(maxTries, SELECT_APP_RETRY_SLEEP_MS, async () => {
logApplicationDictionary.bind(this)();
const possibleAppIds = getPossibleDebuggerAppKeys(/** @type {string[]} */ (bundleIds), this.appDict);
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) {
this.log.debug(`Skipping app '${attemptedAppIdKey}' because it is not active`);
continue;
}

this.log.debug(`Attempting app '${attemptedAppIdKey}'`);
/** @type {string} */
let appIdKey;
/** @type {import('@appium/types').StringRecord} */
let pageDict;
try {
if (!this.appDict[attemptedAppIdKey].isActive) {
this.log.debug(`Skipping app '${attemptedAppIdKey}' because it is not active`);
continue;
}
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)) {
this.log.debug('Empty page dictionary received. Trying again.');
continue;
}
[appIdKey, pageDict] = await this.requireRpcClient().selectApp(attemptedAppIdKey);
} catch (e) {
this.log.info(`Skipping app '${attemptedAppIdKey}'. Original error: ${e.message}`);
continue;
}

// save the page array for this app
this.appDict[appIdKey].pageArray = pageArrayFromDict(pageDict);
// save the page array for this app
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);
if (result) {
return result;
}
// 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);
if (result) {
return result;
}

if (currentUrl) {
this.log.debug(`Received app, but expected url ('${currentUrl}') was not found. Trying again.`);
} else {
this.log.debug('Received app, but no match was found. Trying again.');
}
} catch (err) {
this.log.debug(`Error checking application: '${err.message}'. Retrying connection`);
if (currentUrl) {
this.log.debug(`Received app, but expected url ('${currentUrl}') was not found. Trying again.`);
} else {
this.log.debug('Received app, but no match was found. Trying again.');
}
} catch (err) {
this.log.debug(err.stack);
this.log.warn(`Error checking application ${attemptedAppIdKey}: '${err.message}'`);
}
retryCount++;
throw new Error('Failed to find an app to select');
});
} catch (ign) {
this.log.errorAndThrow(`Could not connect to a valid app after ${maxTries} tries.`);
}
return null;
}
retryCount++;
throw new Error(
`Could not connect to a valid webapp. Make sure it is debuggable and has at least one active page.`
);
}));
}

/**
*
* @this {import('../remote-debugger').RemoteDebugger}
* @param {Record<string, any>} appsDict
* @param {Record<string, import('../utils').AppInfo>} appsDict
* @param {string?} currentUrl
* @param {boolean} [ignoreAboutBlankUrl]
* @returns {AppPages?}
* @returns {AppPage?}
*/
export function searchForPage (appsDict, currentUrl = null, ignoreAboutBlankUrl = false) {
for (const appDict of _.values(appsDict)) {
if (!appDict || !appDict.isActive || !appDict.pageArray || appDict.pageArray.promise) {
if (!appDict || !appDict.isActive || !appDict.pageArray || _.isEmpty(appDict.pageArray)) {
continue;
}

for (const dict of appDict.pageArray) {
if ((!ignoreAboutBlankUrl || dict.url !== BLANK_PAGE_URL) &&
(!currentUrl || dict.url === currentUrl || dict.url === `${currentUrl}/`)) {
return { appIdKey: appDict.id, pageDict: dict };
for (const page of appDict.pageArray) {
if ((!ignoreAboutBlankUrl || page.url !== BLANK_PAGE_URL) &&
(!currentUrl || page.url === currentUrl || page.url === `${currentUrl}/`)) {
return {
appIdKey: appDict.id,
pageDict: page
};
}
}
}
Expand Down Expand Up @@ -292,23 +262,11 @@ export async function selectPage (appIdKey, pageIdKey, skipReadyCheck = false) {

/**
* @this {import('../remote-debugger').RemoteDebugger}
* @param {Record<string, any>} apps
* @returns {void}
*/
function logApplicationDictionary (apps) {

function getValueString (key, value) {
if (_.isFunction(value)) {
return '[Function]';
}
if (key === 'pageArray' && !_.isArray(value)) {
return `"Waiting for data"`;
}
return JSON.stringify(value);
}

function logApplicationDictionary () {
this.log.debug('Current applications available:');
for (const [app, info] of _.toPairs(apps)) {
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 All @@ -321,9 +279,30 @@ function logApplicationDictionary (apps) {
}
}
} else {
const valueString = getValueString(key, value);
const valueString = _.isFunction(value) ? '[Function]' : JSON.stringify(value);
this.log.debug(` ${key}: ${valueString}`);
}
}
}
}

/**
* @typedef {Object} AppPage
* @property {string} appIdKey
* @property {Page} pageDict
*/

/**
* @typedef {Object} App
* @property {string} id
* @property {string} bundleId
*/

/**
* @typedef {Object} Page
* @property {string} url
* @property {string} title
* @property {number} id
* @property {boolean} isKey
* @property {string} [bundleId]
*/
Loading

0 comments on commit 8bb5ce3

Please sign in to comment.