diff --git a/KeychainExample/e2e/testCases/accessControlTest.spec.js b/KeychainExample/e2e/testCases/accessControlTest.spec.js index 09f8f5d0..74263cd0 100644 --- a/KeychainExample/e2e/testCases/accessControlTest.spec.js +++ b/KeychainExample/e2e/testCases/accessControlTest.spec.js @@ -1,10 +1,14 @@ -import { by, device, element, expect, waitFor } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; +import { by, device, element, expect } from 'detox'; import { waitForAuthValidity, enterBiometrics, enterPasscode, } from '../utils/authHelpers'; +import { + expectCredentialsLoadedMessage, + expectCredentialsSavedMessage, + expectCredentialsResetMessage, +} from '../utils/statusMessageHelpers'; describe('Access Control', () => { beforeEach(async () => { @@ -28,16 +32,14 @@ describe('Access Control', () => { await enterPasscode(); // Hide keyboard if open await element(by.text('Keychain Example')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(4000); + await expectCredentialsSavedMessage(); await waitForAuthValidity(); await element(by.text('Load')).tap(); await enterPasscode(); // Hide keyboard if open await element(by.text('Keychain Example')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernamePasscode', 'testPasswordPasscode', 'KeystoreAESGCM' @@ -69,15 +71,16 @@ describe('Access Control', () => { await element(by.text('Save')).tap(); await enterBiometrics(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await waitForAuthValidity(); await element(by.text('Load')).tap(); await enterBiometrics(); - await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics'); + await expectCredentialsLoadedMessage( + 'testUsernameBiometrics', + 'testPasswordBiometrics' + ); } ); @@ -91,7 +94,10 @@ describe('Access Control', () => { ).toBeVisible(); await element(by.text('Load')).tap(); await enterBiometrics(); - await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics'); + await expectCredentialsLoadedMessage( + 'testUsernameBiometrics', + 'testPasswordBiometrics' + ); } ); @@ -112,11 +118,12 @@ describe('Access Control', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameAny', 'testPasswordAny'); + await expectCredentialsLoadedMessage( + 'testUsernameAny', + 'testPasswordAny' + ); } ); @@ -129,7 +136,10 @@ describe('Access Control', () => { element(by.text('hasGenericPassword: true')) ).toBeVisible(); await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameAny', 'testPasswordAny'); + await expectCredentialsLoadedMessage( + 'testUsernameAny', + 'testPasswordAny' + ); } ); }); @@ -139,6 +149,6 @@ describe('Access Control', () => { // Hide keyboard await element(by.text('Reset')).tap(); - await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible(); + await expectCredentialsResetMessage(); }); }); diff --git a/KeychainExample/e2e/testCases/securityLevelTest.spec.js b/KeychainExample/e2e/testCases/securityLevelTest.spec.js index 51f2f212..3f51a2bb 100644 --- a/KeychainExample/e2e/testCases/securityLevelTest.spec.js +++ b/KeychainExample/e2e/testCases/securityLevelTest.spec.js @@ -1,5 +1,9 @@ -import { by, device, element, expect, waitFor } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; +import { by, element, expect, device } from 'detox'; +import { + expectCredentialsLoadedMessage, + expectCredentialsSavedMessage, + expectCredentialsResetMessage, +} from '../utils/statusMessageHelpers'; describe(':android:Security Level', () => { beforeEach(async () => { @@ -19,11 +23,9 @@ describe(':android:Security Level', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAny', 'testPasswordAny', undefined, @@ -46,11 +48,9 @@ describe(':android:Security Level', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameSoftware', 'testPasswordSoftware', undefined, @@ -74,11 +74,9 @@ describe(':android:Security Level', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameHardware', 'testPasswordHardware', undefined, @@ -93,6 +91,6 @@ describe(':android:Security Level', () => { // Hide keyboard await element(by.text('Reset')).tap(); - await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible(); + await expectCredentialsResetMessage(); }); }); diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 6e071d49..d6f09557 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,7 +1,12 @@ -import { by, device, element, expect, waitFor } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; +import { by, element, expect, device } from 'detox'; import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers'; +import { + expectCredentialsLoadedMessage, + expectCredentialsSavedMessage, + expectCredentialsResetMessage, +} from '../utils/statusMessageHelpers'; + describe(':android:Storage Types', () => { beforeEach(async () => { await device.launchApp({ newInstance: true }); @@ -20,11 +25,9 @@ describe(':android:Storage Types', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAESCBC', 'testPasswordAESCBC', 'KeystoreAESCBC', @@ -46,13 +49,11 @@ describe(':android:Storage Types', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); await enterBiometrics(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await waitForAuthValidity(); await element(by.text('Load')).tap(); await enterBiometrics(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAESGCM', 'testPasswordAESGCM', 'KeystoreAESGCM', @@ -79,11 +80,9 @@ describe(':android:Storage Types', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(3000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAESGCMNoAuth', 'testPasswordAESGCMNoAuth', 'KeystoreAESGCM_NoAuth', @@ -105,15 +104,10 @@ describe(':android:Storage Types', () => { await expect(element(by.text('Save'))).toBeVisible(); await element(by.text('Save')).tap(); - await waitFor(element(by.text(/^Credentials saved! .*$/))) - .toExist() - .withTimeout(5000); + await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); await enterBiometrics(); - await waitFor(element(by.text(/^Credentials loaded! .*$/))) - .toExist() - .withTimeout(5000); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameRSA', 'testPasswordRSA', 'KeystoreRSAECB', @@ -127,6 +121,6 @@ describe(':android:Storage Types', () => { // Hide keyboard await element(by.text('Reset')).tap(); - await expect(element(by.text(/^Credentials Reset!$/))).toExist(); + await expectCredentialsResetMessage(); }); }); diff --git a/KeychainExample/e2e/utils/authHelpers.js b/KeychainExample/e2e/utils/authHelpers.ts similarity index 75% rename from KeychainExample/e2e/utils/authHelpers.js rename to KeychainExample/e2e/utils/authHelpers.ts index 943a5223..c81cc46d 100644 --- a/KeychainExample/e2e/utils/authHelpers.js +++ b/KeychainExample/e2e/utils/authHelpers.ts @@ -1,9 +1,9 @@ -import { device } from 'detox'; import cp from 'child_process'; +import { device } from 'detox'; // Wait for 5 seconds to ensure auth validity period has expired export const waitForAuthValidity = async () => { - await new Promise((resolve) => setTimeout(resolve, 5500)); // Added 500ms buffer + await new Promise((resolve) => setTimeout(resolve, 5500)); // buffer needed for auth validity period }; export const enterBiometrics = async () => { @@ -12,16 +12,14 @@ export const enterBiometrics = async () => { if (device.getPlatform() === 'android') { await new Promise((resolve) => setTimeout(resolve, 1000)); cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); - await new Promise((resolve) => setTimeout(resolve, 500)); } }; export const enterPasscode = async () => { if (device.getPlatform() === 'android') { - await new Promise((resolve) => setTimeout(resolve, 1500)); + await new Promise((resolve) => setTimeout(resolve, 1000)); cp.spawnSync('adb', ['shell', 'input', 'text', '1111']); - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); cp.spawnSync('adb', ['shell', 'input', 'keyevent', '66']); - await new Promise((resolve) => setTimeout(resolve, 1500)); } }; diff --git a/KeychainExample/e2e/utils/detoxHelpers.ts b/KeychainExample/e2e/utils/detoxHelpers.ts new file mode 100644 index 00000000..14eb7c2b --- /dev/null +++ b/KeychainExample/e2e/utils/detoxHelpers.ts @@ -0,0 +1,36 @@ +import { by, element, waitFor, expect } from 'detox'; + +async function retry( + operation: () => Promise, + maxAttempts: number = 3, + delayMs: number = 1000 +): Promise { + let attempts = 0; + + while (attempts < maxAttempts) { + try { + return await operation(); + } catch (error) { + attempts++; + if (attempts === maxAttempts) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + throw new Error('Unreachable code'); +} + +export async function expectRegexText(regex: RegExp, timeout?: number) { + try { + return await retry(async () => + timeout + ? waitFor(element(by.text(regex))) + .toBeVisible() + .withTimeout(timeout) + : expect(element(by.text(regex))).toBeVisible() + ); + } catch (error) { + throw new Error(`Failed to find text matching ${regex}: ${error}`); + } +} diff --git a/KeychainExample/e2e/utils/matchLoadInfo.ts b/KeychainExample/e2e/utils/matchLoadInfo.ts deleted file mode 100644 index dbbd42a7..00000000 --- a/KeychainExample/e2e/utils/matchLoadInfo.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { by, element, waitFor } from 'detox'; - -export const matchLoadInfo = async ( - username: string, - password: string, - storage?: string, - service?: string -) => { - let regexPattern; - - if (!storage) { - regexPattern = `^Credentials loaded! .*"password":"${password}","username":"${username}"`; - } else { - regexPattern = `^Credentials loaded! .*"storage":"${storage}","password":"${password}","username":"${username}"`; - } - - if (service) { - regexPattern += `,"service":"${service}"`; - } - - regexPattern += '.*$'; - const regex = new RegExp(regexPattern); - await waitFor(element(by.text(regex))) - .toExist() - .withTimeout(3000); -}; diff --git a/KeychainExample/e2e/utils/statusMessageHelpers.ts b/KeychainExample/e2e/utils/statusMessageHelpers.ts new file mode 100644 index 00000000..8832e8d5 --- /dev/null +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -0,0 +1,49 @@ +import { expectRegexText } from './detoxHelpers'; + +const TIMEOUT = 10000; + +function buildLoadedCredentialsRegex( + username: string, + password: string, + storage?: string, + service?: string +): RegExp { + let pattern = '^Credentials loaded! .*'; + // Conditionally add storage if provided. + if (storage) { + pattern += `"storage":"${storage}",`; + } + // Always add password and username. + pattern += `"password":"${password}","username":"${username}"`; + // Conditionally add service if provided. + if (service) { + pattern += `,"service":"${service}"`; + } + pattern += '.*$'; + return new RegExp(pattern); +} + +export async function expectCredentialsSavedMessage() { + const regex = /^Credentials saved! .*$/; + await expectRegexText(regex, TIMEOUT); +} + +export async function expectCredentialsResetMessage() { + const regex = /^Credentials Reset!$/; + await expectRegexText(regex, TIMEOUT); +} + +export async function expectCredentialsLoadedMessage( + username: string, + password: string, + storage?: string, + service?: string +) { + const regex = buildLoadedCredentialsRegex( + username, + password, + storage, + service + ); + await expectRegexText(regex, TIMEOUT); +}