Skip to content

Commit

Permalink
Merge pull request #305 from webdriverio/ws/fix-147-ios-alert-issue-p…
Browse files Browse the repository at this point in the history
…roperly

fix: revert first fix and now do it properly
  • Loading branch information
wswebcreation authored Apr 1, 2024
2 parents b6b6386 + 5fcfcb3 commit 8997b6a
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 141 deletions.
62 changes: 17 additions & 45 deletions tests/helpers/Biometrics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import NativeAlert from '../screenobjects/components/NativeAlert.js';
import { DEFAULT_PIN, INCORRECT_PIN } from './Constants.js';
import { executeInHomeScreenContext } from './Utils.js';

class Biometrics {
// Due to the iOS driver issue we need to wait for the alert in a different way
// meaning that we will not use them for now
// private get iosAllowBiometry() {return $('~Don’t Allow');}
// private get allowBiometry() {return $('-ios class chain:**/XCUIElementTypeButton[`name == "Allow" OR name=="OK"`]');}
private get iosAllowBiometry() {return $('~Don’t Allow');}
private get allowBiometry() {return $('-ios class chain:**/XCUIElementTypeButton[`name == "Allow" OR name=="OK"`]');}
private get androidBiometryAlert() {
const regex = '(Please log in|Login with.*)';

Expand Down Expand Up @@ -37,47 +35,21 @@ class Biometrics {
* Allow biometric usage on iOS if it isn't already accepted
*/
async allowIosBiometricUsage() {
// IMPORTANT: The code below is not working as expected
// // When Touch/FaceID is used for the first time it could be that an alert is shown which needs to be accepted
// try {
// await this.iosAllowBiometry.waitForDisplayed({ timeout: 3 * 1000 });
// await this.allowBiometry.click();
// } catch (e) {
// // This means that allow using touch/facID has already been accepted and thus the alert is not shown
// }
// When Touch/FaceID is used for the first time it could be that an alert is shown which needs to be accepted
//
// This is related to a bug in the appium-xcuitest-driver. The issue is that the alert is only shown in the scope outside
// of the app, take for example the home screen (com.apple.springboard)
// See:
// - https://github.com/appium/appium-xcuitest-driver/issues/2311
// - https://github.com/appium/appium-xcuitest-driver/issues/2349
// there are two ways to solve this issue:
// 1. Use the mobile: alert command to accept the alert
// try {
// await driver.waitUntil(async () => {
// const buttons = (await driver.execute('mobile: alert', { action: 'getButtons' })) as string[];
// return buttons.includes('Allow');
// }, { timeout: 3 * 1000 });
// await driver.execute('mobile: alert', { action: 'accept' });
// } catch (e) {
// // This means that allow using touch/facID has already been accepted and thus the alert is not shown
// }
// 2. Use the mobile: launchApp command to go back to the app and accept the alert and go back to the app
// try {
// await driver.execute('mobile: launchApp', { bundleId: 'com.apple.springboard' });
// await this.iosAllowBiometry.waitForDisplayed({ timeout: 3 * 1000 });
// await this.allowBiometry.click();
// await driver.execute('mobile: launchApp', { bundleId: BUNDLE_ID });
// } catch (e) {
// // This means that allow using touch/facID has already been accepted and thus the alert is not shown
// }
//
// We will use the first option and will use a helper to interact with the alert
try {
await NativeAlert.acceptIOSAlertPermissionDialog({ buttonText: 'Allow|OK', timeout: 3 * 1000 });
} catch (e) {
// This means that allow using touch/facID has already been accepted and thus the alert is not shown
}
// NOTE:
// With `appium-xcuitest-driver` V6 and higher this alert can't by default be detected by Appium. To work around this
// we switch to the home screen context and accept the alert there.
// This is a workaround for the issue described here:
// - https://github.com/appium/appium/issues/19716
await executeInHomeScreenContext(async () => {
try {
await this.iosAllowBiometry.waitForDisplayed({ timeout: 3 * 1000 });
await this.allowBiometry.click();
} catch (e) {
// This means that allow using touch/facID has already been accepted and thus the alert is not shown
}
});
}

/**
Expand Down
45 changes: 45 additions & 0 deletions tests/helpers/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,48 @@ export async function relaunchApp(identifier:string) {
await driver.execute(launchCommand, appIdentifier);

}

type AppInfo = {
processArguments: {
env: { [key: string]: any };
args: any[];
};
name: string;
pid: number;
bundleId: string;
};

/**
* Typically, app dialogs are initiated by the application itself and can be interacted with via standard Appium commands. However, there are occasions
* when a dialog is initiated by the operating system, rather than the app. An example of this is the "Touch/Face ID" permission dialog on iOS. This is happening
* with `appium-xcuitest-driver` V6 and higher.
* Since this dialog is outside the app's context, normal Appium interactions within the app context won't work. To interact with such dialogs, a strategy is to switch
* the interaction context to the home screen. The `executeInHomeScreenContext` function is designed for this purpose. For iOS, it temporarily changes the
* interaction context to the home screen (com.apple.springboard), allowing interaction with the system dialog, and then switches back to the original app context
* post-interaction.
* Src: https://appium.github.io/appium-xcuitest-driver/latest/guides/troubleshooting/#interact-with-dialogs-managed-by-comapplespringboard
*/
export async function executeInHomeScreenContext(action:() => Promise<void>): Promise<any> {
// For Android, directly execute the action as this workaround isn't necessary
if (driver.isAndroid) {
return action();
}

// Retrieve the currently active app information
const activeAppInfo: AppInfo = await driver.execute('mobile: activeAppInfo');
// Switch the active context to the iOS home screen
await driver.updateSettings({ 'defaultActiveApplication': 'com.apple.springboard' });
let result;

try {
// Execute the action in the home screen context
result = await action();
} catch (e) {
// Ignore any exceptions during the action
}

// Revert to the original app context
await driver.updateSettings({ 'defaultActiveApplication': activeAppInfo.bundleId });

return result;
}
79 changes: 0 additions & 79 deletions tests/screenobjects/components/NativeAlert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@ const SELECTORS = {
},
};

type IOSAlertPermissionDialog = {buttonAmount?:number, buttonText?:string, timeout?:number}

class NativeAlert {
// IMPORTANT:
// The Alerts/Permissions for iOS have issues. As of `appium-xcuitest-driver` V6 the normal `getAlert`-methods are not working. This is still
// an issue in the latest driver (7.8.2). The issue is that the alert is only shown in the scope outside of the app, take for
// example the home screen (com.apple.springboard)
// See:
// - https://github.com/appium/appium-xcuitest-driver/issues/2311
// - https://github.com/appium/appium-xcuitest-driver/issues/2349

/**
* Wait for the alert to exist.
*
Expand All @@ -30,78 +20,12 @@ class NativeAlert {
? SELECTORS.ANDROID.ALERT_TITLE
: SELECTORS.IOS.ALERT;

// Due to the iOS driver issue we need to wait for the alert in a different way
if (driver.isIOS){
return this.waitForIOSAlertPermissionDialog({ timeout: 11000, buttonAmount: isShown ? 1 : 0 });
}

return $(selector).waitForExist({
timeout: 11000,
reverse: !isShown,
});
}

/**
* Get the buttons of the iOS alert/permission
*/
static async getIOSAlertPermissionDialogButtons(){
return (await driver.execute('mobile: alert', { action: 'getButtons' })) as string[];
}

/**
* Wait for the alert/permission to exist based on it's label, or the amount of buttons
* The buttonText can be a string of a regex so we can check on for example "Allow|OK" which can be different
* between iOS versions
*/
static async waitForIOSAlertPermissionDialog({ buttonAmount, buttonText, timeout = 3000 }:IOSAlertPermissionDialog){
if (!buttonText && buttonAmount === undefined) {
throw new Error('Provide either buttonText or the amount of expected buttons to wait for!');
}

// If the amount of buttons is 0, then we need to wait for the alert to disappear. Normally we can wait for the alert not to exist
// but due to the error for iOS we need to:
// - wait for the alert to exist (even though we know it's not there), we will use 1 second for it
// - get the buttons, which will throw an error if the alert is not there like
// `no such alert: An attempt was made to operate on a modal dialog when one was not open`
// And then return
// If that fails, meaning there are buttons, we will not reach the catch and continue the normal flow which will then throw an error
// because the button amount is bigger than 0 which should not be the case
try {
let buttons = [];
// Check if the alert is shown
await driver.waitUntil(async () => {
try {
// If there is no alert then this will throw an error
buttons = await this.getIOSAlertPermissionDialogButtons();
if (buttons.length > (buttonAmount as number)){
return false;
}
const regex = new RegExp(buttonText as string, 'i');

return buttons.some(button => regex.test(button));
} catch (e) {
// This means that the alert is not shown/available yet. We now need to check if this is as expected
// because we are waiting for the alert to "disappear"
return buttonAmount !== undefined && buttonAmount === 0;
}
}, { timeout });
} catch (e) {
// This means that the alert is not shown
console.log('Alert is not shown = ', e);
if (buttonAmount !== undefined && buttonAmount === 0){
throw new Error('Alert/Permission is still shown');
}
}
}

/**
* Accept the alert/permission based on it's label
*/
static async acceptIOSAlertPermissionDialog({ buttonText, timeout = 3000 }:IOSAlertPermissionDialog){
await this.waitForIOSAlertPermissionDialog({ buttonText, timeout });
await driver.execute('mobile: alert', { action: 'accept' });
}

/**
* Press a button in a cross-platform way.
*
Expand All @@ -116,9 +40,6 @@ class NativeAlert {
const buttonSelector = driver.isAndroid
? SELECTORS.ANDROID.ALERT_BUTTON.replace(/{BUTTON_TEXT}/, selector.toUpperCase())
: `~${selector}`;
if (driver.isIOS) {
return this.acceptIOSAlertPermissionDialog({ buttonText: selector, timeout: 3000 });
}
await $(buttonSelector).click();
}

Expand Down
31 changes: 14 additions & 17 deletions tests/specs/app.biometric.login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import LoginScreen from '../screenobjects/LoginScreen.js';
import Biometrics from '../helpers/Biometrics.js';
import NativeAlert from '../screenobjects/components/NativeAlert.js';
import AndroidSettings from '../screenobjects/AndroidSettings.js';
import { relaunchApp } from '../helpers/Utils.js';
import { executeInHomeScreenContext, relaunchApp } from '../helpers/Utils.js';
import { BUNDLE_ID, PACKAGE_NAME } from '../helpers/Constants.js';

/**
Expand Down Expand Up @@ -69,23 +69,20 @@ describe('WebdriverIO and Appium, when interacting with a biometric button,', ()

// Android doesn't show an alert, but keeps the "use fingerprint" native modal in the screen
if (driver.isIOS) {
// IMPORTANT:
// Due to the iOS driver issue, see:
// -tests/screenobjects/components/NativeAlert.ts
// -tests/helpers/Biometrics.ts
// we can't interact with this specific alert and there is no alternative way. We commented out the code below and added
// a wait for demo purposes.
//
// // Wait for the alert and validate it
// await NativeAlert.waitForIsShown();
// // There's the English and US version of the "Not Recognized|Not Recognised"" text, so we just check for "Not Recogni
// await expect(await NativeAlert.text()).toContain('Not Recogni');
// NOTE:
// With `appium-xcuitest-driver` V6 and higher this alert can't by default be detected by Appium. To work around this
// we switch to the home screen context and accept the alert there.
// This is a workaround for the issue described here:
await executeInHomeScreenContext(async () => {
// Wait for the alert and validate it
await NativeAlert.waitForIsShown();
// There's the English and US version of the "Not Recognized|Not Recognised"" text, so we just check for "Not Recogni
await expect(await NativeAlert.text()).toContain('Not Recogni');

// // Close the alert
// await NativeAlert.topOnButtonWithText('Cancel');

// Wait, this is not a good practice and is only used for demo purposes
await driver.pause(5000);
// Close the alert
await NativeAlert.topOnButtonWithText('Cancel');
await NativeAlert.waitForIsShown(false);
});
} else {
await AndroidSettings.waitAndTap('Cancel');
// @TODO: This takes very long, need to fix this
Expand Down

0 comments on commit 8997b6a

Please sign in to comment.