From a148ad41b311f091f160985ab7d2c1f728bba2d8 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 13:11:04 +0000 Subject: [PATCH 01/13] feat: refactor e2e tests to use expectCredentialsSavedMessage helper for consistency --- .../e2e/testCases/accessControlTest.spec.js | 15 +++++-------- .../e2e/testCases/securityLevelTest.spec.js | 15 +++++-------- .../e2e/testCases/storageTypesTest.spec.js | 22 ++++++++----------- KeychainExample/e2e/utils/authHelpers.js | 10 ++++++++- KeychainExample/src/App.tsx | 6 ++++- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/KeychainExample/e2e/testCases/accessControlTest.spec.js b/KeychainExample/e2e/testCases/accessControlTest.spec.js index 09f8f5d0..11bab5fb 100644 --- a/KeychainExample/e2e/testCases/accessControlTest.spec.js +++ b/KeychainExample/e2e/testCases/accessControlTest.spec.js @@ -1,9 +1,10 @@ -import { by, device, element, expect, waitFor } from 'detox'; +import { by, device, element, expect } from 'detox'; import { matchLoadInfo } from '../utils/matchLoadInfo'; import { waitForAuthValidity, enterBiometrics, enterPasscode, + expectCredentialsSavedMessage, } from '../utils/authHelpers'; describe('Access Control', () => { @@ -28,9 +29,7 @@ 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(); @@ -69,9 +68,7 @@ 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(); @@ -112,9 +109,7 @@ 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'); } diff --git a/KeychainExample/e2e/testCases/securityLevelTest.spec.js b/KeychainExample/e2e/testCases/securityLevelTest.spec.js index 51f2f212..b255845c 100644 --- a/KeychainExample/e2e/testCases/securityLevelTest.spec.js +++ b/KeychainExample/e2e/testCases/securityLevelTest.spec.js @@ -1,5 +1,6 @@ -import { by, device, element, expect, waitFor } from 'detox'; +import { by, device, element, expect } from 'detox'; import { matchLoadInfo } from '../utils/matchLoadInfo'; +import { expectCredentialsSavedMessage } from '../utils/authHelpers'; describe(':android:Security Level', () => { beforeEach(async () => { @@ -19,9 +20,7 @@ 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( 'testUsernameAny', @@ -46,9 +45,7 @@ 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( 'testUsernameSoftware', @@ -74,9 +71,7 @@ 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( 'testUsernameHardware', diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 6e071d49..2212a243 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,6 +1,10 @@ import { by, device, element, expect, waitFor } from 'detox'; import { matchLoadInfo } from '../utils/matchLoadInfo'; -import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers'; +import { + enterBiometrics, + expectCredentialsSavedMessage, + waitForAuthValidity, +} from '../utils/authHelpers'; describe(':android:Storage Types', () => { beforeEach(async () => { @@ -20,9 +24,7 @@ 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( 'testUsernameAESCBC', @@ -46,9 +48,7 @@ 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(); @@ -79,9 +79,7 @@ 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( 'testUsernameAESGCMNoAuth', @@ -105,9 +103,7 @@ 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! .*$/))) diff --git a/KeychainExample/e2e/utils/authHelpers.js b/KeychainExample/e2e/utils/authHelpers.js index 943a5223..2ab52a05 100644 --- a/KeychainExample/e2e/utils/authHelpers.js +++ b/KeychainExample/e2e/utils/authHelpers.js @@ -1,5 +1,6 @@ -import { device } from 'detox'; import cp from 'child_process'; +import { by, device, element, expect, waitFor } from 'detox'; +const statusTestID = 'statusMessage'; // Wait for 5 seconds to ensure auth validity period has expired export const waitForAuthValidity = async () => { @@ -25,3 +26,10 @@ export const enterPasscode = async () => { await new Promise((resolve) => setTimeout(resolve, 1500)); } }; + +export async function expectCredentialsSavedMessage() { + await waitFor(element(by.id(statusTestID))).toBeVisible(); + const text = await element(by.id(statusTestID)); + + expect(text).toHaveText(/^Credentials saved! .*$/); +} diff --git a/KeychainExample/src/App.tsx b/KeychainExample/src/App.tsx index 95210951..1cf43788 100644 --- a/KeychainExample/src/App.tsx +++ b/KeychainExample/src/App.tsx @@ -264,7 +264,11 @@ export default function App() { /> )} - {!!status && {status}} + {!!status && ( + + {status} + + )} From 64a1bc0c6044c8fe569cc2fbcca44f7fc785b69a Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 13:25:40 +0000 Subject: [PATCH 02/13] fix: await expect in expectCredentialsSavedMessage for proper async handling --- KeychainExample/e2e/utils/authHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KeychainExample/e2e/utils/authHelpers.js b/KeychainExample/e2e/utils/authHelpers.js index 2ab52a05..c5ee49c6 100644 --- a/KeychainExample/e2e/utils/authHelpers.js +++ b/KeychainExample/e2e/utils/authHelpers.js @@ -31,5 +31,5 @@ export async function expectCredentialsSavedMessage() { await waitFor(element(by.id(statusTestID))).toBeVisible(); const text = await element(by.id(statusTestID)); - expect(text).toHaveText(/^Credentials saved! .*$/); + await expect(text).toHaveText(/^Credentials saved! .*$/); } From faf7e4835b65b52ef935d637d19c614b38c12177 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 14:14:34 +0000 Subject: [PATCH 03/13] feat: replace matchLoadInfo with expectCredentialsLoadedMessage for improved clarity and consistency --- .../e2e/testCases/accessControlTest.spec.js | 31 +++++++--- .../e2e/testCases/securityLevelTest.spec.js | 15 +++-- .../e2e/testCases/storageTypesTest.spec.js | 19 +++--- .../utils/{authHelpers.js => authHelpers.ts} | 10 +--- KeychainExample/e2e/utils/matchLoadInfo.ts | 26 -------- .../e2e/utils/statusMessageHelpers.ts | 59 +++++++++++++++++++ 6 files changed, 102 insertions(+), 58 deletions(-) rename KeychainExample/e2e/utils/{authHelpers.js => authHelpers.ts} (76%) delete mode 100644 KeychainExample/e2e/utils/matchLoadInfo.ts create mode 100644 KeychainExample/e2e/utils/statusMessageHelpers.ts diff --git a/KeychainExample/e2e/testCases/accessControlTest.spec.js b/KeychainExample/e2e/testCases/accessControlTest.spec.js index 11bab5fb..74263cd0 100644 --- a/KeychainExample/e2e/testCases/accessControlTest.spec.js +++ b/KeychainExample/e2e/testCases/accessControlTest.spec.js @@ -1,11 +1,14 @@ import { by, device, element, expect } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; import { waitForAuthValidity, enterBiometrics, enterPasscode, - expectCredentialsSavedMessage, } from '../utils/authHelpers'; +import { + expectCredentialsLoadedMessage, + expectCredentialsSavedMessage, + expectCredentialsResetMessage, +} from '../utils/statusMessageHelpers'; describe('Access Control', () => { beforeEach(async () => { @@ -36,7 +39,7 @@ describe('Access Control', () => { await enterPasscode(); // Hide keyboard if open await element(by.text('Keychain Example')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernamePasscode', 'testPasswordPasscode', 'KeystoreAESGCM' @@ -74,7 +77,10 @@ describe('Access Control', () => { await element(by.text('Load')).tap(); await enterBiometrics(); - await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics'); + await expectCredentialsLoadedMessage( + 'testUsernameBiometrics', + 'testPasswordBiometrics' + ); } ); @@ -88,7 +94,10 @@ describe('Access Control', () => { ).toBeVisible(); await element(by.text('Load')).tap(); await enterBiometrics(); - await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics'); + await expectCredentialsLoadedMessage( + 'testUsernameBiometrics', + 'testPasswordBiometrics' + ); } ); @@ -111,7 +120,10 @@ describe('Access Control', () => { await element(by.text('Save')).tap(); await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameAny', 'testPasswordAny'); + await expectCredentialsLoadedMessage( + 'testUsernameAny', + 'testPasswordAny' + ); } ); @@ -124,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' + ); } ); }); @@ -134,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 b255845c..d20b6028 100644 --- a/KeychainExample/e2e/testCases/securityLevelTest.spec.js +++ b/KeychainExample/e2e/testCases/securityLevelTest.spec.js @@ -1,6 +1,9 @@ import { by, device, element, expect } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; -import { expectCredentialsSavedMessage } from '../utils/authHelpers'; +import { + expectCredentialsLoadedMessage, + expectCredentialsSavedMessage, + expectCredentialsResetMessage, +} from '../utils/statusMessageHelpers'; describe(':android:Security Level', () => { beforeEach(async () => { @@ -22,7 +25,7 @@ describe(':android:Security Level', () => { await element(by.text('Save')).tap(); await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAny', 'testPasswordAny', undefined, @@ -47,7 +50,7 @@ describe(':android:Security Level', () => { await element(by.text('Save')).tap(); await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameSoftware', 'testPasswordSoftware', undefined, @@ -73,7 +76,7 @@ describe(':android:Security Level', () => { await element(by.text('Save')).tap(); await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameHardware', 'testPasswordHardware', undefined, @@ -88,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 2212a243..4333f7d0 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,10 +1,11 @@ import { by, device, element, expect, waitFor } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; +import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers'; + import { - enterBiometrics, + expectCredentialsLoadedMessage, expectCredentialsSavedMessage, - waitForAuthValidity, -} from '../utils/authHelpers'; + expectCredentialsResetMessage, +} from '../utils/statusMessageHelpers'; describe(':android:Storage Types', () => { beforeEach(async () => { @@ -26,7 +27,7 @@ describe(':android:Storage Types', () => { await element(by.text('Save')).tap(); await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAESCBC', 'testPasswordAESCBC', 'KeystoreAESCBC', @@ -52,7 +53,7 @@ describe(':android:Storage Types', () => { await waitForAuthValidity(); await element(by.text('Load')).tap(); await enterBiometrics(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAESGCM', 'testPasswordAESGCM', 'KeystoreAESGCM', @@ -81,7 +82,7 @@ describe(':android:Storage Types', () => { await element(by.text('Save')).tap(); await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameAESGCMNoAuth', 'testPasswordAESGCMNoAuth', 'KeystoreAESGCM_NoAuth', @@ -109,7 +110,7 @@ describe(':android:Storage Types', () => { await waitFor(element(by.text(/^Credentials loaded! .*$/))) .toExist() .withTimeout(5000); - await matchLoadInfo( + await expectCredentialsLoadedMessage( 'testUsernameRSA', 'testPasswordRSA', 'KeystoreRSAECB', @@ -123,6 +124,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 76% rename from KeychainExample/e2e/utils/authHelpers.js rename to KeychainExample/e2e/utils/authHelpers.ts index c5ee49c6..39121467 100644 --- a/KeychainExample/e2e/utils/authHelpers.js +++ b/KeychainExample/e2e/utils/authHelpers.ts @@ -1,6 +1,5 @@ import cp from 'child_process'; -import { by, device, element, expect, waitFor } from 'detox'; -const statusTestID = 'statusMessage'; +import { device } from 'detox'; // Wait for 5 seconds to ensure auth validity period has expired export const waitForAuthValidity = async () => { @@ -26,10 +25,3 @@ export const enterPasscode = async () => { await new Promise((resolve) => setTimeout(resolve, 1500)); } }; - -export async function expectCredentialsSavedMessage() { - await waitFor(element(by.id(statusTestID))).toBeVisible(); - const text = await element(by.id(statusTestID)); - - await expect(text).toHaveText(/^Credentials saved! .*$/); -} 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..112e0c2f --- /dev/null +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -0,0 +1,59 @@ +import { by, element, expect, waitFor } from 'detox'; +const statusTestID = 'statusMessage'; + +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); +} + +async function expectCredentialsMessage() { + await waitFor(element(by.id(statusTestID))).toBeVisible(); + return element(by.id(statusTestID)); +} + +export async function expectCredentialsSavedMessage() { + const text = await expectCredentialsMessage(); + const regex = /^Credentials saved! .*$/; + // @ts-expect-error - regex pattern is not recognized by TS + await expect(text).toHaveText(regex); +} + +export async function expectCredentialsResetMessage() { + const text = await expectCredentialsMessage(); + const regex = /^Credentials Reset!$/; + // @ts-expect-error - regex pattern is not recognized by TS + await expect(text).toHaveText(regex); +} + +export async function expectCredentialsLoadedMessage( + username: string, + password: string, + storage?: string, + service?: string +) { + const text = await expectCredentialsMessage(); + const regex = buildLoadedCredentialsRegex( + username, + password, + storage, + service + ); + // @ts-expect-error - regex pattern is not recognized by TS + await expect(text).toHaveText(regex); +} From 89d7f0479f217e7833383e617ef9e3a72e3b3586 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 14:29:55 +0000 Subject: [PATCH 04/13] refactor: handle expecting regex per platform --- .../e2e/utils/statusMessageHelpers.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/KeychainExample/e2e/utils/statusMessageHelpers.ts b/KeychainExample/e2e/utils/statusMessageHelpers.ts index 112e0c2f..fc7b4915 100644 --- a/KeychainExample/e2e/utils/statusMessageHelpers.ts +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -27,18 +27,25 @@ async function expectCredentialsMessage() { return element(by.id(statusTestID)); } +async function expectRegexText(regex: RegExp) { + // toHaveText does not support regex on iOS + // by.text(regex) is flakey on Android + if (device.getPlatform() === 'android') { + const text = await expectCredentialsMessage(); + // @ts-expect-error - regex pattern is not recognized by TS + await expect(text).toHaveText(regex); + } + await expect(element(by.text(regex))).toBeVisible(); +} + export async function expectCredentialsSavedMessage() { - const text = await expectCredentialsMessage(); const regex = /^Credentials saved! .*$/; - // @ts-expect-error - regex pattern is not recognized by TS - await expect(text).toHaveText(regex); + await expectRegexText(regex); } export async function expectCredentialsResetMessage() { - const text = await expectCredentialsMessage(); const regex = /^Credentials Reset!$/; - // @ts-expect-error - regex pattern is not recognized by TS - await expect(text).toHaveText(regex); + expectRegexText(regex); } export async function expectCredentialsLoadedMessage( @@ -47,13 +54,11 @@ export async function expectCredentialsLoadedMessage( storage?: string, service?: string ) { - const text = await expectCredentialsMessage(); const regex = buildLoadedCredentialsRegex( username, password, storage, service ); - // @ts-expect-error - regex pattern is not recognized by TS - await expect(text).toHaveText(regex); + expectRegexText(regex); } From d1ebb0fa02503e9f831f305371230bfef8b748dd Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 14:52:19 +0000 Subject: [PATCH 05/13] fix: add return statement in expectRegexText --- KeychainExample/e2e/utils/statusMessageHelpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/KeychainExample/e2e/utils/statusMessageHelpers.ts b/KeychainExample/e2e/utils/statusMessageHelpers.ts index fc7b4915..7c552470 100644 --- a/KeychainExample/e2e/utils/statusMessageHelpers.ts +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -34,6 +34,7 @@ async function expectRegexText(regex: RegExp) { const text = await expectCredentialsMessage(); // @ts-expect-error - regex pattern is not recognized by TS await expect(text).toHaveText(regex); + return; } await expect(element(by.text(regex))).toBeVisible(); } From 72d7307fe82fcae2f4d1806aa9a48e8610989a74 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 15:24:49 +0000 Subject: [PATCH 06/13] feat: implement ResetDevice helper for consistent app reset in e2e tests --- KeychainExample/e2e/testCases/accessControlTest.spec.js | 3 ++- KeychainExample/e2e/testCases/securityLevelTest.spec.js | 5 +++-- KeychainExample/e2e/testCases/storageTypesTest.spec.js | 5 +++-- KeychainExample/e2e/utils/detoxHelpers.ts | 3 +++ 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 KeychainExample/e2e/utils/detoxHelpers.ts diff --git a/KeychainExample/e2e/testCases/accessControlTest.spec.js b/KeychainExample/e2e/testCases/accessControlTest.spec.js index 74263cd0..93adee1c 100644 --- a/KeychainExample/e2e/testCases/accessControlTest.spec.js +++ b/KeychainExample/e2e/testCases/accessControlTest.spec.js @@ -9,10 +9,11 @@ import { expectCredentialsSavedMessage, expectCredentialsResetMessage, } from '../utils/statusMessageHelpers'; +import { ResetDevice } from '../utils/detoxHelpers'; describe('Access Control', () => { beforeEach(async () => { - await device.launchApp({ newInstance: true }); + await ResetDevice(); }); ['genericPassword', 'internetCredentials'].forEach((type) => { it( diff --git a/KeychainExample/e2e/testCases/securityLevelTest.spec.js b/KeychainExample/e2e/testCases/securityLevelTest.spec.js index d20b6028..ba7205f2 100644 --- a/KeychainExample/e2e/testCases/securityLevelTest.spec.js +++ b/KeychainExample/e2e/testCases/securityLevelTest.spec.js @@ -1,13 +1,14 @@ -import { by, device, element, expect } from 'detox'; +import { by, element, expect } from 'detox'; import { expectCredentialsLoadedMessage, expectCredentialsSavedMessage, expectCredentialsResetMessage, } from '../utils/statusMessageHelpers'; +import { ResetDevice } from '../utils/detoxHelpers'; describe(':android:Security Level', () => { beforeEach(async () => { - await device.launchApp({ newInstance: true }); + await ResetDevice(); }); ['genericPassword', 'internetCredentials'].forEach((type) => { it(':android:should save with Any security level - ' + type, async () => { diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 4333f7d0..287e1048 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,4 +1,4 @@ -import { by, device, element, expect, waitFor } from 'detox'; +import { by, element, expect, waitFor } from 'detox'; import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers'; import { @@ -6,10 +6,11 @@ import { expectCredentialsSavedMessage, expectCredentialsResetMessage, } from '../utils/statusMessageHelpers'; +import { ResetDevice } from '../utils/detoxHelpers'; describe(':android:Storage Types', () => { beforeEach(async () => { - await device.launchApp({ newInstance: true }); + await ResetDevice(); }); ['genericPassword', 'internetCredentials'].forEach((type) => { it(':android:should save with AES_CBC storage - ' + type, async () => { diff --git a/KeychainExample/e2e/utils/detoxHelpers.ts b/KeychainExample/e2e/utils/detoxHelpers.ts new file mode 100644 index 00000000..ba60731f --- /dev/null +++ b/KeychainExample/e2e/utils/detoxHelpers.ts @@ -0,0 +1,3 @@ +export const ResetDevice = async () => { + await device.launchApp({ delete: true, newInstance: true }); +}; From 9073931fc15dffb5580391166aa75df959d14e2b Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 15:30:37 +0000 Subject: [PATCH 07/13] refactor: simplify removing platform-based expectRegexTest - due to `ResetDevice` properly resetting our tests --- KeychainExample/e2e/utils/detoxHelpers.ts | 6 +++++ .../e2e/utils/statusMessageHelpers.ts | 26 +++---------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/KeychainExample/e2e/utils/detoxHelpers.ts b/KeychainExample/e2e/utils/detoxHelpers.ts index ba60731f..23e0caf0 100644 --- a/KeychainExample/e2e/utils/detoxHelpers.ts +++ b/KeychainExample/e2e/utils/detoxHelpers.ts @@ -1,3 +1,9 @@ +import { by, element, expect, device } from 'detox'; + export const ResetDevice = async () => { await device.launchApp({ delete: true, newInstance: true }); }; + +export function expectRegexText(regex: RegExp) { + return expect(element(by.text(regex))); +} diff --git a/KeychainExample/e2e/utils/statusMessageHelpers.ts b/KeychainExample/e2e/utils/statusMessageHelpers.ts index 7c552470..3bc20e2c 100644 --- a/KeychainExample/e2e/utils/statusMessageHelpers.ts +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -1,5 +1,4 @@ -import { by, element, expect, waitFor } from 'detox'; -const statusTestID = 'statusMessage'; +import { expectRegexText } from './detoxHelpers'; function buildLoadedCredentialsRegex( username: string, @@ -22,31 +21,14 @@ function buildLoadedCredentialsRegex( return new RegExp(pattern); } -async function expectCredentialsMessage() { - await waitFor(element(by.id(statusTestID))).toBeVisible(); - return element(by.id(statusTestID)); -} - -async function expectRegexText(regex: RegExp) { - // toHaveText does not support regex on iOS - // by.text(regex) is flakey on Android - if (device.getPlatform() === 'android') { - const text = await expectCredentialsMessage(); - // @ts-expect-error - regex pattern is not recognized by TS - await expect(text).toHaveText(regex); - return; - } - await expect(element(by.text(regex))).toBeVisible(); -} - export async function expectCredentialsSavedMessage() { const regex = /^Credentials saved! .*$/; - await expectRegexText(regex); + await expectRegexText(regex).toBeVisible(); } export async function expectCredentialsResetMessage() { const regex = /^Credentials Reset!$/; - expectRegexText(regex); + await expectRegexText(regex).toBeVisible(); } export async function expectCredentialsLoadedMessage( @@ -61,5 +43,5 @@ export async function expectCredentialsLoadedMessage( storage, service ); - expectRegexText(regex); + await expectRegexText(regex).toBeVisible(); } From 480cfe31d0a9b3dabfbbf402595ab7d14ff10569 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 15:54:48 +0000 Subject: [PATCH 08/13] refactor: replace expectRegexText with waitForRegexText and revert delete on reset --- KeychainExample/e2e/utils/detoxHelpers.ts | 12 ++++++++---- KeychainExample/e2e/utils/statusMessageHelpers.ts | 10 ++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/KeychainExample/e2e/utils/detoxHelpers.ts b/KeychainExample/e2e/utils/detoxHelpers.ts index 23e0caf0..78c7f366 100644 --- a/KeychainExample/e2e/utils/detoxHelpers.ts +++ b/KeychainExample/e2e/utils/detoxHelpers.ts @@ -1,9 +1,13 @@ -import { by, element, expect, device } from 'detox'; +import { by, element, waitFor, device } from 'detox'; export const ResetDevice = async () => { - await device.launchApp({ delete: true, newInstance: true }); + await device.launchApp({ newInstance: true }); }; -export function expectRegexText(regex: RegExp) { - return expect(element(by.text(regex))); +export function waitForRegexText(regex: RegExp, timeout?: number) { + return timeout + ? waitFor(element(by.text(regex))) + .toBeVisible() + .withTimeout(timeout) + : waitFor(element(by.text(regex))).toBeVisible(); } diff --git a/KeychainExample/e2e/utils/statusMessageHelpers.ts b/KeychainExample/e2e/utils/statusMessageHelpers.ts index 3bc20e2c..ce90e7fe 100644 --- a/KeychainExample/e2e/utils/statusMessageHelpers.ts +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -1,4 +1,6 @@ -import { expectRegexText } from './detoxHelpers'; +import { waitForRegexText } from './detoxHelpers'; + +const TIMEOUT = 10000; function buildLoadedCredentialsRegex( username: string, @@ -23,12 +25,12 @@ function buildLoadedCredentialsRegex( export async function expectCredentialsSavedMessage() { const regex = /^Credentials saved! .*$/; - await expectRegexText(regex).toBeVisible(); + await waitForRegexText(regex, TIMEOUT); } export async function expectCredentialsResetMessage() { const regex = /^Credentials Reset!$/; - await expectRegexText(regex).toBeVisible(); + await waitForRegexText(regex, TIMEOUT); } export async function expectCredentialsLoadedMessage( @@ -43,5 +45,5 @@ export async function expectCredentialsLoadedMessage( storage, service ); - await expectRegexText(regex).toBeVisible(); + await waitForRegexText(regex, TIMEOUT); } From ee97bc9e9473c833208a30ec316dd7d3cd5f55bd Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 16:06:39 +0000 Subject: [PATCH 09/13] refactor: implement expectRegexText with retry logic for improved reliability --- KeychainExample/e2e/utils/detoxHelpers.ts | 41 +++++++++++++++---- .../e2e/utils/statusMessageHelpers.ts | 8 ++-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/KeychainExample/e2e/utils/detoxHelpers.ts b/KeychainExample/e2e/utils/detoxHelpers.ts index 78c7f366..aaa1257d 100644 --- a/KeychainExample/e2e/utils/detoxHelpers.ts +++ b/KeychainExample/e2e/utils/detoxHelpers.ts @@ -1,13 +1,40 @@ -import { by, element, waitFor, device } from 'detox'; +import { by, element, waitFor, device, expect } from 'detox'; export const ResetDevice = async () => { await device.launchApp({ newInstance: true }); }; -export function waitForRegexText(regex: RegExp, timeout?: number) { - return timeout - ? waitFor(element(by.text(regex))) - .toBeVisible() - .withTimeout(timeout) - : waitFor(element(by.text(regex))).toBeVisible(); +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/statusMessageHelpers.ts b/KeychainExample/e2e/utils/statusMessageHelpers.ts index ce90e7fe..8832e8d5 100644 --- a/KeychainExample/e2e/utils/statusMessageHelpers.ts +++ b/KeychainExample/e2e/utils/statusMessageHelpers.ts @@ -1,4 +1,4 @@ -import { waitForRegexText } from './detoxHelpers'; +import { expectRegexText } from './detoxHelpers'; const TIMEOUT = 10000; @@ -25,12 +25,12 @@ function buildLoadedCredentialsRegex( export async function expectCredentialsSavedMessage() { const regex = /^Credentials saved! .*$/; - await waitForRegexText(regex, TIMEOUT); + await expectRegexText(regex, TIMEOUT); } export async function expectCredentialsResetMessage() { const regex = /^Credentials Reset!$/; - await waitForRegexText(regex, TIMEOUT); + await expectRegexText(regex, TIMEOUT); } export async function expectCredentialsLoadedMessage( @@ -45,5 +45,5 @@ export async function expectCredentialsLoadedMessage( storage, service ); - await waitForRegexText(regex, TIMEOUT); + await expectRegexText(regex, TIMEOUT); } From b81051503f4a69192a2247a0c634217e68824740 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 17:14:52 +0000 Subject: [PATCH 10/13] refactor: streamline wait times in authHelpers --- KeychainExample/e2e/testCases/storageTypesTest.spec.js | 5 +---- KeychainExample/e2e/utils/authHelpers.ts | 10 ++++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 287e1048..28f53415 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,4 +1,4 @@ -import { by, element, expect, waitFor } from 'detox'; +import { by, element, expect } from 'detox'; import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers'; import { @@ -108,9 +108,6 @@ describe(':android:Storage Types', () => { await expectCredentialsSavedMessage(); await element(by.text('Load')).tap(); await enterBiometrics(); - await waitFor(element(by.text(/^Credentials loaded! .*$/))) - .toExist() - .withTimeout(5000); await expectCredentialsLoadedMessage( 'testUsernameRSA', 'testPasswordRSA', diff --git a/KeychainExample/e2e/utils/authHelpers.ts b/KeychainExample/e2e/utils/authHelpers.ts index 39121467..859f5bb8 100644 --- a/KeychainExample/e2e/utils/authHelpers.ts +++ b/KeychainExample/e2e/utils/authHelpers.ts @@ -3,25 +3,23 @@ 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 () => { // Biometric prompt is not available in the IOS simulator // https://github.com/oblador/react-native-keychain/issues/340 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)); + cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); } }; export const enterPasscode = async () => { if (device.getPlatform() === 'android') { - await new Promise((resolve) => setTimeout(resolve, 1500)); + await new Promise((resolve) => setTimeout(resolve, 500)); cp.spawnSync('adb', ['shell', 'input', 'text', '1111']); - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 500)); cp.spawnSync('adb', ['shell', 'input', 'keyevent', '66']); - await new Promise((resolve) => setTimeout(resolve, 1500)); } }; From 4e6a47babda87dbdeb47111cc70c2bca0c306542 Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 17:38:21 +0000 Subject: [PATCH 11/13] revert: ResetDevice helper --- KeychainExample/e2e/testCases/accessControlTest.spec.js | 3 +-- KeychainExample/e2e/testCases/securityLevelTest.spec.js | 5 ++--- KeychainExample/e2e/testCases/storageTypesTest.spec.js | 5 ++--- KeychainExample/e2e/utils/detoxHelpers.ts | 6 +----- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/KeychainExample/e2e/testCases/accessControlTest.spec.js b/KeychainExample/e2e/testCases/accessControlTest.spec.js index 93adee1c..74263cd0 100644 --- a/KeychainExample/e2e/testCases/accessControlTest.spec.js +++ b/KeychainExample/e2e/testCases/accessControlTest.spec.js @@ -9,11 +9,10 @@ import { expectCredentialsSavedMessage, expectCredentialsResetMessage, } from '../utils/statusMessageHelpers'; -import { ResetDevice } from '../utils/detoxHelpers'; describe('Access Control', () => { beforeEach(async () => { - await ResetDevice(); + await device.launchApp({ newInstance: true }); }); ['genericPassword', 'internetCredentials'].forEach((type) => { it( diff --git a/KeychainExample/e2e/testCases/securityLevelTest.spec.js b/KeychainExample/e2e/testCases/securityLevelTest.spec.js index ba7205f2..3f51a2bb 100644 --- a/KeychainExample/e2e/testCases/securityLevelTest.spec.js +++ b/KeychainExample/e2e/testCases/securityLevelTest.spec.js @@ -1,14 +1,13 @@ -import { by, element, expect } from 'detox'; +import { by, element, expect, device } from 'detox'; import { expectCredentialsLoadedMessage, expectCredentialsSavedMessage, expectCredentialsResetMessage, } from '../utils/statusMessageHelpers'; -import { ResetDevice } from '../utils/detoxHelpers'; describe(':android:Security Level', () => { beforeEach(async () => { - await ResetDevice(); + await device.launchApp({ newInstance: true }); }); ['genericPassword', 'internetCredentials'].forEach((type) => { it(':android:should save with Any security level - ' + type, async () => { diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 28f53415..d6f09557 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,4 +1,4 @@ -import { by, element, expect } from 'detox'; +import { by, element, expect, device } from 'detox'; import { enterBiometrics, waitForAuthValidity } from '../utils/authHelpers'; import { @@ -6,11 +6,10 @@ import { expectCredentialsSavedMessage, expectCredentialsResetMessage, } from '../utils/statusMessageHelpers'; -import { ResetDevice } from '../utils/detoxHelpers'; describe(':android:Storage Types', () => { beforeEach(async () => { - await ResetDevice(); + await device.launchApp({ newInstance: true }); }); ['genericPassword', 'internetCredentials'].forEach((type) => { it(':android:should save with AES_CBC storage - ' + type, async () => { diff --git a/KeychainExample/e2e/utils/detoxHelpers.ts b/KeychainExample/e2e/utils/detoxHelpers.ts index aaa1257d..14eb7c2b 100644 --- a/KeychainExample/e2e/utils/detoxHelpers.ts +++ b/KeychainExample/e2e/utils/detoxHelpers.ts @@ -1,8 +1,4 @@ -import { by, element, waitFor, device, expect } from 'detox'; - -export const ResetDevice = async () => { - await device.launchApp({ newInstance: true }); -}; +import { by, element, waitFor, expect } from 'detox'; async function retry( operation: () => Promise, From 65ab40235296b79037a56ab6d6e61a89b536905e Mon Sep 17 00:00:00 2001 From: Bowlerr Date: Mon, 24 Mar 2025 18:00:03 +0000 Subject: [PATCH 12/13] fix: increase wait times in biometrics and passcode entry for improved reliability --- KeychainExample/e2e/utils/authHelpers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/KeychainExample/e2e/utils/authHelpers.ts b/KeychainExample/e2e/utils/authHelpers.ts index 859f5bb8..c81cc46d 100644 --- a/KeychainExample/e2e/utils/authHelpers.ts +++ b/KeychainExample/e2e/utils/authHelpers.ts @@ -10,16 +10,16 @@ export const enterBiometrics = async () => { // Biometric prompt is not available in the IOS simulator // https://github.com/oblador/react-native-keychain/issues/340 if (device.getPlatform() === 'android') { - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 1000)); cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); } }; export const enterPasscode = async () => { if (device.getPlatform() === 'android') { - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 1000)); cp.spawnSync('adb', ['shell', 'input', 'text', '1111']); - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 1000)); cp.spawnSync('adb', ['shell', 'input', 'keyevent', '66']); } }; From 0f5f3597346c72583699ceac84e952d2791457c5 Mon Sep 17 00:00:00 2001 From: MazurDorian Date: Sun, 25 May 2025 00:36:16 +0200 Subject: [PATCH 13/13] chore: remove testId --- KeychainExample/src/App.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/KeychainExample/src/App.tsx b/KeychainExample/src/App.tsx index 1cf43788..95210951 100644 --- a/KeychainExample/src/App.tsx +++ b/KeychainExample/src/App.tsx @@ -264,11 +264,7 @@ export default function App() { /> )} - {!!status && ( - - {status} - - )} + {!!status && {status}}