Skip to content

Commit 9b0f1f0

Browse files
authored
feat: Improve Appium Inspector stability by handling USB/Wifi disconnection gracefully (#2328)
* Improve Appium Inspector stability by handling USB/Wifi disconnection * handled undefined localstorage * handled undefined localstorage * handled undefined localstorage * handled undefined localstorage * Toggle for Auto Restart Session added * Toggle for Auto Restart Session added * Images updated in assets folder * Images are updated in assets folder * Comments Fixed * minor fix * implemented custom session expired error message * comment fixed * comment fixed * Comment Fixed * Comment fixed * Comment Fixed * lint, prettier check * comment fixed * else part added for the condition * W3C error code implemented to check session expire * Comment Fixed * Comment Fixed * Comment Fixed * Comment Fixed * Catch block pushed to callClientMethod from applyClientMethod * Constant Added * lint and prettier the code * prettier the code * comment fixed * replacing with unicode for ellipses and hyphen
1 parent c12da86 commit 9b0f1f0

File tree

6 files changed

+190
-77
lines changed

6 files changed

+190
-77
lines changed

app/common/public/locales/en/translation.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,5 +314,7 @@
314314
"Fireflink DeviceFarm Domain": "Fireflink DeviceFarm Domain",
315315
"Fireflink DeviceFarm Access Key": "Fireflink DeviceFarm Access Key",
316316
"Fireflink DeviceFarm License ID": "Fireflink DeviceFarm License ID",
317-
"Fireflink DeviceFarm Project Name": "Fireflink DeviceFarm Project Name"
317+
"Fireflink DeviceFarm Project Name": "Fireflink DeviceFarm Project Name",
318+
"RestartSessionMessage": "Device Connection Lost – Restarting the Session…",
319+
"ToggleRestartSession": "Toggle Session Reload Upon Device Disconnect"
318320
}

app/common/renderer/actions/SessionInspector.js

Lines changed: 118 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@ import _ from 'lodash';
22

33
import {SAVED_CLIENT_FRAMEWORK, SET_SAVED_GESTURES} from '../../shared/setting-defs.js';
44
import {POINTER_TYPES} from '../constants/gestures.js';
5-
import {APP_MODE, NATIVE_APP} from '../constants/session-inspector.js';
5+
import {APP_MODE, NATIVE_APP, UNKNOWN_ERROR} from '../constants/session-inspector.js';
66
import i18n from '../i18next.js';
77
import InspectorDriver from '../lib/appium/inspector-driver.js';
88
import {CLIENT_FRAMEWORK_MAP} from '../lib/client-frameworks/map.js';
99
import {getSetting, setSetting} from '../polyfills.js';
1010
import {readTextFromUploadedFiles} from '../utils/file-handling.js';
1111
import {getOptimalXPath, getSuggestedLocators} from '../utils/locator-generation.js';
1212
import {log} from '../utils/logger.js';
13+
import {notification} from '../utils/notification.js';
1314
import {
1415
findDOMNodeByPath,
1516
findJSONElementByPath,
1617
xmlToDOM,
1718
xmlToJSON,
1819
} from '../utils/source-parsing.js';
19-
import {showError} from './SessionBuilder.js';
20+
import {newSession, showError} from './SessionBuilder.js';
2021

2122
export const SET_SESSION_DETAILS = 'SET_SESSION_DETAILS';
2223
export const SET_SOURCE_AND_SCREENSHOT = 'SET_SOURCE_AND_SCREENSHOT';
@@ -117,6 +118,7 @@ export const TOGGLE_SHOW_ATTRIBUTES = 'TOGGLE_SHOW_ATTRIBUTES';
117118
export const TOGGLE_REFRESHING_STATE = 'TOGGLE_REFRESHING_STATE';
118119

119120
export const SET_GESTURE_UPLOAD_ERROR = 'SET_GESTURE_UPLOAD_ERROR';
121+
export const SET_AUTO_SESSION_RESTART = 'SET_AUTO_SESSION_RESTART';
120122

121123
const KEEP_ALIVE_PING_INTERVAL = 20 * 1000;
122124
const NO_NEW_COMMAND_LIMIT = 24 * 60 * 60 * 1000; // Set timeout to 24 hours
@@ -272,65 +274,90 @@ export function applyClientMethod(params) {
272274
params.methodName !== 'getPageSource' &&
273275
params.methodName !== 'gesture' &&
274276
getState().inspector.isRecording;
275-
try {
276-
dispatch({type: METHOD_CALL_REQUESTED});
277-
const callAction = callClientMethod(params);
278-
const {
277+
dispatch({type: METHOD_CALL_REQUESTED});
278+
const callAction = callClientMethod(params);
279+
const {
280+
contexts,
281+
contextsError,
282+
commandRes,
283+
currentContext,
284+
currentContextError,
285+
source,
286+
screenshot,
287+
windowSize,
288+
sourceError,
289+
screenshotError,
290+
windowSizeError,
291+
variableName,
292+
variableIndex,
293+
strategy,
294+
selector,
295+
} = await callAction(dispatch, getState);
296+
297+
// TODO: Implement recorder code for gestures
298+
if (isRecording) {
299+
// Add 'findAndAssign' line of code. Don't do it for arrays though. Arrays already have 'find' expression
300+
if (strategy && selector && !variableIndex && variableIndex !== 0) {
301+
const findAction = findAndAssign(strategy, selector, variableName, false);
302+
findAction(dispatch, getState);
303+
}
304+
305+
// now record the actual action
306+
let args = [variableName, variableIndex];
307+
args = args.concat(params.args || []);
308+
dispatch({type: RECORD_ACTION, action: params.methodName, params: args});
309+
}
310+
dispatch({type: METHOD_CALL_DONE});
311+
312+
if (source) {
313+
dispatch({
314+
type: SET_SOURCE_AND_SCREENSHOT,
279315
contexts,
280-
contextsError,
281-
commandRes,
282316
currentContext,
283-
currentContextError,
284-
source,
317+
sourceJSON: xmlToJSON(source),
318+
sourceXML: source,
285319
screenshot,
286320
windowSize,
321+
contextsError,
322+
currentContextError,
287323
sourceError,
288324
screenshotError,
289325
windowSizeError,
290-
variableName,
291-
variableIndex,
292-
strategy,
293-
selector,
294-
} = await callAction(dispatch, getState);
295-
296-
// TODO: Implement recorder code for gestures
297-
if (isRecording) {
298-
// Add 'findAndAssign' line of code. Don't do it for arrays though. Arrays already have 'find' expression
299-
if (strategy && selector && !variableIndex && variableIndex !== 0) {
300-
const findAction = findAndAssign(strategy, selector, variableName, false);
301-
findAction(dispatch, getState);
302-
}
303-
304-
// now record the actual action
305-
let args = [variableName, variableIndex];
306-
args = args.concat(params.args || []);
307-
dispatch({type: RECORD_ACTION, action: params.methodName, params: args});
308-
}
309-
dispatch({type: METHOD_CALL_DONE});
326+
});
327+
}
328+
window.dispatchEvent(new Event('resize'));
329+
return commandRes;
330+
};
331+
}
310332

311-
if (source) {
312-
dispatch({
313-
type: SET_SOURCE_AND_SCREENSHOT,
314-
contexts,
315-
currentContext,
316-
sourceJSON: xmlToJSON(source),
317-
sourceXML: source,
318-
screenshot,
319-
windowSize,
320-
contextsError,
321-
currentContextError,
322-
sourceError,
323-
screenshotError,
324-
windowSizeError,
325-
});
326-
}
327-
window.dispatchEvent(new Event('resize'));
328-
return commandRes;
329-
} catch (error) {
330-
log.error(error);
333+
export function restartSession(error, params) {
334+
return async (dispatch, getState) => {
335+
if (error?.name !== UNKNOWN_ERROR) {
331336
showError(error, {methodName: params.methodName, secs: 10});
332-
dispatch({type: METHOD_CALL_DONE});
337+
return dispatch({type: METHOD_CALL_DONE});
333338
}
339+
showError(error, {methodName: params.methodName, secs: 3});
340+
notification.info({
341+
message: i18n.t('RestartSessionMessage'),
342+
duration: 3,
343+
});
344+
const quitSes = quitSession('Window closed');
345+
const newSes = newSession(getState().builder.caps);
346+
const getPageSrc = applyClientMethod({methodName: 'getPageSource', ignoreResult: true});
347+
const storeSessionSet = storeSessionSettings();
348+
const getSavedClientFrame = getSavedClientFramework();
349+
const runKeepAliveLp = runKeepAliveLoop();
350+
const setSesTime = setSessionTime(Date.now());
351+
352+
await quitSes(dispatch, getState);
353+
await newSes(dispatch, getState);
354+
await getPageSrc(dispatch, getState);
355+
await storeSessionSet(dispatch, getState);
356+
await getSavedClientFrame(dispatch);
357+
runKeepAliveLp(dispatch, getState);
358+
setSesTime(dispatch);
359+
dispatch({type: SET_AUTO_SESSION_RESTART, autoSessionRestart: true});
360+
dispatch({type: METHOD_CALL_DONE});
334361
};
335362
}
336363

@@ -878,9 +905,11 @@ export function keepSessionAlive() {
878905

879906
export function callClientMethod(params) {
880907
return async (dispatch, getState) => {
881-
const {driver, appMode, isUsingMjpegMode, isSourceRefreshOn} = getState().inspector;
908+
const {driver, appMode, isUsingMjpegMode, isSourceRefreshOn, autoSessionRestart} =
909+
getState().inspector;
882910
const {methodName, ignoreResult = true} = params;
883911
params.appMode = appMode;
912+
params.autoSessionRestart = autoSessionRestart;
884913

885914
// don't retrieve screenshot if we're already using the mjpeg stream
886915
if (isUsingMjpegMode) {
@@ -893,28 +922,38 @@ export function callClientMethod(params) {
893922

894923
log.info(`Calling client method with params:`);
895924
log.info(params);
896-
const action = keepSessionAlive();
897-
action(dispatch, getState);
898-
const inspectorDriver = InspectorDriver.instance(driver);
899-
const res = await inspectorDriver.run(params);
900-
let {commandRes} = res;
901-
902-
// Ignore empty objects
903-
if (_.isObject(res) && _.isEmpty(res)) {
904-
commandRes = null;
905-
}
925+
try {
926+
const action = keepSessionAlive();
927+
action(dispatch, getState);
928+
const inspectorDriver = InspectorDriver.instance(driver);
929+
const res = await inspectorDriver.run(params);
930+
let {commandRes} = res;
931+
932+
// Ignore empty objects
933+
if (_.isObject(res) && _.isEmpty(res)) {
934+
commandRes = null;
935+
}
906936

907-
if (!ignoreResult) {
908-
// if the user is running actions manually, we want to show the full response with the
909-
// ability to scroll etc...
910-
const result = JSON.stringify(commandRes, null, ' ');
911-
const truncatedResult = _.truncate(result, {length: 2000});
912-
log.info(`Result of client command was:`);
913-
log.info(truncatedResult);
914-
setVisibleCommandResult(result, methodName)(dispatch);
937+
if (!ignoreResult) {
938+
// if the user is running actions manually, we want to show the full response with the
939+
// ability to scroll etc...
940+
const result = JSON.stringify(commandRes, null, ' ');
941+
const truncatedResult = _.truncate(result, {length: 2000});
942+
log.info(`Result of client command was:`);
943+
log.info(truncatedResult);
944+
setVisibleCommandResult(result, methodName)(dispatch);
945+
}
946+
res.elementId = res.id;
947+
return res;
948+
} catch (error) {
949+
log.error(error);
950+
if (getState().inspector.autoSessionRestart) {
951+
const restartSes = restartSession(error, params);
952+
return await restartSes(dispatch, getState);
953+
}
954+
showError(error, {methodName: params.methodName, secs: 10});
955+
dispatch({type: METHOD_CALL_DONE});
915956
}
916-
res.elementId = res.id;
917-
return res;
918957
};
919958
}
920959

@@ -1096,3 +1135,10 @@ export function toggleShowAttributes() {
10961135
dispatch({type: TOGGLE_SHOW_ATTRIBUTES});
10971136
};
10981137
}
1138+
1139+
export function toggleAutoSessionRestart() {
1140+
return (dispatch, getState) => {
1141+
const autoSessionRestart = !getState().inspector.autoSessionRestart;
1142+
dispatch({type: SET_AUTO_SESSION_RESTART, autoSessionRestart});
1143+
};
1144+
}

app/common/renderer/components/SessionInspector/Header/HeaderButtons.jsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
VideoCameraOutlined,
1212
} from '@ant-design/icons';
1313
import {Button, Divider, Select, Space, Tooltip} from 'antd';
14-
import {BiCircle, BiSquare} from 'react-icons/bi';
14+
import {BiCircle, BiRecycle, BiSquare} from 'react-icons/bi';
1515
import {HiOutlineHome, HiOutlineMicrophone} from 'react-icons/hi';
1616
import {IoChevronBackOutline} from 'react-icons/io5';
1717

@@ -40,6 +40,8 @@ const HeaderButtons = (props) => {
4040
currentContext,
4141
setContext,
4242
t,
43+
autoSessionRestart,
44+
toggleAutoSessionRestart,
4345
} = props;
4446

4547
const deviceControls = (
@@ -225,12 +227,24 @@ const HeaderButtons = (props) => {
225227
</Tooltip>
226228
);
227229

230+
const sessionReloadButton = (
231+
<Tooltip title={t('ToggleRestartSession')}>
232+
<Button
233+
id={autoSessionRestart ? 'btnDisableRestartSession' : 'btnEnableRestartSession'}
234+
icon={<BiRecycle />}
235+
type={autoSessionRestart ? BUTTON.PRIMARY : undefined}
236+
onClick={toggleAutoSessionRestart}
237+
/>
238+
</Tooltip>
239+
);
240+
228241
return (
229242
<div className={styles.headerButtons}>
230243
<Space size="middle">
231244
{deviceControls}
232245
{appModeControls}
233246
{generalControls}
247+
{sessionReloadButton}
234248
{quitSessionButton}
235249
</Space>
236250
<Divider />

app/common/renderer/constants/session-inspector.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export const APP_MODE = {
99

1010
export const NATIVE_APP = 'NATIVE_APP';
1111

12+
export const UNKNOWN_ERROR = 'unknown error';
13+
export const SESSION_EXPIRED = 'Session Expired';
14+
1215
export const LOCATOR_STRATEGIES = {
1316
ID: 'id',
1417
XPATH: 'xpath',

0 commit comments

Comments
 (0)