From f25f7140374bfb519c99a5d8172732481bc6115f Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:13:54 +0100 Subject: [PATCH 1/3] chore: main stack navigator - startup saga synch --- ts/navigation/AppStackNavigator.tsx | 25 ++++++++++++++--- ts/navigation/NavigationService.ts | 12 ++++++++- ts/sagas/startup.ts | 42 ++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/ts/navigation/AppStackNavigator.tsx b/ts/navigation/AppStackNavigator.tsx index b471a8cd648..35c0e5be766 100644 --- a/ts/navigation/AppStackNavigator.tsx +++ b/ts/navigation/AppStackNavigator.tsx @@ -1,5 +1,9 @@ /* eslint-disable functional/immutable-data */ -import { LinkingOptions, NavigationContainer } from "@react-navigation/native"; +import { + LinkingOptions, + NavigationContainer, + NavigationContainerProps +} from "@react-navigation/native"; import * as React from "react"; import { useRef } from "react"; import { View } from "react-native"; @@ -38,10 +42,22 @@ import { useStoredExperimentalDesign } from "../common/context/DSExperimentalCon import { IONavigationLightTheme } from "../theme/navigations"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; import AuthenticatedStackNavigator from "./AuthenticatedStackNavigator"; -import NavigationService, { navigationRef } from "./NavigationService"; +import NavigationService, { + navigationRef, + setMainNavigatorReady +} from "./NavigationService"; import NotAuthenticatedStackNavigator from "./NotAuthenticatedStackNavigator"; import ROUTES from "./routes"; +type OnStateChangeStateType = Parameters< + NonNullable +>[0]; +const isMainNavigatorReady = (state: OnStateChangeStateType) => + state && + state.routes && + state.routes.length > 0 && + state.routes[0].name === ROUTES.MAIN; + export const AppStackNavigator = (): React.ReactElement => { // This hook is used since we are in a child of the Context Provider // to setup the experimental design system value from AsyncStorage @@ -153,7 +169,10 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { NavigationService.setNavigationReady(); routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name; }} - onStateChange={async () => { + onStateChange={async state => { + if (isMainNavigatorReady(state)) { + setMainNavigatorReady(); + } const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.current?.getCurrentRoute()?.name; if (currentRouteName !== undefined) { diff --git a/ts/navigation/NavigationService.ts b/ts/navigation/NavigationService.ts index 31b2bc4d218..3679e48dd66 100644 --- a/ts/navigation/NavigationService.ts +++ b/ts/navigation/NavigationService.ts @@ -10,6 +10,13 @@ export const navigationRef = React.createRef(); // eslint-disable-next-line functional/no-let let isNavigationReady: boolean = false; +// eslint-disable-next-line functional/no-let +let isMainNavigatorReady = false; + +export const setMainNavigatorReady = () => { + isMainNavigatorReady = true; +}; + export const setNavigationReady = () => { // eslint-disable-next-line functional/immutable-data isNavigationReady = true; @@ -44,6 +51,8 @@ const dispatchNavigationAction = (action: NavigationAction) => { navigationRef.current?.dispatch(action); }; +const getIsMainNavigatorReady = () => isMainNavigatorReady; + const getCurrentRouteName = (): string | undefined => navigationRef.current?.getCurrentRoute()?.name; @@ -66,5 +75,6 @@ export default { getCurrentRoute, getCurrentState, getIsNavigationReady, - setNavigationReady + setNavigationReady, + getIsMainNavigatorReady }; diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 652e9a5e3c6..e8d0594bb0d 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -426,13 +426,11 @@ export function* initializeApplicationSaga( const watchAbortOnboardingSagaTask = yield* fork(watchAbortOnboardingSaga); yield* put(startupLoadSuccess(StartupStatusEnum.ONBOARDING)); - // FIXME IOPID-1298: find any better way to handle this - // We need this workaround to let the inner AppStackNavigator stack be ready, - // before continuing with any other navigation action to avoid: - // Error: The 'navigation' object hasn't been initialized yet... - // Here the navigationRef is ready, but because we changed the navigation inner stack - // based on StartupStatusEnum value, we need to wait for the new stack to be ready. - yield* delay(0 as Millisecond); + if (!handleSessionExpiration) { + yield* call(waitForMainNavigator); + } + + // yield* delay(0 as Millisecond); const hasPreviousSessionAndPin = previousSessionToken && O.isSome(maybeStoredPin); if (hasPreviousSessionAndPin && showIdentificationModal) { @@ -517,9 +515,6 @@ export function* initializeApplicationSaga( yield* call(updateInstallationSaga, backendClient.createOrUpdateInstallation); yield* put(startupLoadSuccess(StartupStatusEnum.AUTHENTICATED)); - // FIXME IOPID-1298: find any better way to handle this - // As above for StartupStatusEnum.ONBOARDING - yield* delay(0 as Millisecond); // // User is autenticated, session token is valid // @@ -695,6 +690,33 @@ function* waitForNavigatorServiceInitialization() { }); } +function* waitForMainNavigator() { + // eslint-disable-next-line functional/no-let + let isMainNavReady = yield* call(NavigationService.getIsMainNavigatorReady); + + // eslint-disable-next-line functional/no-let + let timeoutLogged = false; + const startTime = performance.now(); + + // before continuing we must wait for the main navigator tack to be ready + while (!isMainNavReady) { + const elapsedTime = performance.now() - startTime; + if (!timeoutLogged && elapsedTime >= warningWaitNavigatorTime) { + timeoutLogged = true; + + yield* call(mixpanelTrack, "MAIN_NAVIGATOR_STACK_READY_TIMEOUT"); + } + yield* delay(navigatorPollingTime); + isMainNavReady = yield* call(NavigationService.getIsMainNavigatorReady); + } + + const initTime = performance.now() - startTime; + + yield* call(mixpanelTrack, "MAIN_NAVIGATOR_STACK_READY_OK", { + elapsedTime: initTime + }); +} + /** * Remove all the local notifications related to authentication with spid. * From 7942bc94b2d2abf251aa8a5c9d21940fb7bf28a9 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Tue, 23 Jan 2024 23:27:01 +0100 Subject: [PATCH 2/3] better keyboard closing for E2E tests. Replaced reloadReactNative with launchApp --- ts/__e2e__/payment.e2e.ts | 7 ++++--- ts/__e2e__/utils.ts | 10 ++++++++++ ts/features/bonus/cgn/__e2e__/cgn.e2e.ts | 2 +- .../bonus/cgn/components/detail/CgnUnsubscribe.tsx | 7 +------ .../bonus/cgn/components/detail/ToastPatch.e2e.ts | 1 - ts/features/bonus/cgn/components/detail/ToastPatch.ts | 1 - .../euCovidCert/__e2e__/euCovidCertExpired.e2e.ts | 2 +- .../euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts | 2 +- .../euCovidCert/__e2e__/euCovidCertValid.e2e.ts | 2 +- ts/features/messages/__e2e__/messages.e2e.ts | 2 +- .../onboarding/__e2e__/creditCardOnboarding.e2e.ts | 6 +++--- 11 files changed, 23 insertions(+), 19 deletions(-) delete mode 100644 ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts delete mode 100644 ts/features/bonus/cgn/components/detail/ToastPatch.ts diff --git a/ts/__e2e__/payment.e2e.ts b/ts/__e2e__/payment.e2e.ts index 2586a887670..53b6f2553ba 100644 --- a/ts/__e2e__/payment.e2e.ts +++ b/ts/__e2e__/payment.e2e.ts @@ -1,11 +1,11 @@ import I18n from "../i18n"; import { formatNumberCentsToAmount } from "../utils/stringBuilder"; import { e2eWaitRenderTimeout } from "./config"; -import { ensureLoggedIn } from "./utils"; +import { closeKeyboard, ensureLoggedIn } from "./utils"; describe("Payment", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); @@ -106,8 +106,9 @@ describe("Payment", () => { await element(matchNoticeCodeInput).typeText("123123123123123123"); await element(by.id("EntityCodeInputMask")).typeText("12345678901"); + // Close the keyboard - await element(by.label("Fine")).atIndex(0).tap(); + await closeKeyboard(); await element(by.text(I18n.t("global.buttons.continue"))).tap(); diff --git a/ts/__e2e__/utils.ts b/ts/__e2e__/utils.ts index eeca5bb9847..ef0e68658fb 100644 --- a/ts/__e2e__/utils.ts +++ b/ts/__e2e__/utils.ts @@ -117,3 +117,13 @@ export const ensureLoggedIn = async () => { await loginWithSPID(); } }; + +export const closeKeyboard = async () => { + // Sometimes the device ignores the locale set by the detox setup + // In such case we can try to close the keyboard using the english translation + try { + await element(by.label("Fine")).atIndex(0).tap(); + } catch (e) { + await element(by.label("Done")).atIndex(0).tap(); + } +}; diff --git a/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts b/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts index d92b2e4167a..37d9defeadf 100644 --- a/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts +++ b/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts @@ -42,7 +42,7 @@ const activateBonusSuccess = async () => { describe("CGN", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx index 9ef0c1fd4cd..ec0eebcd348 100644 --- a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx +++ b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx @@ -11,7 +11,6 @@ import { isError, isReady } from "../../../../../common/model/RemoteValue"; import { navigateBack } from "../../../../../store/actions/navigation"; import { cgnDetails } from "../../store/actions/details"; import { IOToast } from "../../../../../components/Toast"; -import { skipToastShowingDueToE2ECrash } from "./ToastPatch"; const CgnUnsubscribe = () => { const dispatch = useIODispatch(); @@ -39,11 +38,7 @@ const CgnUnsubscribe = () => { if (isReady(unsubscriptionStatus)) { navigateBack(); dispatch(cgnDetails.request()); - // This is needed to prevent a crash while running E2E tests. Showing - // the toast causes random crashes upon calling device.reloadReactNative - if (!skipToastShowingDueToE2ECrash) { - IOToast.success(I18n.t("bonus.cgn.activation.deactivate.toast")); - } + IOToast.success(I18n.t("bonus.cgn.activation.deactivate.toast")); } if (isError(unsubscriptionStatus) && !isFirstRender.current) { IOToast.error(I18n.t("global.genericError")); diff --git a/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts b/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts deleted file mode 100644 index cd3658899d7..00000000000 --- a/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts +++ /dev/null @@ -1 +0,0 @@ -export const skipToastShowingDueToE2ECrash = true; diff --git a/ts/features/bonus/cgn/components/detail/ToastPatch.ts b/ts/features/bonus/cgn/components/detail/ToastPatch.ts deleted file mode 100644 index b6d723d80f9..00000000000 --- a/ts/features/bonus/cgn/components/detail/ToastPatch.ts +++ /dev/null @@ -1 +0,0 @@ -export const skipToastShowingDueToE2ECrash = false; diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts index 9237521d1ad..84b19fee096 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts @@ -11,7 +11,7 @@ const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; describe("EuCovidCert Expired", () => { beforeAll(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts index 4424b3d2c17..c2de8638b12 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts @@ -11,7 +11,7 @@ const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; describe("EuCovidCert Revoked", () => { beforeAll(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts index 2782e192109..8f2fe2ac19b 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts @@ -13,7 +13,7 @@ const fullScreenQrCodeTestId = "fullScreenQRCode"; describe("EuCovidCert Valid", () => { beforeAll(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/messages/__e2e__/messages.e2e.ts b/ts/features/messages/__e2e__/messages.e2e.ts index 28a723b3d65..a9d2037e1b3 100644 --- a/ts/features/messages/__e2e__/messages.e2e.ts +++ b/ts/features/messages/__e2e__/messages.e2e.ts @@ -4,7 +4,7 @@ import { ensureLoggedIn } from "../../../__e2e__/utils"; describe("Messages Screen", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts index 642a158fd29..1ca4f8d1945 100644 --- a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts +++ b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts @@ -1,10 +1,10 @@ import { e2eWaitRenderTimeout } from "../../../../__e2e__/config"; -import { ensureLoggedIn } from "../../../../__e2e__/utils"; +import { closeKeyboard, ensureLoggedIn } from "../../../../__e2e__/utils"; import I18n from "../../../../i18n"; describe("Credit Card onboarding", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); @@ -51,7 +51,7 @@ describe("Credit Card onboarding", () => { await element(by.id("securityCodeInputMask")).typeText("123"); // Close the keyboard - await element(by.label("Fine")).atIndex(0).tap(); + await closeKeyboard(); await element(by.text(I18n.t("global.buttons.continue"))).tap(); await waitFor(element(by.id("saveOrContinueButton"))) From d77cc5988d838f091dabaf77ce9ef10f7a5eb064 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Tue, 23 Jan 2024 23:44:04 +0100 Subject: [PATCH 3/3] E2E Add credit card by test Id --- ts/components/wallet/WalletHomeHeader.tsx | 1 + .../wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ts/components/wallet/WalletHomeHeader.tsx b/ts/components/wallet/WalletHomeHeader.tsx index f1e9c364215..4098a42100c 100644 --- a/ts/components/wallet/WalletHomeHeader.tsx +++ b/ts/components/wallet/WalletHomeHeader.tsx @@ -48,6 +48,7 @@ export const useWalletHomeHeaderBottomSheet = (): IOBottomSheetModal => { const navigationListItems: ReadonlyArray = [ { title: I18n.t("wallet.paymentMethod"), + testId: "wallet.paymentMethod", subtitle: I18n.t("wallet.paymentMethodDesc"), onPress: () => navigateToWalletAddPaymentMethod({ diff --git a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts index 1ca4f8d1945..195c5d6cffa 100644 --- a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts +++ b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts @@ -24,12 +24,12 @@ describe("Credit Card onboarding", () => { // Button "+ Add" await element(by.id("walletAddNewPaymentMethodTestId")).tap(); - await waitFor(element(by.text(I18n.t("wallet.paymentMethod")))) + await waitFor(element(by.id("wallet.paymentMethod"))) .toBeVisible() .withTimeout(e2eWaitRenderTimeout); // Add payment method listItem in bottomSheet - await element(by.text(I18n.t("wallet.paymentMethod"))).tap(); + await element(by.id("wallet.paymentMethod")).tap(); await waitFor(element(by.text(I18n.t("wallet.methods.card.name")))) .toBeVisible()