From 0145a4296c0552a272e3e665b282334af1d90fbe Mon Sep 17 00:00:00 2001 From: Sanskar Atrey Date: Thu, 26 Dec 2024 16:11:10 +0530 Subject: [PATCH] feat: added react native detox framework for e2e automation (#165) --- .detoxrc.js | 99 +++++++++++++++ .en | 1 + .../workflows/android-detox-automation.yml | 116 ++++++++++++++++++ .github/workflows/iOS-detox-automation.yml | 111 +++++++++++++++++ .gitignore | 7 +- android | 2 +- detox-tests/babel.config.js | 8 ++ detox-tests/e2e/card-flow-e2e.test.ts | 56 +++++++++ detox-tests/e2e/jest.config.js | 17 +++ detox-tests/fixtures/Constants.ts | 8 ++ detox-tests/package.json | 17 +++ detox-tests/tsconfig.json | 9 ++ detox-tests/utils/DetoxHelpers.ts | 12 ++ ios | 2 +- package.json | 12 +- server.js | 24 ++-- src/components/common/CustomButton.res | 2 + src/components/common/CustomInput.res | 2 + src/components/elements/PaymentSheetUi.res | 3 + src/pages/payment/ConfirmButtonAnimation.res | 1 + src/utility/test/TestUtils.res | 4 + 21 files changed, 499 insertions(+), 14 deletions(-) create mode 100644 .detoxrc.js create mode 100644 .github/workflows/android-detox-automation.yml create mode 100644 .github/workflows/iOS-detox-automation.yml create mode 100644 detox-tests/babel.config.js create mode 100644 detox-tests/e2e/card-flow-e2e.test.ts create mode 100644 detox-tests/e2e/jest.config.js create mode 100644 detox-tests/fixtures/Constants.ts create mode 100644 detox-tests/package.json create mode 100644 detox-tests/tsconfig.json create mode 100644 detox-tests/utils/DetoxHelpers.ts create mode 100644 src/utility/test/TestUtils.res diff --git a/.detoxrc.js b/.detoxrc.js new file mode 100644 index 00000000..8caaa48b --- /dev/null +++ b/.detoxrc.js @@ -0,0 +1,99 @@ +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + $0: 'jest', + config: 'detox-tests/e2e/jest.config.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: + 'ios/build/Build/Products/Debug-iphonesimulator/HyperSwitch.app', + build: + 'xcodebuild -workspace ios/hyperswitch.xcworkspace -scheme hyperswitch -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', + }, + 'ios.release': { + type: 'ios.app', + binaryPath: + 'ios/build/Build/Products/Release-iphonesimulator/YOUR_APP.app', + build: + 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/demo-app/build/outputs/apk/debug/demo-app-debug.apk', + testBinaryPath: + 'android/demo-app/build/outputs/apk/androidTest/debug/demo-app-debug-androidTest.apk', + build: + 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', + reversePorts: [8081], + }, + 'android.release': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', + build: + 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', + }, + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: 'iPhone 15', + }, + }, + attached: { + type: 'android.attached', + device: { + adbName: '.*', + }, + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_3a_API_34_extension_level_7_arm64-v8a', + }, + }, + ciEmulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_API_29_AOSP', + }, + }, + }, + configurations: { + 'ios.sim.debug': { + device: 'simulator', + app: 'ios.debug', + }, + 'ios.sim.release': { + device: 'simulator', + app: 'ios.release', + }, + 'android.att.debug': { + device: 'attached', + app: 'android.debug', + }, + 'android.att.release': { + device: 'attached', + app: 'android.release', + }, + 'android.emu.debug': { + device: 'emulator', + app: 'android.debug', + }, + 'android.emu.ci.debug': { + device: 'ciEmulator', + app: 'android.debug', + }, + 'android.emu.release': { + device: 'emulator', + app: 'android.release', + }, + }, +}; diff --git a/.en b/.en index 7dd46777..2651c597 100644 --- a/.en +++ b/.en @@ -3,3 +3,4 @@ HYPERSWITCH_SECRET_KEY="" HYPERSWITCH_SENTRY_DSN="" HYPERSWITCH_CODEPUSH_ANDROID_KEY="" HYPERSWITCH_CODEPUSH_IOS_KEY="" +PROFILE_ID="" \ No newline at end of file diff --git a/.github/workflows/android-detox-automation.yml b/.github/workflows/android-detox-automation.yml new file mode 100644 index 00000000..5870927f --- /dev/null +++ b/.github/workflows/android-detox-automation.yml @@ -0,0 +1,116 @@ +# .github/workflows/e2e-android.yml +name: e2e-android +on: push + +jobs: + e2e-android: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Free Disk Space + run: | + sudo rm -rf /opt/hostedtoolcache + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + # cache: 'npm' + # cache-dependency-path: '**/package-lock.json' + + - name: Install dependencies + run: npm install + + - name: Create .env file + run: | + touch .env + echo ${{ secrets.HYPERSWITCH_PUBLISHABLE_KEY }} + echo STATIC_DIR = ./dist >> .env + echo HYPERSWITCH_PUBLISHABLE_KEY = ${{ secrets.HYPERSWITCH_PUBLISHABLE_KEY }}>> .env + echo HYPERSWITCH_SECRET_KEY = ${{ secrets.HYPERSWITCH_SECRET_KEY }}>> .env + echo PROFILE_ID = ${{ secrets.PROFILE_ID }}>> .env + + - name: Start server + run: | + nohup node server.js & + + - name: Check Server + run: | + curl http://localhost:5252/create-payment-intent + + - name: Setup Java + uses: actions/setup-java@v3 + with: + cache: gradle + distribution: temurin + java-version: 17 + + # - name: Cache Detox build + # id: cache-detox-build + # uses: actions/cache@v3 + # with: + # path: android/app/build + # key: ${{ runner.os }}-detox-build + # restore-keys: | + # ${{ runner.os }}-detox-build + + # - name: List branches + # run: | + # cd android + # git fetch --all + # git branch -r + + - name: Checkout Android Repo + uses: actions/checkout@v4 + with: + repository: juspay/hyperswitch-sdk-android + ref: detox-poc + path: android + fetch-depth: 0 + + - name: Check Android branch + run: | + cd android && git branch && cd .. + + - name: Generate & Supply JS Bundle to Test APK + run: | + npm run re:start && npm run bundle:android + + - name: Detox build + run: cd android && ls && cd .. && npx detox build --configuration android.emu.ci.debug + + - name: Get device name + id: device + run: node -e "console.log('AVD_NAME=' + require('./.detoxrc').devices.ciEmulator.device.avdName)" >> $GITHUB_OUTPUT + + - name: Check Android branch + run: | + cd android && git branch && cd .. + + - name: Detox test + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 30 + target: default + arch: x86_64 + profile: pixel + avd-name: ${{ steps.device.outputs.AVD_NAME }} + script: npx detox test --configuration android.emu.ci.debug --headless --loglevel trace --record-logs all + + - name: Upload artifacts + if: failure() + uses: actions/upload-artifact@v3 + with: + name: detox-artifacts + path: artifacts diff --git a/.github/workflows/iOS-detox-automation.yml b/.github/workflows/iOS-detox-automation.yml new file mode 100644 index 00000000..27023f2f --- /dev/null +++ b/.github/workflows/iOS-detox-automation.yml @@ -0,0 +1,111 @@ +# .github/workflows/e2e-ios.yml +name: e2e-ios +on: push + +jobs: + e2e-ios: + runs-on: macos-14 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Checkout iOS Repo + uses: actions/checkout@v4 + with: + repository: juspay/hyperswitch-sdk-ios + ref: main + path: ios + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Dependency Install + run: | + jq 'del(.resolutions) | + del(.dependencies["react-native-hyperswitch-netcetera-3ds"]) | + del(.dependencies["react-native-hyperswitch-scancard"])' package.json > package.json.tmp && + mv package.json.tmp package.json + npm install + + - name: Create .env file + run: | + touch .env + echo ${{ secrets.HYPERSWITCH_PUBLISHABLE_KEY }} + echo STATIC_DIR = ./dist >> .env + echo HYPERSWITCH_PUBLISHABLE_KEY = ${{ secrets.HYPERSWITCH_PUBLISHABLE_KEY }}>> .env + echo HYPERSWITCH_SECRET_KEY = ${{ secrets.HYPERSWITCH_SECRET_KEY }}>> .env + echo PROFILE_ID = ${{ secrets.PROFILE_ID }}>> .env + + - name: Start server + run: | + nohup node server.js & + + - name: Check Server + run: | + curl http://localhost:5252/create-payment-intent + + - name: Install macOS dependencies + run: | + brew tap wix/brew + brew install applesimutils + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + + # - name: Setup Ruby, JRuby and TruffleRuby + # uses: ruby/setup-ruby@v1.204.0 + + # - name: Cache CocoaPods + # id: cache-cocoapods + # uses: actions/cache@v3 + # with: + # path: ios/Pods + # key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }} + # restore-keys: | + # ${{ runner.os }}-pods- + + - name: Install CocoaPods + run: | + ls + cd ios + ls + pod setup --verbose + sudo gem install cocoapods + pod install + cd .. + + - name: Detox rebuild framework cache + run: npx detox rebuild-framework-cache + + - name: Start Rescript + run: | + npm run re:start + + - name: Start Metro + run: | + nohup npm run start & + + - name: Cache Detox build + id: cache-detox-build + uses: actions/cache@v3 + with: + path: ios/build + key: ${{ runner.os }}-detox-build + restore-keys: | + ${{ runner.os }}-detox-build + + - name: Detox build + run: npx detox build --configuration ios.sim.debug + + - name: Detox test + run: npx detox test --configuration ios.sim.debug --loglevel trace --record-logs all + + - name: Upload artifacts + if: failure() + uses: actions/upload-artifact@v3 + with: + name: detox-artifacts + path: artifacts diff --git a/.gitignore b/.gitignore index 2ce4f430..313f67fd 100644 --- a/.gitignore +++ b/.gitignore @@ -112,4 +112,9 @@ hyperswitch-react-native/lib/bs/ #next .next/ next-env.d.ts -/out \ No newline at end of file +/out + +#detox +*.log +*.trace.json +*.detox.jsonl \ No newline at end of file diff --git a/android b/android index d4d85a4c..d9bfe485 160000 --- a/android +++ b/android @@ -1 +1 @@ -Subproject commit d4d85a4cfd3fa753b022d1c4261daa8637b56ea2 +Subproject commit d9bfe4850f4ef3de7684278d4912057801062e3f diff --git a/detox-tests/babel.config.js b/detox-tests/babel.config.js new file mode 100644 index 00000000..1f47a7af --- /dev/null +++ b/detox-tests/babel.config.js @@ -0,0 +1,8 @@ +module.exports = { + presets: [ + '@babel/preset-env', // Handles modern JavaScript features like `export` + ], + plugins: [ + '@babel/plugin-transform-modules-commonjs', // Transforms ES modules to CommonJS for Jest compatibility + ], +}; diff --git a/detox-tests/e2e/card-flow-e2e.test.ts b/detox-tests/e2e/card-flow-e2e.test.ts new file mode 100644 index 00000000..4266eb26 --- /dev/null +++ b/detox-tests/e2e/card-flow-e2e.test.ts @@ -0,0 +1,56 @@ +import * as testIds from "../../src/utility/test/TestUtils.bs.js"; +import { device } from "detox" +import { visaSandboxCard, LAUNCH_PAYMENT_SHEET_BTN_TEXT } from "../fixtures/Constants" +import { waitForVisibility, typeTextInInput } from "../utils/DetoxHelpers" +describe('card-flow-e2e-test', () => { + jest.retryTimes(6); + beforeAll(async () => { + await device.launchApp({ + launchArgs: { detoxEnableSynchronization: 1 }, + newInstance: true, + }); + await device.enableSynchronization(); + }); + + it('demo app should load successfully', async () => { + await waitForVisibility(element(by.text(LAUNCH_PAYMENT_SHEET_BTN_TEXT))) + }); + + it('payment sheet should open', async () => { + await element(by.text(LAUNCH_PAYMENT_SHEET_BTN_TEXT)).tap(); + await waitForVisibility(element(by.text('Test Mode'))) + }) + + it('should enter details in card form', async () => { + const cardNumberInput = await element(by.id(testIds.cardNumberInputTestId)) + const expiryInput = await element(by.id(testIds.expiryInputTestId)) + const cvcInput = await element(by.id(testIds.cvcInputTestId)) + + await waitFor(cardNumberInput).toExist(); + await waitForVisibility(cardNumberInput); + await cardNumberInput.tap(); + + await cardNumberInput.clearText(); + await typeTextInInput(cardNumberInput, visaSandboxCard.cardNumber) + + await waitFor(expiryInput).toExist(); + await waitForVisibility(expiryInput); + await expiryInput.typeText(visaSandboxCard.expiryDate); + + await waitFor(cvcInput).toExist(); + await waitForVisibility(cvcInput); + await cvcInput.typeText(visaSandboxCard.cvc); + }); + + it('should be able to succesfully complete card payment', async () => { + const payNowButton = await element(by.id(testIds.payButtonTestId)) + await waitFor(payNowButton).toExist(); + await waitForVisibility(payNowButton) + await payNowButton.tap(); + + if (device.getPlatform() === "ios") + await waitForVisibility(element(by.text('Payment complete'))) + else + await waitForVisibility(element(by.text('succeeded'))) + }) +}); diff --git a/detox-tests/e2e/jest.config.js b/detox-tests/e2e/jest.config.js new file mode 100644 index 00000000..519a0bc2 --- /dev/null +++ b/detox-tests/e2e/jest.config.js @@ -0,0 +1,17 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + preset: 'ts-jest', + rootDir: '..', + testMatch: ['/e2e/**/*.test.ts'], + testTimeout: 1200000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, + transform: { + // Add a transformer for `.bs.js` files + '\\.bs\\.js$': 'babel-jest', // or specify a custom transformer if needed + }, +}; diff --git a/detox-tests/fixtures/Constants.ts b/detox-tests/fixtures/Constants.ts new file mode 100644 index 00000000..fd047d75 --- /dev/null +++ b/detox-tests/fixtures/Constants.ts @@ -0,0 +1,8 @@ +type card = { + cardNumber: string, + expiryDate: string, + cvc: string, +} + +export const visaSandboxCard = { cardNumber: "4242424242424242", expiryDate: "04/44", cvc: "123" } +export const LAUNCH_PAYMENT_SHEET_BTN_TEXT = "Launch Payment Sheet" diff --git a/detox-tests/package.json b/detox-tests/package.json new file mode 100644 index 00000000..108e64ae --- /dev/null +++ b/detox-tests/package.json @@ -0,0 +1,17 @@ +{ + "name": "detox-tests", + "version": "1.0.0", + "main": "e2e/jest.config.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "^22.10.2", + "ts-jest": "^29.2.5", + "typescript": "^5.7.2" + } +} diff --git a/detox-tests/tsconfig.json b/detox-tests/tsconfig.json new file mode 100644 index 00000000..0fec8191 --- /dev/null +++ b/detox-tests/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + + "target": "ES2022", + "module": "commonjs", + + }, + "include": ["**/*.ts"] +} diff --git a/detox-tests/utils/DetoxHelpers.ts b/detox-tests/utils/DetoxHelpers.ts new file mode 100644 index 00000000..bf4e634e --- /dev/null +++ b/detox-tests/utils/DetoxHelpers.ts @@ -0,0 +1,12 @@ +const DEFAULT_TIMEOUT = 10000; + +export async function waitForVisibility(element: Detox.IndexableNativeElement, timeout = DEFAULT_TIMEOUT) { + await waitFor(element) + .toBeVisible() + .withTimeout(timeout); +} + +export async function typeTextInInput(element: Detox.IndexableNativeElement, text: string) { + device.getPlatform() == "ios" ? + await element.typeText(text) : await element.replaceText(text); +} diff --git a/ios b/ios index 2ea07018..286793cc 160000 --- a/ios +++ b/ios @@ -1 +1 @@ -Subproject commit 2ea0701856632061f92ea3e54d48f825d63aabba +Subproject commit 286793ccee8b746f6459642e24ea4183946e1eb7 diff --git a/package.json b/package.json index bb432d4b..1ce3671a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,10 @@ "deploy-to-s3": "node ./scripts/pushToS3.js", "lint": "eslint .", "test": "jest", + "android:e2e": "npx detox test --configuration android.emu.debug --loglevel trace --record-logs all", + "build:android-e2e": "npx detox build --configuration android.emu.debug", + "build:ios-e2e": "npx detox build --configuration ios.sim.debug", + "ios-e2e": "npx detox test --configuration ios.sim.debug --loglevel trace --record-logs all", "prepare": "husky" }, "dependencies": { @@ -79,6 +83,7 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", + "detox": "^20.28.0", "dotenv": "^10.0.0", "esbuild": "^0.23.1", "eslint": "^8.19.0", @@ -191,5 +196,8 @@ "@react-native/assets-registry": "patch:@react-native/assets-registry@npm%3A0.75.2#~/.yarn/patches/@react-native-assets-registry-npm-0.75.2-553af80bf2.patch", "react-native-code-push": "patch:react-native-code-push@npm%3A8.3.1#~/.yarn/patches/react-native-code-push-npm-8.3.1-9608679893.patch", "@react-native/gradle-plugin": "patch:@react-native/gradle-plugin@npm%3A0.75.2#~/.yarn/patches/@react-native-gradle-plugin-npm-0.75.2-3de59c69c5.patch" - } -} + }, + "workspaces": [ + "detox-tests" + ] +} \ No newline at end of file diff --git a/server.js b/server.js index 8992ea33..a50a07f7 100644 --- a/server.js +++ b/server.js @@ -5,7 +5,7 @@ app.use(cors()); app.use(express.static('./dist')); app.use(express.json()); -require('dotenv').config({ path: './.env' }); +require('dotenv').config({path: './.env'}); try { var hyper = require('@juspay-tech/hyperswitch-node')( @@ -18,7 +18,7 @@ try { app.get('/create-payment-intent', async (req, res) => { try { - var paymentIntent = await hyper.paymentIntents.create({ + const createPaymentBody = { amount: 2999, currency: 'USD', authentication_type: 'no_three_ds', @@ -26,7 +26,6 @@ app.get('/create-payment-intent', async (req, res) => { capture_method: 'automatic', email: 'abc@gmail.com', business_country: 'US', - business_label: 'default', billing: { address: { line1: '1467', @@ -38,7 +37,7 @@ app.get('/create-payment-intent', async (req, res) => { country: 'PL', first_name: 'joseph', last_name: 'Doe', - } + }, }, shipping: { address: { @@ -51,9 +50,16 @@ app.get('/create-payment-intent', async (req, res) => { country: 'PL', first_name: 'joseph', last_name: 'Doe', - } + }, }, - }); + }; + + const profileId = process.env.PROFILE_ID; + if (profileId) { + createPaymentBody.profile_id = profileId; + } + + var paymentIntent = await hyper.paymentIntents.create(createPaymentBody); // Send publishable key and PaymentIntent details to client res.send({ @@ -61,7 +67,7 @@ app.get('/create-payment-intent', async (req, res) => { clientSecret: paymentIntent.client_secret, }); } catch (err) { - console.log(err) + console.log(err); return res.status(400).send({ error: { @@ -81,7 +87,7 @@ app.get('/create-ephemeral-key', async (req, res) => { 'Content-Type': 'application/json', 'api-key': process.env.HYPERSWITCH_SECRET_KEY, }, - body: JSON.stringify({ customer_id: "hyperswitch_sdk_demo_id" }), + body: JSON.stringify({customer_id: 'hyperswitch_sdk_demo_id'}), }, ); const ephemeralKey = await response.json(); @@ -108,7 +114,7 @@ app.get('/payment_methods', async (req, res) => { 'Content-Type': 'application/json', 'api-key': process.env.HYPERSWITCH_SECRET_KEY, }, - body: JSON.stringify({ customer_id: 'hyperswitch_sdk_demo_id' }), + body: JSON.stringify({customer_id: 'hyperswitch_sdk_demo_id'}), }, ); const json = await response.json(); diff --git a/src/components/common/CustomButton.res b/src/components/common/CustomButton.res index ad0a8067..e526df2b 100644 --- a/src/components/common/CustomButton.res +++ b/src/components/common/CustomButton.res @@ -21,6 +21,7 @@ let make = ( ~borderRadius=0., ~borderColor="#ffffff", ~children=None, + ~testID=?, ) => { let fillAnimation = React.useRef(Animated.Value.create(0.)).current let { @@ -105,6 +106,7 @@ let make = ( ])}> Option.getOr("")} style={array([ viewStyle( ~height=100.->pct, diff --git a/src/components/common/CustomInput.res b/src/components/common/CustomInput.res index 8b099757..3b070591 100644 --- a/src/components/common/CustomInput.res +++ b/src/components/common/CustomInput.res @@ -47,6 +47,7 @@ let make = ( ~enableShadow=true, ~animate=true, ~animateLabel=?, + ~name="", ) => { let { placeholderColor, @@ -204,6 +205,7 @@ let make = ( ), viewStyle(~padding=0.->dp, ~height=(height -. 10.)->dp, ~width=100.->pct, ()), ])} + testID=name secureTextEntry=showPass autoCapitalize=#none multiline diff --git a/src/components/elements/PaymentSheetUi.res b/src/components/elements/PaymentSheetUi.res index c4fadd00..6d822ed5 100644 --- a/src/components/elements/PaymentSheetUi.res +++ b/src/components/elements/PaymentSheetUi.res @@ -134,6 +134,7 @@ let make = ( pct, ~borderRadius, ())}> pct, ())}> toInputRef) state=cardNumber setState={text => onChangeCardNumber(text, expireRef)} @@ -181,6 +182,7 @@ let make = ( )}> pct, ())}> onChangeCardExpire(text, cvvRef)} @@ -217,6 +219,7 @@ let make = ( pct, ())}> { if !(isAllValuesValid && hasSomeFields) { logger( diff --git a/src/utility/test/TestUtils.res b/src/utility/test/TestUtils.res new file mode 100644 index 00000000..317166bf --- /dev/null +++ b/src/utility/test/TestUtils.res @@ -0,0 +1,4 @@ +let cardNumberInputTestId = "CardNumberInputTestId" +let cvcInputTestId = "CVCInputTestId" +let expiryInputTestId = "ExpiryInputTestId" +let payButtonTestId = "PayButtonTestId"