diff --git a/.eslintrc.js b/.eslintrc.js index 22b94d79e369..ac4546567833 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -164,7 +164,7 @@ module.exports = { }, }, { - files: ['tests/**/*.{js,jsx,ts,tsx}', '.github/**/*.{js,jsx,ts,tsx}'], + files: ['workflow_tests/**/*.{js,jsx,ts,tsx}', 'tests/**/*.{js,jsx,ts,tsx}', '.github/**/*.{js,jsx,ts,tsx}'], rules: { '@lwc/lwc/no-async-await': 'off', 'no-await-in-loop': 'off', diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index 494326869cca..b6558b049647 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -13,7 +13,8 @@ jobs: outputs: IS_DEPLOYER: ${{ fromJSON(steps.isDeployer.outputs.IS_DEPLOYER) || github.actor == 'OSBotify' }} steps: - - id: isDeployer + - name: Check if user is deployer + id: isDeployer run: | if gh api /orgs/Expensify/teams/mobile-deployers/memberships/${{ github.actor }} --silent; then echo "IS_DEPLOYER=true" >> "$GITHUB_OUTPUT" @@ -39,7 +40,8 @@ jobs: ref: staging token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Set up git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 8943669c2ba8..54ae1048b57b 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -13,18 +13,20 @@ jobs: # It does not run for pull requests created by OSBotify if: ${{ github.event.issue.pull_request || (github.event_name == 'pull_request_target' && github.event.pull_request.user.login != 'OSBotify') }} steps: - - uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73 + - name: CLA comment check + uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73 id: sign with: text: ${{ github.event.comment.body }} regex: '\s*I have read the CLA Document and I hereby sign the CLA\s*' - - uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73 + - name: CLA comment re-check + uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73 id: recheck with: text: ${{ github.event.comment.body }} regex: '\s*recheck\s*' - name: CLA Assistant - if: ${{ steps.recheck.outputs.match != '' || steps.sign.outputs.match != '' }} || github.event_name == 'pull_request_target' + if: ${{ steps.recheck.outputs.match != '' || steps.sign.outputs.match != '' || github.event_name == 'pull_request_target' }} # Version: 2.1.2-beta uses: cla-assistant/github-action@948230deb0d44dd38957592f08c6bd934d96d0cf env: diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index b2703731df79..ba907334c595 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -54,18 +54,21 @@ jobs: NEW_VERSION: ${{ steps.bumpVersion.outputs.NEW_VERSION }} steps: - - uses: softprops/turnstyle@ca99add00ff0c9cbc697d22631d2992f377e5bd5 + - name: Run turnstyle + uses: softprops/turnstyle@ca99add00ff0c9cbc697d22631d2992f377e5bd5 with: poll-interval-seconds: 10 env: GITHUB_TOKEN: ${{ github.token }} - - uses: actions/checkout@v3 + - name: Check out + uses: actions/checkout@v3 with: ref: main token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Setup git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} @@ -89,7 +92,8 @@ jobs: - name: Update main branch run: git push origin main - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b3105ee05c2c..f2ff67680940 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,8 @@ jobs: ref: staging token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Setup git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} @@ -29,12 +30,14 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/production' steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: ref: production token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Setup git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml index 8065a5c88cb2..f42d19ca8241 100644 --- a/.github/workflows/deployBlocker.yml +++ b/.github/workflows/deployBlocker.yml @@ -11,7 +11,8 @@ jobs: if: ${{ github.event.label.name == 'DeployBlockerCash' }} steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -63,7 +64,8 @@ jobs: 2. Find someone who can quickly fix the issue. 3. Fix the issue yourself. - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml index 7b71f6263c88..e2323af2486e 100644 --- a/.github/workflows/finishReleaseCycle.yml +++ b/.github/workflows/finishReleaseCycle.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - name: Reopen and comment on issue + - name: Reopen and comment on issue (not a team member) if: ${{ !fromJSON(steps.isDeployer.outputs.IS_DEPLOYER) }} uses: Expensify/App/.github/actions/javascript/reopenIssueWithComment@main with: @@ -41,8 +41,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} - - name: Reopen and comment on issue - if: ${{ fromJSON(steps.isDeployer.outputs.IS_DEPLOYER) && fromJSON(steps.checkDeployBlockers.outputs.HAS_DEPLOY_BLOCKERS) }} + - name: Reopen and comment on issue (has blockers) + if: ${{ fromJSON(steps.isDeployer.outputs.IS_DEPLOYER) && fromJSON(steps.checkDeployBlockers.outputs.HAS_DEPLOY_BLOCKERS || 'false') }} uses: Expensify/App/.github/actions/javascript/reopenIssueWithComment@main with: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -51,7 +51,8 @@ jobs: This issue either has unchecked items or has not yet been marked with the `:shipit:` emoji of approval. Reopening! - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -62,12 +63,14 @@ jobs: needs: validate if: ${{ fromJSON(needs.validate.outputs.isValid) }} steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: ref: staging token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Setup Git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} @@ -79,7 +82,8 @@ jobs: # Force-update the remote production branch. git push --force origin production - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -98,12 +102,14 @@ jobs: runs-on: ubuntu-latest needs: [updateProduction, createNewPatchVersion] steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: ref: main token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Setup Git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} @@ -115,7 +121,8 @@ jobs: # Force-update the remote staging branch git push --force origin staging - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1983e406c77b..5953a4aa89e2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,9 +11,11 @@ jobs: if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Lint JavaScript and Typescript with ESLint run: npm run lint diff --git a/.github/workflows/lockDeploys.yml b/.github/workflows/lockDeploys.yml index a49a5519f690..6ca025bb2a25 100644 --- a/.github/workflows/lockDeploys.yml +++ b/.github/workflows/lockDeploys.yml @@ -9,7 +9,8 @@ jobs: if: ${{ github.event.label.name == '🔐 LockCashDeploys 🔐' && contains(github.event.issue.labels.*.name, 'StagingDeployCash') && github.actor != 'OSBotify' }} runs-on: macos-12 steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: ref: main token: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -27,7 +28,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - if: ${{ failure() }} + - name: Announce failed workflow + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index cd65b8451636..ad002e164837 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -18,7 +18,8 @@ jobs: outputs: IS_DEPLOYER: ${{ fromJSON(steps.isUserDeployer.outputs.IS_DEPLOYER) || github.actor == 'OSBotify' }} steps: - - id: isUserDeployer + - name: Check if user is deployer + id: isUserDeployer run: | if gh api /orgs/Expensify/teams/mobile-deployers/memberships/${{ github.actor }} --silent; then echo "IS_DEPLOYER=true" >> "$GITHUB_OUTPUT" @@ -35,8 +36,10 @@ jobs: if: ${{ github.event_name != 'release' }} needs: validateActor steps: - - uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Set version id: getVersion @@ -54,14 +57,17 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: ubuntu-latest-xl steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + - name: Setup Ruby + uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 with: ruby-version: '2.7' bundler-cache: true @@ -128,9 +134,11 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: macos-12-xl steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Decrypt Developer ID Certificate run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg @@ -165,14 +173,17 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: macos-12-xl steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + - name: Setup Ruby + uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 with: ruby-version: '2.7' bundler-cache: true @@ -267,9 +278,11 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: ubuntu-latest-xl steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Setup Cloudflare CLI run: pip3 install cloudflare @@ -324,7 +337,8 @@ jobs: if: ${{ failure() }} needs: [android, desktop, iOS, web] steps: - - uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main + - name: Post Slack message on failure + uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -334,7 +348,8 @@ jobs: if: ${{ success() }} needs: [android, desktop, iOS, web] steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - name: Set version run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" @@ -394,9 +409,11 @@ jobs: if: ${{ always() }} needs: [android, desktop, iOS, web] steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Set version run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index e3977734fc50..186490c7baaf 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -2,7 +2,8 @@ name: Process new code merged to main on: push: - branches: [main] + branches: + - main jobs: typecheck: @@ -20,12 +21,14 @@ jobs: if: ${{ always() }} steps: - - if: ${{ needs.typecheck.result == 'failure' || needs.lint.result == 'failure' || needs.test.result == 'failure' }} + - name: Announce failed workflow in Slack + if: ${{ needs.typecheck.result == 'failure' || needs.lint.result == 'failure' || needs.test.result == 'failure' }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - if: ${{ needs.typecheck.result == 'failure' || needs.lint.result == 'failure' || needs.test.result == 'failure' }} + - name: Exit failed workflow + if: ${{ needs.typecheck.result == 'failure' || needs.lint.result == 'failure' || needs.test.result == 'failure' }} run: exit 1 chooseDeployActions: @@ -33,7 +36,7 @@ jobs: needs: confirmPassingBuild outputs: MERGED_PR: ${{ steps.getMergedPullRequest.outputs.number }} - SHOULD_DEPLOY: ${{ steps.shouldDeploy.outputs.SHOULD_DEPLOY }} + SHOULD_DEPLOY: ${{ fromJSON(steps.shouldDeploy.outputs.SHOULD_DEPLOY) }} steps: - name: Get merged pull request @@ -75,18 +78,21 @@ jobs: needs: [chooseDeployActions, createNewVersion] runs-on: ubuntu-latest steps: - - uses: softprops/turnstyle@ca99add00ff0c9cbc697d22631d2992f377e5bd5 + - name: Run turnstyle + uses: softprops/turnstyle@ca99add00ff0c9cbc697d22631d2992f377e5bd5 with: poll-interval-seconds: 10 env: GITHUB_TOKEN: ${{ github.token }} - - uses: actions/checkout@v3 + - name: Checkout main + uses: actions/checkout@v3 with: ref: main token: ${{ secrets.OS_BOTIFY_TOKEN }} - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + - name: Setup Git for OSBotify + uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} @@ -98,7 +104,8 @@ jobs: # Force-update the remote staging branch git push --force origin staging - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -134,7 +141,8 @@ jobs: if: ${{ github.actor != 'OSBotify' && !fromJSON(needs.isExpensifyEmployee.outputs.IS_EXPENSIFY_EMPLOYEE) }} steps: # Version: 2.3.4 - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: token: ${{ secrets.OS_BOTIFY_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e79a02281ae0..72bdd0468fd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,9 +18,11 @@ jobs: chunk: [ 1, 2, 3 ] name: test (job ${{ fromJSON(matrix.chunk) }}) steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Get number of CPU cores id: cpu-cores @@ -53,9 +55,11 @@ jobs: runs-on: ubuntu-latest name: Shell tests steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Test CI git logic run: tests/unit/CIGitLogicTest.sh diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index fd8118895679..6ded44d7059f 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -19,7 +19,8 @@ jobs: outputs: READY_TO_BUILD: ${{ fromJSON(steps.isExpensifyEmployee.outputs.IS_EXPENSIFY_EMPLOYEE) && fromJSON(steps.hasReadyToBuildLabel.outputs.HAS_READY_TO_BUILD_LABEL) }} steps: - - id: isExpensifyEmployee + - name: Is Expensify employee + id: isExpensifyEmployee run: | if gh api /orgs/Expensify/teams/expensify-expensify/memberships/${{ github.actor }} --silent; then echo "IS_EXPENSIFY_EMPLOYEE=true" >> "$GITHUB_OUTPUT" @@ -70,7 +71,8 @@ jobs: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} steps: # This action checks-out the repository, so the workflow can access it. - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} @@ -80,9 +82,11 @@ jobs: sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + - name: Setup Ruby + uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 with: ruby-version: '2.7' bundler-cache: true @@ -117,7 +121,8 @@ jobs: MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - - uses: actions/upload-artifact@v3 + - name: Upload Artifact + uses: actions/upload-artifact@v3 with: name: android path: ./android_paths.json @@ -131,7 +136,8 @@ jobs: runs-on: macos-12-xl steps: # This action checks-out the repository, so the workflow can access it. - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} @@ -144,12 +150,14 @@ jobs: sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - - name: Setup Xcode + - name: Setup XCode run: sudo xcode-select -switch /Applications/Xcode_14.2.app - - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + - name: Setup Ruby + uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 with: ruby-version: '2.7' bundler-cache: true @@ -198,7 +206,8 @@ jobs: S3_BUCKET: ad-hoc-expensify-cash S3_REGION: us-east-1 - - uses: actions/upload-artifact@v3 + - name: Upload Artifact + uses: actions/upload-artifact@v3 with: name: ios path: ./ios_paths.json @@ -211,7 +220,8 @@ jobs: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} runs-on: macos-12-xl steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} @@ -221,7 +231,8 @@ jobs: sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Decrypt Developer ID Certificate run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg @@ -252,7 +263,8 @@ jobs: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} runs-on: ubuntu-latest-xl steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} @@ -262,7 +274,8 @@ jobs: sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main - name: Configure AWS Credentials uses: Expensify/App/.github/actions/composite/configureAwsCredentials@main @@ -283,7 +296,7 @@ jobs: postGithubComment: runs-on: ubuntu-latest name: Post a GitHub comment with app download links for testing - needs: [validateActor, getBranchRef, android, ios, desktop, web] + needs: [validateActor, getBranchRef, android, iOS, desktop, web] if: ${{ always() }} env: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} @@ -294,7 +307,8 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} - - uses: actions/download-artifact@v3 + - name: Download Artifact + uses: actions/download-artifact@v3 if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} - name: Read JSONs with android paths @@ -310,7 +324,7 @@ jobs: - name: Read JSONs with iOS paths id: get_ios_path - if: ${{ needs.ios.result == 'success' }} + if: ${{ needs.iOS.result == 'success' }} run: | content_ios="$(cat ./ios/ios_paths.json)" content_ios="${content_ios//'%'/'%25'}" diff --git a/.github/workflows/validateGithubActions.yml b/.github/workflows/validateGithubActions.yml index f496c5e4b27e..bcda941e1b05 100644 --- a/.github/workflows/validateGithubActions.yml +++ b/.github/workflows/validateGithubActions.yml @@ -12,9 +12,11 @@ jobs: if: github.actor != 'OSBotify' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main # Rebuild all the actions on this branch and check for a diff. Fail if there is one, # because that would be a sign that the PR author did not rebuild the Github Actions diff --git a/.github/workflows/verifyPodfile.yml b/.github/workflows/verifyPodfile.yml index 64188769f0bd..d8d931e476d1 100644 --- a/.github/workflows/verifyPodfile.yml +++ b/.github/workflows/verifyPodfile.yml @@ -14,8 +14,9 @@ jobs: if: github.actor != 'OSBotify' runs-on: macos-latest steps: - - uses: actions/checkout@v3 - - - uses: Expensify/App/.github/actions/composite/setupNode@main - - - run: ./.github/scripts/verifyPodfile.sh + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Node + uses: Expensify/App/.github/actions/composite/setupNode@main + - name: Verify podfile + run: ./.github/scripts/verifyPodfile.sh diff --git a/.github/workflows/verifySignedCommits.yml b/.github/workflows/verifySignedCommits.yml index e1068e71e041..ee1b0c4c78da 100644 --- a/.github/workflows/verifySignedCommits.yml +++ b/.github/workflows/verifySignedCommits.yml @@ -9,6 +9,7 @@ jobs: verifySignedCommits: runs-on: ubuntu-latest steps: - - uses: Expensify/App/.github/actions/javascript/verifySignedCommits@main + - name: Verify signed commits + uses: Expensify/App/.github/actions/javascript/verifySignedCommits@main with: GITHUB_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 4919ddc1fdc9..aae9baad529f 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,9 @@ tests/e2e/results/ # Typescript tsconfig.tsbuildinfo + +# Mock-github +/repo/ + +# Workflow test logs +/workflow_tests/logs/ diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index 01e316e675d8..c871764117ed 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -69,8 +69,8 @@ "comment": "Workspace Details" }, { - "/": "/save-the-world/*", - "comment": "Save the World" + "/": "/teachersunite/*", + "comment": "Teachers Unite!" } ] } diff --git a/android/app/build.gradle b/android/app/build.gradle index 64dd8a10eb44..62c95d20e3ca 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,7 +22,7 @@ react { // The list of variants to that are debuggable. For those we're going to // skip the bundling of the JS bundle and the assets. By default is just 'debug'. // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. - debuggableVariants = ["developmentDebug"] + debuggableVariants = ["developmentDebug", "productionDebug"] /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001036817 - versionName "1.3.68-17" + versionCode 1001036901 + versionName "1.3.69-1" } flavorDimensions "default" diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index d87226269a8b..64ed3fda8b02 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 02BE6CF80ED1BD2445267F92 /* Pods-NewExpensify.release development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release development.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release development.xcconfig"; sourceTree = ""; }; 0B09CE5BDAF34DD3573AB4E2 /* Pods-NewExpensify.debug adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug adhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug adhoc.xcconfig"; sourceTree = ""; }; + 0B627F2A465153FFA6E3A4E0 /* Pods-NewExpensify.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugdevelopment.xcconfig"; sourceTree = ""; }; 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = NewExpensify/AppDelegate.mm; sourceTree = ""; }; 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NewExpensify/Images.xcassets; sourceTree = ""; }; 0E27AA27706D894246E7946D /* Pods-NewExpensify.debug production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug production.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug production.xcconfig"; sourceTree = ""; }; @@ -64,38 +65,49 @@ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = NewExpensify/main.m; sourceTree = ""; }; 18D050DF262400AF000D658B /* BridgingFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgingFile.swift; sourceTree = ""; }; 1DDE5449979A136852B939B5 /* Pods-NewExpensify.release adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release adhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release adhoc.xcconfig"; sourceTree = ""; }; + 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; + 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; 34A8FDD1F9AA58B8F15C8380 /* Pods-NewExpensify.release production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release production.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release production.xcconfig"; sourceTree = ""; }; 374FB8D528A133A7000D84EF /* OriginImageRequestHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OriginImageRequestHandler.h; path = NewExpensify/OriginImageRequestHandler.h; sourceTree = ""; }; 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = OriginImageRequestHandler.mm; path = NewExpensify/OriginImageRequestHandler.mm; sourceTree = ""; }; 3D393D7ABC1092F1DE91397F /* Pods-NewExpensify-NewExpensifyTests.debug staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug staging.xcconfig"; sourceTree = ""; }; 432FF5842B766535509FC547 /* Pods-NewExpensify-NewExpensifyTests.debug adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug adhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug adhoc.xcconfig"; sourceTree = ""; }; 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-Medium.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-Medium.otf"; sourceTree = ""; }; + 47D5DF3C6779D41BE70CD031 /* Pods-NewExpensify.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseproduction.xcconfig"; sourceTree = ""; }; + 4E8BF7B08BA3181991BFCF4B /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; sourceTree = ""; }; 52796131E6554494B2DDB056 /* ExpensifyNeue-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Bold.otf"; path = "../assets/fonts/native/ExpensifyNeue-Bold.otf"; sourceTree = ""; }; + 57DBBEDB9692E096D4BA0141 /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; sourceTree = ""; }; 6B5211DB0EEB46E12DF4AD2D /* Pods-NewExpensify-NewExpensifyTests.release adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release adhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release adhoc.xcconfig"; sourceTree = ""; }; 6BE16DA6EFF88513DB1CD47B /* Pods-NewExpensify-NewExpensifyTests.release staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release staging.xcconfig"; sourceTree = ""; }; + 6F6A514B4DF07A60EC8355BA /* Pods-NewExpensify.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugadhoc.xcconfig"; sourceTree = ""; }; 6FB387B20AE4E6E98858B6AA /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7041848326A8E40900E09F4D /* RCTStartupTimer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTStartupTimer.h; path = NewExpensify/RCTStartupTimer.h; sourceTree = ""; }; 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTStartupTimer.m; path = NewExpensify/RCTStartupTimer.m; sourceTree = ""; }; 70CF6E81262E297300711ADC /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = NewExpensify/BootSplash.storyboard; sourceTree = ""; }; 75CABB0D0ABB0082FE0EB600 /* Pods-NewExpensify.release staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release staging.xcconfig"; sourceTree = ""; }; + 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; 8D3B36BF88E773E3C1A383FA /* Pods-NewExpensify.debug staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug staging.xcconfig"; sourceTree = ""; }; 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release production.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release production.xcconfig"; sourceTree = ""; }; AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BD6E1BA27D6ABE0AC9D70586 /* Pods-NewExpensify-NewExpensifyTests.release development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release development.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release development.xcconfig"; sourceTree = ""; }; + BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; + CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; CECC4CBB97A55705A33BEA9E /* Pods-NewExpensify.debug development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug development.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug development.xcconfig"; sourceTree = ""; }; D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-MediumItalic.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf"; sourceTree = ""; }; DB76E0D5C670190A0997C71E /* Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig"; sourceTree = ""; }; DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Bold.otf"; path = "../assets/fonts/native/ExpensifyMono-Bold.otf"; sourceTree = ""; }; DD7904292792E76D004484B4 /* RCTBootSplash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTBootSplash.h; path = NewExpensify/RCTBootSplash.h; sourceTree = ""; }; DD79042A2792E76D004484B4 /* RCTBootSplash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTBootSplash.m; path = NewExpensify/RCTBootSplash.m; sourceTree = ""; }; + E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; E2F1036F70CBFE39E9352674 /* Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig"; sourceTree = ""; }; E704648954784DDFBAADF568 /* ExpensifyMono-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Regular.otf"; path = "../assets/fonts/native/ExpensifyMono-Regular.otf"; sourceTree = ""; }; E9DF872C2525201700607FDC /* AirshipConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AirshipConfig.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; F0C450E92705020500FD2970 /* colors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = colors.json; path = ../colors.json; sourceTree = ""; }; + F15A36A3262EEC3B0CAB8EDF /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; sourceTree = ""; }; F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Regular.otf"; path = "../assets/fonts/native/ExpensifyNeue-Regular.otf"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -239,6 +251,18 @@ 6B5211DB0EEB46E12DF4AD2D /* Pods-NewExpensify-NewExpensifyTests.release adhoc.xcconfig */, 6BE16DA6EFF88513DB1CD47B /* Pods-NewExpensify-NewExpensifyTests.release staging.xcconfig */, 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */, + 0B627F2A465153FFA6E3A4E0 /* Pods-NewExpensify.debugdevelopment.xcconfig */, + 6F6A514B4DF07A60EC8355BA /* Pods-NewExpensify.debugadhoc.xcconfig */, + CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */, + 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */, + BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */, + 47D5DF3C6779D41BE70CD031 /* Pods-NewExpensify.releaseproduction.xcconfig */, + 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, + 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, + F15A36A3262EEC3B0CAB8EDF /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */, + 4E8BF7B08BA3181991BFCF4B /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */, + E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, + 57DBBEDB9692E096D4BA0141 /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */, ); path = Pods; sourceTree = ""; @@ -641,9 +665,9 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 00E356F61AD99517003FC87E /* Debug Development */ = { + 00E356F61AD99517003FC87E /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E2F1036F70CBFE39E9352674 /* Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig */; + baseConfigurationReference = 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -664,11 +688,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewExpensify.app/NewExpensify"; }; - name = "Debug Development"; + name = DebugDevelopment; }; - 00E356F71AD99517003FC87E /* Release Development */ = { + 00E356F71AD99517003FC87E /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BD6E1BA27D6ABE0AC9D70586 /* Pods-NewExpensify-NewExpensifyTests.release development.xcconfig */; + baseConfigurationReference = 4E8BF7B08BA3181991BFCF4B /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -687,11 +711,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewExpensify.app/NewExpensify"; }; - name = "Release Development"; + name = ReleaseDevelopment; }; - 13B07F941A680F5B00A75B9A /* Debug Development */ = { + 13B07F941A680F5B00A75B9A /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CECC4CBB97A55705A33BEA9E /* Pods-NewExpensify.debug development.xcconfig */; + baseConfigurationReference = 0B627F2A465153FFA6E3A4E0 /* Pods-NewExpensify.debugdevelopment.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -719,11 +743,11 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; - name = "Debug Development"; + name = DebugDevelopment; }; - 13B07F951A680F5B00A75B9A /* Release Development */ = { + 13B07F951A680F5B00A75B9A /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 02BE6CF80ED1BD2445267F92 /* Pods-NewExpensify.release development.xcconfig */; + baseConfigurationReference = 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -749,9 +773,9 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; - name = "Release Development"; + name = ReleaseDevelopment; }; - 83CBBA201A601CBA00E9B192 /* Debug Development */ = { + 83CBBA201A601CBA00E9B192 /* DebugDevelopment */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -814,9 +838,9 @@ REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; - name = "Debug Development"; + name = DebugDevelopment; }; - 83CBBA211A601CBA00E9B192 /* Release Development */ = { + 83CBBA211A601CBA00E9B192 /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -873,9 +897,9 @@ SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; - name = "Release Development"; + name = ReleaseDevelopment; }; - CF9AF93E29EE9276001FA527 /* Debug Production */ = { + CF9AF93E29EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -938,11 +962,11 @@ REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; - name = "Debug Production"; + name = DebugProduction; }; - CF9AF93F29EE9276001FA527 /* Debug Production */ = { + CF9AF93F29EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0E27AA27706D894246E7946D /* Pods-NewExpensify.debug production.xcconfig */; + baseConfigurationReference = CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -970,11 +994,11 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; - name = "Debug Production"; + name = DebugProduction; }; - CF9AF94029EE9276001FA527 /* Debug Production */ = { + CF9AF94029EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DB76E0D5C670190A0997C71E /* Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig */; + baseConfigurationReference = F15A36A3262EEC3B0CAB8EDF /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -995,9 +1019,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewExpensify.app/NewExpensify"; }; - name = "Debug Production"; + name = DebugProduction; }; - CF9AF94429EE927A001FA527 /* Debug AdHoc */ = { + CF9AF94429EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -1060,11 +1084,11 @@ REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; - name = "Debug AdHoc"; + name = DebugAdHoc; }; - CF9AF94529EE927A001FA527 /* Debug AdHoc */ = { + CF9AF94529EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0B09CE5BDAF34DD3573AB4E2 /* Pods-NewExpensify.debug adhoc.xcconfig */; + baseConfigurationReference = 6F6A514B4DF07A60EC8355BA /* Pods-NewExpensify.debugadhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconAdHoc; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1092,11 +1116,11 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; - name = "Debug AdHoc"; + name = DebugAdHoc; }; - CF9AF94629EE927A001FA527 /* Debug AdHoc */ = { + CF9AF94629EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 432FF5842B766535509FC547 /* Pods-NewExpensify-NewExpensifyTests.debug adhoc.xcconfig */; + baseConfigurationReference = 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -1117,9 +1141,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewExpensify.app/NewExpensify"; }; - name = "Debug AdHoc"; + name = DebugAdHoc; }; - CF9AF94729EE928E001FA527 /* Release Production */ = { + CF9AF94729EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -1176,11 +1200,11 @@ SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; - name = "Release Production"; + name = ReleaseProduction; }; - CF9AF94829EE928E001FA527 /* Release Production */ = { + CF9AF94829EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 34A8FDD1F9AA58B8F15C8380 /* Pods-NewExpensify.release production.xcconfig */; + baseConfigurationReference = 47D5DF3C6779D41BE70CD031 /* Pods-NewExpensify.releaseproduction.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1206,11 +1230,11 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; - name = "Release Production"; + name = ReleaseProduction; }; - CF9AF94929EE928E001FA527 /* Release Production */ = { + CF9AF94929EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */; + baseConfigurationReference = 57DBBEDB9692E096D4BA0141 /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -1229,9 +1253,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewExpensify.app/NewExpensify"; }; - name = "Release Production"; + name = ReleaseProduction; }; - CF9AF94D29EE9293001FA527 /* Release AdHoc */ = { + CF9AF94D29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -1288,11 +1312,11 @@ SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; - name = "Release AdHoc"; + name = ReleaseAdHoc; }; - CF9AF94E29EE9293001FA527 /* Release AdHoc */ = { + CF9AF94E29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1DDE5449979A136852B939B5 /* Pods-NewExpensify.release adhoc.xcconfig */; + baseConfigurationReference = BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconAdHoc; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1318,11 +1342,11 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; - name = "Release AdHoc"; + name = ReleaseAdHoc; }; - CF9AF94F29EE9293001FA527 /* Release AdHoc */ = { + CF9AF94F29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6B5211DB0EEB46E12DF4AD2D /* Pods-NewExpensify-NewExpensifyTests.release adhoc.xcconfig */; + baseConfigurationReference = E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -1341,7 +1365,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewExpensify.app/NewExpensify"; }; - name = "Release AdHoc"; + name = ReleaseAdHoc; }; /* End XCBuildConfiguration section */ @@ -1349,41 +1373,41 @@ 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "NewExpensifyTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 00E356F61AD99517003FC87E /* Debug Development */, - CF9AF94629EE927A001FA527 /* Debug AdHoc */, - CF9AF94029EE9276001FA527 /* Debug Production */, - 00E356F71AD99517003FC87E /* Release Development */, - CF9AF94F29EE9293001FA527 /* Release AdHoc */, - CF9AF94929EE928E001FA527 /* Release Production */, + 00E356F61AD99517003FC87E /* DebugDevelopment */, + CF9AF94629EE927A001FA527 /* DebugAdHoc */, + CF9AF94029EE9276001FA527 /* DebugProduction */, + 00E356F71AD99517003FC87E /* ReleaseDevelopment */, + CF9AF94F29EE9293001FA527 /* ReleaseAdHoc */, + CF9AF94929EE928E001FA527 /* ReleaseProduction */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug Development"; + defaultConfigurationName = DebugDevelopment; }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NewExpensify" */ = { isa = XCConfigurationList; buildConfigurations = ( - 13B07F941A680F5B00A75B9A /* Debug Development */, - CF9AF94529EE927A001FA527 /* Debug AdHoc */, - CF9AF93F29EE9276001FA527 /* Debug Production */, - 13B07F951A680F5B00A75B9A /* Release Development */, - CF9AF94E29EE9293001FA527 /* Release AdHoc */, - CF9AF94829EE928E001FA527 /* Release Production */, + 13B07F941A680F5B00A75B9A /* DebugDevelopment */, + CF9AF94529EE927A001FA527 /* DebugAdHoc */, + CF9AF93F29EE9276001FA527 /* DebugProduction */, + 13B07F951A680F5B00A75B9A /* ReleaseDevelopment */, + CF9AF94E29EE9293001FA527 /* ReleaseAdHoc */, + CF9AF94829EE928E001FA527 /* ReleaseProduction */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug Development"; + defaultConfigurationName = DebugDevelopment; }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NewExpensify" */ = { isa = XCConfigurationList; buildConfigurations = ( - 83CBBA201A601CBA00E9B192 /* Debug Development */, - CF9AF94429EE927A001FA527 /* Debug AdHoc */, - CF9AF93E29EE9276001FA527 /* Debug Production */, - 83CBBA211A601CBA00E9B192 /* Release Development */, - CF9AF94D29EE9293001FA527 /* Release AdHoc */, - CF9AF94729EE928E001FA527 /* Release Production */, + 83CBBA201A601CBA00E9B192 /* DebugDevelopment */, + CF9AF94429EE927A001FA527 /* DebugAdHoc */, + CF9AF93E29EE9276001FA527 /* DebugProduction */, + 83CBBA211A601CBA00E9B192 /* ReleaseDevelopment */, + CF9AF94D29EE9293001FA527 /* ReleaseAdHoc */, + CF9AF94729EE928E001FA527 /* ReleaseProduction */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug Development"; + defaultConfigurationName = DebugDevelopment; }; /* End XCConfigurationList section */ }; diff --git a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify AdHoc.xcscheme b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify AdHoc.xcscheme index 0e0fad6399a0..6cd590456eed 100644 --- a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify AdHoc.xcscheme +++ b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify AdHoc.xcscheme @@ -57,7 +57,7 @@ @@ -75,7 +75,7 @@ + buildConfiguration = "DebugAdHoc"> diff --git a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme index 77f512242f67..93d775217f11 100644 --- a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme +++ b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme @@ -23,7 +23,7 @@ @@ -41,7 +41,7 @@ + buildConfiguration = "DebugDevelopment"> diff --git a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme index f68be2705527..b6b5874506a8 100644 --- a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme +++ b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme @@ -75,7 +75,7 @@ + buildConfiguration = "DebugProduction"> diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index bb7c187963ec..c9ba137b13de 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.68 + 1.3.69 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.68.17 + 1.3.69.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 425fc3525ebd..e802856c4dd5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.68 + 1.3.69 CFBundleSignature ???? CFBundleVersion - 1.3.68.17 + 1.3.69.1 diff --git a/ios/Podfile b/ios/Podfile index 6445685db014..b30510572448 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -22,7 +22,7 @@ prepare_react_native_project! # dependencies: { # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), # ``` -flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled(['Debug Production', 'Debug Development', 'Debug AdHoc']) +flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled(['DebugProduction', 'DebugDevelopment', 'DebugAdHoc']) linkage = ENV['USE_FRAMEWORKS'] if linkage != nil @@ -54,12 +54,12 @@ target 'NewExpensify' do permissions_path = '../node_modules/react-native-permissions/ios' project 'NewExpensify', - 'Debug Development' => :debug, - 'Debug AdHoc' => :debug, - 'Debug Production' => :debug, - 'Release Development' => :release, - 'Release AdHoc' => :release, - 'Release Production' => :release + 'DebugDevelopment' => :debug, + 'DebugAdHoc' => :debug, + 'DebugProduction' => :debug, + 'ReleaseDevelopment' => :release, + 'ReleaseAdHoc' => :release, + 'ReleaseProduction' => :release pod 'Permission-LocationAccuracy', :path => "#{permissions_path}/LocationAccuracy" pod 'Permission-LocationAlways', :path => "#{permissions_path}/LocationAlways" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2bea672171fe..aeb1887223cd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1309,6 +1309,6 @@ SPEC CHECKSUMS: Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 845537d35601574adcd0794e17003ba7dbccdbfd +PODFILE CHECKSUM: 2daf34c870819a933f3fefe426801d54b2ff2a14 COCOAPODS: 1.12.1 diff --git a/package-lock.json b/package-lock.json index 9336f8d44eee..169ef273b29b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "new.expensify", - "version": "1.3.68-17", + "version": "1.3.69-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.68-17", + "version": "1.3.69-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@expensify/react-native-web": "0.18.15", + "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", @@ -18,6 +19,8 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", + "@kie/act-js": "^2.0.1", + "@kie/mock-github": "^1.0.0", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "7.4.0", "@react-native-async-storage/async-storage": "^1.17.10", @@ -36,6 +39,7 @@ "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", + "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.2.6", "awesome-phonenumber": "^5.4.0", "babel-plugin-transform-remove-console": "^6.9.4", @@ -48,6 +52,7 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", "fbjs": "^3.0.2", + "focus-trap-react": "^10.2.1", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-when": "^3.5.2", @@ -85,7 +90,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.72", + "react-native-onyx": "1.0.76", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^4.0.0", @@ -128,6 +133,8 @@ "@babel/runtime": "^7.20.0", "@electron/notarize": "^1.2.3", "@jest/globals": "^29.5.0", + "@kie/act-js": "^2.0.1", + "@kie/mock-github": "^1.0.0", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", @@ -179,7 +186,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^25.8.0", + "electron": "^25.8.1", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -221,7 +228,8 @@ "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.3", "webpack-font-preload-plugin": "^1.5.0", - "webpack-merge": "^5.8.0" + "webpack-merge": "^5.8.0", + "yaml": "^2.2.1" }, "engines": { "node": "16.15.1", @@ -3688,6 +3696,33 @@ "tslib": "^2.4.0" } }, + "node_modules/@formatjs/intl-datetimeformat": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-datetimeformat/-/intl-datetimeformat-6.10.0.tgz", + "integrity": "sha512-5tJWZxOyP5D4PDrqv27h0LWAPHhQM9BHR0pDBTZOKWFEZNrS2IgREbyalSGwQLtN1tZaDrt3YeNfNLbZk1wSUw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/intl-localematcher": "0.4.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-datetimeformat/node_modules/@formatjs/ecma402-abstract": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", + "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "dependencies": { + "@formatjs/intl-localematcher": "0.4.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-datetimeformat/node_modules/@formatjs/intl-localematcher": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", + "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@formatjs/intl-enumerator": { "version": "1.3.0", "license": "MIT", @@ -5193,6 +5228,110 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", "dev": true }, + "node_modules/@kie/act-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.3.0.tgz", + "integrity": "sha512-Q9k0b05uA46jXKWmVfoGDW+0xsCcE7QPiHi8IH7h41P36DujHKBj4k28RCeIEx3IwUCxYHKwubN8DH4Vzc9XcA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@kie/mock-github": "^2.0.0", + "adm-zip": "^0.5.10", + "ajv": "^8.12.0", + "bin-links": "^4.0.1", + "express": "^4.18.1", + "follow-redirects": "^1.15.2", + "tar": "^6.1.13", + "yaml": "^2.1.3" + }, + "bin": { + "act-js": "bin/act" + } + }, + "node_modules/@kie/act-js/node_modules/@kie/mock-github": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@kie/mock-github/-/mock-github-2.0.0.tgz", + "integrity": "sha512-od6UyICJYKMnz9HgEWCQAFT/JsCpKkLp+JQH8fV23tf+ZmmQI1dK3C20k6aO5uJhAHA0yOcFtbKFVF4+8i3DTg==", + "dev": true, + "dependencies": { + "@octokit/openapi-types-ghec": "^18.0.0", + "ajv": "^8.11.0", + "express": "^4.18.1", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "nock": "^13.2.7", + "simple-git": "^3.8.0", + "totalist": "^3.0.0" + } + }, + "node_modules/@kie/act-js/node_modules/@octokit/openapi-types-ghec": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types-ghec/-/openapi-types-ghec-18.0.0.tgz", + "integrity": "sha512-xY5FTR/DW2gUJdC5GyzkqrfMHhr3u3hg+dUG6bA5FvuuODw6A7+0JTTSS1ndLQEKGmFxP7chf1BKkhvhnqxCew==", + "dev": true + }, + "node_modules/@kie/act-js/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@kie/act-js/node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@kie/mock-github": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@kie/mock-github/-/mock-github-1.1.0.tgz", + "integrity": "sha512-fD+utlOiyZSOutOcXL0G9jfjbtvOO44PLUyTfgfkrm1+575R/dbvU6AcJfjc1DtHeRv7FC7f4ebyU+a1wgL6CA==", + "dev": true, + "dependencies": { + "@octokit/openapi-types-ghec": "^14.0.0", + "ajv": "^8.11.0", + "express": "^4.18.1", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "nock": "^13.2.7", + "simple-git": "^3.8.0", + "totalist": "^3.0.0" + } + }, + "node_modules/@kie/mock-github/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@kie/mock-github/node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -5699,6 +5838,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@octokit/openapi-types-ghec": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types-ghec/-/openapi-types-ghec-14.0.0.tgz", + "integrity": "sha512-xhd9oEvn2aroGn+sk09Ptx/76Y7aKU0EIgHukHPCU1+rGJreO36baEEk6k8ZPblieHNM39FcykJQmtDrETm0KA==", + "dev": true + }, "node_modules/@octokit/plugin-paginate-rest": { "version": "3.1.0", "dev": true, @@ -6929,14 +7074,6 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "engines": { - "node": ">= 14" - } - }, "node_modules/@react-native-community/cli-hermes": { "version": "11.3.5", "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-11.3.5.tgz", @@ -19380,6 +19517,15 @@ "node": ">= 10.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "dev": true, @@ -21217,6 +21363,46 @@ "node": "*" } }, + "node_modules/bin-links": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.2.tgz", + "integrity": "sha512-jxJ0PbXR8eQyPlExCvCs3JFnikvs1Yp4gUJt6nmgathdOwvur+q22KWC3h20gvWl4T/14DXKj2IlkJwwZkZPOw==", + "dev": true, + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/bin-links/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bin-links/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "devOptional": true, @@ -22650,6 +22836,15 @@ "node": ">=6" } }, + "node_modules/cmd-shim": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.1.tgz", + "integrity": "sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "license": "MIT", @@ -23304,6 +23499,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/cp-file": { "version": "7.0.0", "dev": true, @@ -24840,9 +25044,9 @@ } }, "node_modules/electron": { - "version": "25.8.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz", - "integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==", + "version": "25.8.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz", + "integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -28091,8 +28295,32 @@ "readable-stream": "^2.3.6" } }, + "node_modules/focus-trap": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz", + "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/focus-trap-react": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz", + "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==", + "dependencies": { + "focus-trap": "^7.5.2", + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/follow-redirects": { - "version": "1.15.1", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dev": true, "funding": [ { @@ -28100,7 +28328,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -28299,6 +28526,15 @@ "node": ">=8" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "3.0.1", "dev": true, @@ -37534,6 +37770,21 @@ "node": ">=12.0.0" } }, + "node_modules/nock": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -37717,6 +37968,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "license": "MIT", @@ -38687,14 +38947,6 @@ "node": ">=0.6.0" } }, - "node_modules/patch-package/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "engines": { - "node": ">= 14" - } - }, "node_modules/path-browserify": { "version": "0.0.1", "license": "MIT" @@ -39393,6 +39645,15 @@ "react-is": "^16.13.1" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/property-information": { "version": "5.6.0", "dev": true, @@ -40311,9 +40572,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.72.tgz", - "integrity": "sha512-roJuA92qZH2PLYSqBhSPCse+Ra2EJu4FBpVqguwJRp6oaLNHR1CtPTgU1xMh/kj2nWmdpcqKoOc3nS35asb80g==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.76.tgz", + "integrity": "sha512-mcMlYQCo1B/kom+4hu7CQKKLwvPFjQAJsVIzV2s9aa8XKNlcnYiJbfuM6RSJ1fFmSIeud4Y66rhv4/oWUkSl5A==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -41403,6 +41664,15 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/read-cmd-shim": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/read-config-file": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", @@ -44427,6 +44697,11 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -47697,11 +47972,11 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -50130,6 +50405,35 @@ "tslib": "^2.4.0" } }, + "@formatjs/intl-datetimeformat": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-datetimeformat/-/intl-datetimeformat-6.10.0.tgz", + "integrity": "sha512-5tJWZxOyP5D4PDrqv27h0LWAPHhQM9BHR0pDBTZOKWFEZNrS2IgREbyalSGwQLtN1tZaDrt3YeNfNLbZk1wSUw==", + "requires": { + "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/intl-localematcher": "0.4.0", + "tslib": "^2.4.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", + "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "requires": { + "@formatjs/intl-localematcher": "0.4.0", + "tslib": "^2.4.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", + "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "requires": { + "tslib": "^2.4.0" + } + } + } + }, "@formatjs/intl-enumerator": { "version": "1.3.0", "requires": { @@ -51206,6 +51510,98 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", "dev": true }, + "@kie/act-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.3.0.tgz", + "integrity": "sha512-Q9k0b05uA46jXKWmVfoGDW+0xsCcE7QPiHi8IH7h41P36DujHKBj4k28RCeIEx3IwUCxYHKwubN8DH4Vzc9XcA==", + "dev": true, + "requires": { + "@kie/mock-github": "^2.0.0", + "adm-zip": "^0.5.10", + "ajv": "^8.12.0", + "bin-links": "^4.0.1", + "express": "^4.18.1", + "follow-redirects": "^1.15.2", + "tar": "^6.1.13", + "yaml": "^2.1.3" + }, + "dependencies": { + "@kie/mock-github": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@kie/mock-github/-/mock-github-2.0.0.tgz", + "integrity": "sha512-od6UyICJYKMnz9HgEWCQAFT/JsCpKkLp+JQH8fV23tf+ZmmQI1dK3C20k6aO5uJhAHA0yOcFtbKFVF4+8i3DTg==", + "dev": true, + "requires": { + "@octokit/openapi-types-ghec": "^18.0.0", + "ajv": "^8.11.0", + "express": "^4.18.1", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "nock": "^13.2.7", + "simple-git": "^3.8.0", + "totalist": "^3.0.0" + } + }, + "@octokit/openapi-types-ghec": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types-ghec/-/openapi-types-ghec-18.0.0.tgz", + "integrity": "sha512-xY5FTR/DW2gUJdC5GyzkqrfMHhr3u3hg+dUG6bA5FvuuODw6A7+0JTTSS1ndLQEKGmFxP7chf1BKkhvhnqxCew==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true + } + } + }, + "@kie/mock-github": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@kie/mock-github/-/mock-github-1.1.0.tgz", + "integrity": "sha512-fD+utlOiyZSOutOcXL0G9jfjbtvOO44PLUyTfgfkrm1+575R/dbvU6AcJfjc1DtHeRv7FC7f4ebyU+a1wgL6CA==", + "dev": true, + "requires": { + "@octokit/openapi-types-ghec": "^14.0.0", + "ajv": "^8.11.0", + "express": "^4.18.1", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "nock": "^13.2.7", + "simple-git": "^3.8.0", + "totalist": "^3.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true + } + } + }, "@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -51588,6 +51984,12 @@ "version": "12.11.0", "dev": true }, + "@octokit/openapi-types-ghec": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types-ghec/-/openapi-types-ghec-14.0.0.tgz", + "integrity": "sha512-xhd9oEvn2aroGn+sk09Ptx/76Y7aKU0EIgHukHPCU1+rGJreO36baEEk6k8ZPblieHNM39FcykJQmtDrETm0KA==", + "dev": true + }, "@octokit/plugin-paginate-rest": { "version": "3.1.0", "dev": true, @@ -52501,11 +52903,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" } } }, @@ -61211,6 +61608,12 @@ "version": "1.2.1", "dev": true }, + "adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true + }, "agent-base": { "version": "6.0.2", "dev": true, @@ -62510,6 +62913,36 @@ "version": "5.2.2", "devOptional": true }, + "bin-links": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.2.tgz", + "integrity": "sha512-jxJ0PbXR8eQyPlExCvCs3JFnikvs1Yp4gUJt6nmgathdOwvur+q22KWC3h20gvWl4T/14DXKj2IlkJwwZkZPOw==", + "dev": true, + "requires": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + } + } + } + }, "binary-extensions": { "version": "2.2.0", "devOptional": true @@ -63485,6 +63918,12 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" }, + "cmd-shim": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.1.tgz", + "integrity": "sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q==", + "dev": true + }, "co": { "version": "4.6.0" }, @@ -63919,6 +64358,14 @@ "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } } }, "cp-file": { @@ -64991,9 +65438,9 @@ } }, "electron": { - "version": "25.8.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz", - "integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==", + "version": "25.8.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz", + "integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==", "dev": true, "requires": { "@electron/get": "^2.0.0", @@ -67266,8 +67713,27 @@ "readable-stream": "^2.3.6" } }, + "focus-trap": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz", + "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==", + "requires": { + "tabbable": "^6.2.0" + } + }, + "focus-trap-react": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz", + "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==", + "requires": { + "focus-trap": "^7.5.2", + "tabbable": "^6.2.0" + } + }, "follow-redirects": { - "version": "1.15.1", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dev": true }, "for-each": { @@ -67383,6 +67849,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } }, @@ -73699,6 +74171,18 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==" }, + "nock": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + } + }, "node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -73832,6 +74316,12 @@ "version": "6.1.0", "dev": true }, + "npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true + }, "npm-run-path": { "version": "4.0.1", "requires": { @@ -74463,11 +74953,6 @@ "requires": { "os-tmpdir": "~1.0.2" } - }, - "yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" } } }, @@ -74932,6 +75417,12 @@ "react-is": "^16.13.1" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "property-information": { "version": "5.6.0", "dev": true, @@ -75636,9 +76127,9 @@ } }, "react-native-onyx": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.72.tgz", - "integrity": "sha512-roJuA92qZH2PLYSqBhSPCse+Ra2EJu4FBpVqguwJRp6oaLNHR1CtPTgU1xMh/kj2nWmdpcqKoOc3nS35asb80g==", + "version": "1.0.76", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.76.tgz", + "integrity": "sha512-mcMlYQCo1B/kom+4hu7CQKKLwvPFjQAJsVIzV2s9aa8XKNlcnYiJbfuM6RSJ1fFmSIeud4Y66rhv4/oWUkSl5A==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -76222,6 +76713,12 @@ "memoize-one": ">=3.1.1 <6" } }, + "read-cmd-shim": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "dev": true + }, "read-config-file": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", @@ -78362,6 +78859,11 @@ "version": "2.0.15", "dev": true }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -80552,8 +81054,9 @@ "version": "4.0.0" }, "yaml": { - "version": "1.10.2", - "dev": true + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==" }, "yargs": { "version": "13.3.2", diff --git a/package.json b/package.json index 021dba72e545..256ef013b0d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.68-17", + "version": "1.3.69-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -11,10 +11,10 @@ "postinstall": "scripts/postInstall.sh", "clean": "npx react-native clean-project-auto", "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --variant=developmentDebug --appId=com.expensify.chat.dev", - "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --configuration=\"Debug Development\" --scheme=\"New Expensify Dev\"", + "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --configuration=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"", "pod-install": "cd ios && bundle exec pod install", - "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --configuration=\\\"Debug Development\\\" --scheme=\\\"New Expensify Dev\\\"\"", - "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --configuration=\\\"Debug Development\\\" --scheme=\\\"New Expensify Dev\\\"\"", + "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --configuration=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", + "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --configuration=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", "web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "node web/proxy.js", @@ -47,10 +47,13 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", - "test:e2e": "node tests/e2e/testRunner.js --development" + "test:e2e": "node tests/e2e/testRunner.js --development", + "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", + "workflow-test:generate": "node workflow_tests/utils/preGenerateTest.js" }, "dependencies": { "@expensify/react-native-web": "0.18.15", + "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", @@ -59,6 +62,8 @@ "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", + "@kie/act-js": "^2.0.1", + "@kie/mock-github": "^1.0.0", "@onfido/react-native-sdk": "7.4.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-camera-roll/camera-roll": "5.4.0", @@ -78,6 +83,7 @@ "@rnmapbox/maps": "^10.0.11", "@ua/react-native-airship": "^15.2.6", "awesome-phonenumber": "^5.4.0", + "@types/node": "^18.14.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", @@ -88,6 +94,7 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", "fbjs": "^3.0.2", + "focus-trap-react": "^10.2.1", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-when": "^3.5.2", @@ -125,7 +132,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.72", + "react-native-onyx": "1.0.76", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^4.0.0", @@ -168,6 +175,8 @@ "@babel/runtime": "^7.20.0", "@electron/notarize": "^1.2.3", "@jest/globals": "^29.5.0", + "@kie/act-js": "^2.0.1", + "@kie/mock-github": "^1.0.0", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", @@ -219,7 +228,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^25.8.0", + "electron": "^25.8.1", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -261,7 +270,8 @@ "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.3", "webpack-font-preload-plugin": "^1.5.0", - "webpack-merge": "^5.8.0" + "webpack-merge": "^5.8.0", + "yaml": "^2.2.1" }, "overrides": { "react-native": "$react-native" diff --git a/src/CONST.ts b/src/CONST.ts index 62b0dc4af07c..1ef2f3e83246 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -241,6 +241,7 @@ const CONST = { THREADS: 'threads', CUSTOM_STATUS: 'customStatus', NEW_DOT_CATEGORIES: 'newDotCategories', + NEW_DOT_TAGS: 'newDotTags', }, BUTTON_STATES: { DEFAULT: 'default', @@ -1090,6 +1091,29 @@ const CONST = { DEFAULT: 'en', }, + LANGUAGES: ['en', 'es'], + + PRONOUNS_LIST: [ + 'coCos', + 'eEyEmEir', + 'heHimHis', + 'heHimHisTheyThemTheirs', + 'sheHerHers', + 'sheHerHersTheyThemTheirs', + 'merMers', + 'neNirNirs', + 'neeNerNers', + 'perPers', + 'theyThemTheirs', + 'thonThons', + 'veVerVis', + 'viVir', + 'xeXemXyr', + 'zeZieZirHir', + 'zeHirHirs', + 'callMeByMyName', + ], + POLICY: { TYPE: { FREE: 'free', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ddc2aa4de620..2e0b75910bae 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -240,6 +240,8 @@ const ONYXKEYS = { POLICY_MEMBERS: 'policyMembers_', POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', + POLICY_TAGS: 'policyTags_', + POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', REPORT: 'report_', REPORT_ACTIONS: 'reportActions_', @@ -376,6 +378,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; + [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; @@ -390,6 +393,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: boolean; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; // Forms diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ed4fbb97a41a..3bbdf4709cfc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -98,6 +98,7 @@ export default { MONEY_REQUEST_CURRENCY: ':iouType/new/currency/:reportID?', MONEY_REQUEST_DESCRIPTION: ':iouType/new/description/:reportID?', MONEY_REQUEST_CATEGORY: ':iouType/new/category/:reportID?', + MONEY_REQUEST_TAG: ':iouType/new/tag/:reportID?', MONEY_REQUEST_MERCHANT: ':iouType/new/merchant/:reportID?', MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', @@ -117,6 +118,7 @@ export default { getMoneyRequestMerchantRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`, getMoneyRequestDistanceTabRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`, getMoneyRequestWaypointRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`, + getMoneyRequestTagRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`, SPLIT_BILL_DETAILS: `r/:reportID/split/:reportActionID`, getSplitBillDetailsRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`, getNewTaskRoute: (reportID: string) => `${NEW_TASK}/${reportID}`, @@ -135,10 +137,10 @@ export default { FLAG_COMMENT: `flag/:reportID/:reportActionID`, getFlagCommentRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`, SEARCH: 'search', - SAVE_THE_WORLD: 'save-the-world', - I_KNOW_A_TEACHER: 'save-the-world/i-know-a-teacher', - INTRO_SCHOOL_PRINCIPAL: 'save-the-world/intro-school-principal', - I_AM_A_TEACHER: 'save-the-world/i-am-a-teacher', + TEACHERS_UNITE: 'teachersunite', + I_KNOW_A_TEACHER: 'teachersunite/i-know-a-teacher', + INTRO_SCHOOL_PRINCIPAL: 'teachersunite/intro-school-principal', + I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', DETAILS: 'details', getDetailsRoute: (login: string) => `details?login=${encodeURIComponent(login)}`, PROFILE: 'a/:accountID', diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index c07a4474a68b..d39906faf3a3 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -25,7 +25,6 @@ import HeaderGap from './HeaderGap'; import SafeAreaConsumer from './SafeAreaConsumer'; import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL'; import reportPropTypes from '../pages/reportPropTypes'; -import tryResolveUrlFromApiRoot from '../libs/tryResolveUrlFromApiRoot'; /** * Modal render prop component that exposes modal launching triggers that can be used @@ -351,7 +350,7 @@ function AttachmentModal(props) { setShouldShowArrows(!shouldShowArrows) : undefined} /> ), - [activeSource, setShouldShowArrows, shouldShowArrows], + [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows], ); return ( diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js index f43a26ab94ee..64c97fa99819 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js +++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js @@ -2,9 +2,8 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import CONST from '../../../CONST'; import * as DeviceCapabilities from '../../../libs/DeviceCapabilities'; -const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - function useCarouselArrows() { + const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen); const autoHideArrowTimeout = useRef(null); @@ -25,7 +24,7 @@ function useCarouselArrows() { autoHideArrowTimeout.current = setTimeout(() => { setShouldShowArrowsInternal(false); }, CONST.ARROW_HIDE_DELAY); - }, [cancelAutoHideArrows]); + }, [canUseTouchScreen, cancelAutoHideArrows]); const setShouldShowArrows = useCallback( (show = true) => { diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js index e6ed1a77ce85..ac543d9921d2 100644 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -49,13 +49,16 @@ function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySele const countries = useMemo( () => - _.map(translate('allCountries'), (countryName, countryISO) => ({ - value: countryISO, - keyForList: countryISO, - text: countryName, - isSelected: currentCountry === countryISO, - searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), - })), + _.map(_.keys(CONST.ALL_COUNTRIES), (countryISO) => { + const countryName = translate(`allCountries.${countryISO}`); + return { + value: countryISO, + keyForList: countryISO, + text: countryName, + isSelected: currentCountry === countryISO, + searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), + }; + }), [translate, currentCountry], ); diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index c6bbae0dd645..c838ed8f4060 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -31,7 +31,6 @@ const defaultProps = { function CountryPicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); - const allCountries = translate('allCountries'); const [isPickerVisible, setIsPickerVisible] = useState(false); const [searchValue, setSearchValue] = useState(''); @@ -48,7 +47,7 @@ function CountryPicker({value, errorText, onInputChange, forwardedRef}) { hidePickerModal(); }; - const title = allCountries[value] || ''; + const title = value ? translate(`allCountries.${value}`) : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js new file mode 100644 index 000000000000..2dcab7b9d998 --- /dev/null +++ b/src/components/FocusTrapView/index.js @@ -0,0 +1,75 @@ +/* + * The FocusTrap is only used on web and desktop + */ +import React, {useEffect, useRef} from 'react'; +import FocusTrap from 'focus-trap-react'; +import {View} from 'react-native'; +import {PropTypes} from 'prop-types'; +import {useIsFocused} from '@react-navigation/native'; + +const propTypes = { + /** Children to wrap with FocusTrap */ + children: PropTypes.node.isRequired, + + /** Whether to enable the FocusTrap */ + enabled: PropTypes.bool, + + /** + * Whether to disable auto focus + * It is used when the component inside the FocusTrap have their own auto focus logic + */ + shouldEnableAutoFocus: PropTypes.bool, +}; + +const defaultProps = { + enabled: true, + shouldEnableAutoFocus: false, +}; + +function FocusTrapView({enabled, shouldEnableAutoFocus, ...props}) { + const isFocused = useIsFocused(); + + /** + * Focus trap always needs a focusable element. + * In case that we don't have any focusable elements in the modal, + * the FocusTrap will use fallback View element using this ref. + */ + const ref = useRef(null); + + /** + * We have to set the 'tabindex' attribute to 0 to make the View focusable. + * Currently, it is not possible to set this through props. + * After the upgrade of 'react-native-web' to version 0.19 we can use 'tabIndex={0}' prop instead. + */ + useEffect(() => { + if (!ref.current) { + return; + } + ref.current.setAttribute('tabindex', '0'); + }, []); + + return enabled ? ( + shouldEnableAutoFocus && ref.current, + fallbackFocus: () => ref.current, + clickOutsideDeactivates: true, + }} + > + + + ) : ( + props.children + ); +} + +FocusTrapView.displayName = 'FocusTrapView'; +FocusTrapView.propTypes = propTypes; +FocusTrapView.defaultProps = defaultProps; + +export default FocusTrapView; diff --git a/src/components/FocusTrapView/index.native.js b/src/components/FocusTrapView/index.native.js new file mode 100644 index 000000000000..5720601f5a2b --- /dev/null +++ b/src/components/FocusTrapView/index.native.js @@ -0,0 +1,11 @@ +/* + * The FocusTrap is only used on web and desktop + */ + +function FocusTrapView({children}) { + return children; +} + +FocusTrapView.displayName = 'FocusTrapView'; + +export default FocusTrapView; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index bfe39459ed74..74cf83a4a6f0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -1,4 +1,6 @@ -import React from 'react'; +import React, {memo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import Navigation from '../../../libs/Navigation/Navigation'; import htmlRendererPropTypes from './htmlRendererPropTypes'; import styles from '../../../styles/styles'; @@ -8,12 +10,15 @@ import CONST from '../../../CONST'; import {ShowContextMenuContext, showContextMenuForReport} from '../../ShowContextMenuContext'; import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot'; import * as ReportUtils from '../../../libs/ReportUtils'; -import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; import ROUTES from '../../../ROUTES'; +import ONYXKEYS from '../../../ONYXKEYS'; +import useLocalize from '../../../hooks/useLocalize'; -const propTypes = {...htmlRendererPropTypes, ...withLocalizePropTypes}; +const propTypes = {...htmlRendererPropTypes}; function ImageRenderer(props) { + const {translate} = useLocalize(); + const htmlAttribs = props.tnode.attributes; // There are two kinds of images that need to be displayed: @@ -72,7 +77,7 @@ function ImageRenderer(props) { ) } accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={props.translate('accessibilityHints.viewAttachment')} + accessibilityLabel={translate('accessibilityHints.viewAttachment')} > + lodashGet(prevProps, 'tnode.attributes') === lodashGet(nextProps, 'tnode.attributes') && + lodashGet(prevProps, 'user.shouldUseStagingServer') === lodashGet(nextProps, 'user.shouldUseStagingServer'), + ), +); diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index cc187164f116..75be8c67fbf1 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -257,10 +257,12 @@ function OptionRowLHN(props) { accessible={false} > {shouldShowGreenDotIndicator && ( - + + + )} {optionItem.hasDraftComment && optionItem.isAllowedToComment && ( ({ - value: key, - label: language.label, + const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({ + value: language, + label: props.translate(`languagePage.languages.${language}.label`), + keyForList: language, + isSelected: props.preferredLocale === language, })); return ( (({accessToken, style, ma styleURL={styleURL} onMapIdle={setMapIdle} pitchEnabled={pitchEnabled} + attributionPosition={{...styles.r2, ...styles.b2}} + logoPosition={{...styles.l2, ...styles.b2}} // eslint-disable-next-line {...responder.panHandlers} > diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 87dc0e6795b4..966f5f4340a7 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -8,6 +8,7 @@ import lodashGet from 'lodash/get'; import styles from '../styles/styles'; import * as ReportUtils from '../libs/ReportUtils'; import * as OptionsListUtils from '../libs/OptionsListUtils'; +import Permissions from '../libs/Permissions'; import OptionsSelector from './OptionsSelector'; import ONYXKEYS from '../ONYXKEYS'; import compose from '../libs/compose'; @@ -29,11 +30,11 @@ import Image from './Image'; import useLocalize from '../hooks/useLocalize'; import * as ReceiptUtils from '../libs/ReceiptUtils'; import categoryPropTypes from './categoryPropTypes'; +import tagPropTypes from './tagPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; import transactionPropTypes from './transactionPropTypes'; import DistanceRequestUtils from '../libs/DistanceRequestUtils'; import * as IOU from '../libs/actions/IOU'; -import Permissions from '../libs/Permissions'; const propTypes = { /** Callback to inform parent modal of success */ @@ -69,6 +70,9 @@ const propTypes = { /** IOU Category */ iouCategory: PropTypes.string, + /** IOU Tag */ + iouTag: PropTypes.string, + /** Selected participants from MoneyRequestModal with login / accountID */ selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired, @@ -109,10 +113,6 @@ const propTypes = { /** List styles for OptionsSelector */ listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /* Onyx Props */ - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - /** ID of the transaction that represents the money request */ transactionID: PropTypes.string, @@ -133,6 +133,19 @@ const propTypes = { /** Whether the money request is a distance request */ isDistanceRequest: PropTypes.bool, + + /* Onyx Props */ + /** Collection of categories attached to a policy */ + policyCategories: PropTypes.objectOf(categoryPropTypes), + + /** Collection of tags attached to a policy */ + policyTags: PropTypes.objectOf( + PropTypes.shape({ + name: PropTypes.string, + required: PropTypes.bool, + tags: PropTypes.objectOf(tagPropTypes), + }), + ), }; const defaultProps = { @@ -141,6 +154,7 @@ const defaultProps = { onSelectParticipant: () => {}, iouType: CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, iouCategory: '', + iouTag: '', payeePersonalDetails: null, canModifyParticipants: false, isReadOnly: false, @@ -156,6 +170,7 @@ const defaultProps = { receiptSource: '', listStyles: [], policyCategories: {}, + policyTags: {}, transactionID: '', transaction: {}, mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, @@ -178,6 +193,12 @@ function MoneyRequestConfirmationList(props) { const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; const shouldCategoryBeEditable = !_.isEmpty(props.policyCategories) && Permissions.canUseCategories(props.betas); + // Fetches the first tag list of the policy + const tagListKey = _.first(_.keys(props.policyTags)); + const tagList = lodashGet(props.policyTags, [tagListKey, 'tags'], []); + const tagListName = lodashGet(props.policyTags, [tagListKey, 'name'], ''); + const canUseTags = Permissions.canUseTags(props.betas); + const formattedAmount = CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, props.isDistanceRequest ? currency : props.iouCurrencyCode, @@ -499,6 +520,16 @@ function MoneyRequestConfirmationList(props) { disabled={didConfirm || props.isReadOnly} /> )} + {canUseTags && !!tagList && ( + Navigation.navigate(ROUTES.getMoneyRequestTagRoute(props.iouType, props.reportID))} + style={[styles.moneyRequestMenuItem, styles.mb2]} + disabled={didConfirm || props.isReadOnly} + /> + )} )} @@ -520,6 +551,9 @@ export default compose( policyCategories: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, }, + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, diff --git a/src/components/PlaidLink/index.native.js b/src/components/PlaidLink/index.native.js index cd17453820cf..48cd41e283c3 100644 --- a/src/components/PlaidLink/index.native.js +++ b/src/components/PlaidLink/index.native.js @@ -1,16 +1,13 @@ import {useEffect} from 'react'; import {openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; import Log from '../../libs/Log'; -import CONST from '../../CONST'; import {plaidLinkPropTypes, plaidLinkDefaultProps} from './plaidLinkPropTypes'; function PlaidLink(props) { useDeepLinkRedirector(); usePlaidEmitter((event) => { Log.info('[PlaidLink] Handled Plaid Event: ', false, event); - if (event.eventName === CONST.PLAID.EVENT.ERROR) { - props.onError(event.metadata); - } + props.onEvent(event.eventName, event.metadata); }); useEffect(() => { openLink({ @@ -20,9 +17,6 @@ function PlaidLink(props) { onSuccess: ({publicToken, metadata}) => { props.onSuccess({publicToken, metadata}); }, - onEvent: (event, metadata) => { - props.onEvent(event, metadata); - }, onExit: (exitError, metadata) => { Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); props.onExit(); diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.js b/src/components/Reactions/ReportActionItemEmojiReactions.js index ec2755f1a5dd..c1e1764ed9f1 100644 --- a/src/components/Reactions/ReportActionItemEmojiReactions.js +++ b/src/components/Reactions/ReportActionItemEmojiReactions.js @@ -42,7 +42,7 @@ const defaultProps = { function ReportActionItemEmojiReactions(props) { const {reactionListRef} = useContext(ReportScreenContext); - const popoverReactionListAnchor = useRef(null); + const popoverReactionListAnchors = useRef({}); let totalReactionCount = 0; // Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone @@ -95,7 +95,7 @@ function ReportActionItemEmojiReactions(props) { }; const onReactionListOpen = (event) => { - reactionListRef.current.showReactionList(event, popoverReactionListAnchor.current, reactionEmojiName, props.reportActionID); + reactionListRef.current.showReactionList(event, popoverReactionListAnchors.current[reactionEmojiName], reactionEmojiName, props.reportActionID); }; return { @@ -112,10 +112,7 @@ function ReportActionItemEmojiReactions(props) { return ( totalReactionCount > 0 && ( - + {_.map(formattedReactions, (reaction) => { if (reaction === null) { return; @@ -135,7 +132,7 @@ function ReportActionItemEmojiReactions(props) { > (popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref)} count={reaction.reactionCount} emojiCodes={reaction.emojiCodes} onPress={reaction.onPress} diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index f760e5d5aeb4..f0f8b8a4b09b 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -3,6 +3,7 @@ import React from 'react'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {PickerAvoidingView} from 'react-native-picker-select'; +import FocusTrapView from '../FocusTrapView'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; import CONST from '../../CONST'; import styles from '../../styles/styles'; @@ -124,20 +125,26 @@ class ScreenWrapper extends React.Component { style={styles.flex1} enabled={this.props.shouldEnablePickerAvoiding} > - - {this.props.environment === CONST.ENVIRONMENT.DEV && } - {this.props.environment === CONST.ENVIRONMENT.DEV && } - { - // If props.children is a function, call it to provide the insets to the children. - _.isFunction(this.props.children) - ? this.props.children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd: this.state.didScreenTransitionEnd, - }) - : this.props.children - } - {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && } + + + {this.props.environment === CONST.ENVIRONMENT.DEV && } + {this.props.environment === CONST.ENVIRONMENT.DEV && } + { + // If props.children is a function, call it to provide the insets to the children. + _.isFunction(this.props.children) + ? this.props.children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd: this.state.didScreenTransitionEnd, + }) + : this.props.children + } + {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && } + diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js index 83033d9e97b7..c3538b3c026d 100644 --- a/src/components/ScreenWrapper/propTypes.js +++ b/src/components/ScreenWrapper/propTypes.js @@ -48,6 +48,12 @@ const propTypes = { /** Styles for the offline indicator */ offlineIndicatorStyle: stylePropTypes, + + /** Whether to disable the focus trap */ + shouldDisableFocusTrap: PropTypes.bool, + + /** Whether to disable auto focus of the focus trap */ + shouldEnableAutoFocus: PropTypes.bool, }; const defaultProps = { @@ -63,6 +69,8 @@ const defaultProps = { shouldShowOfflineIndicator: true, offlineIndicatorStyle: [], headerGapStyles: [], + shouldDisableFocusTrap: false, + shouldEnableAutoFocus: false, }; export {propTypes, defaultProps}; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 67733c121ff0..27967fcf67f4 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -12,7 +12,7 @@ import CONST from '../../CONST'; import variables from '../../styles/variables'; import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; import RadioListItem from './RadioListItem'; -import CheckboxListItem from './CheckboxListItem'; +import UserListItem from './UserListItem'; import useKeyboardShortcut from '../../hooks/useKeyboardShortcut'; import SafeAreaConsumer from '../SafeAreaConsumer'; import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; @@ -243,16 +243,20 @@ function BaseSelectionList({ }; const renderItem = ({item, index, section}) => { + const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); const isDisabled = section.isDisabled; - const isItemFocused = !isDisabled && focusedIndex === index + lodashGet(section, 'indexOffset', 0); + const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; + // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. + const showTooltip = normalizedIndex < 10; if (canSelectMultiple) { return ( - selectRow(item, index)} onDismissError={onDismissError} + showTooltip={showTooltip} /> ); } diff --git a/src/components/SelectionList/CheckboxListItem.js b/src/components/SelectionList/UserListItem.js similarity index 57% rename from src/components/SelectionList/CheckboxListItem.js rename to src/components/SelectionList/UserListItem.js index 256182a38e8b..dd90fc750510 100644 --- a/src/components/SelectionList/CheckboxListItem.js +++ b/src/components/SelectionList/UserListItem.js @@ -5,7 +5,7 @@ import lodashGet from 'lodash/get'; import PressableWithFeedback from '../Pressable/PressableWithFeedback'; import styles from '../../styles/styles'; import Text from '../Text'; -import {checkboxListItemPropTypes} from './selectionListPropTypes'; +import {userListItemPropTypes} from './selectionListPropTypes'; import Avatar from '../Avatar'; import OfflineWithFeedback from '../OfflineWithFeedback'; import CONST from '../../CONST'; @@ -13,10 +13,39 @@ import * as StyleUtils from '../../styles/StyleUtils'; import Icon from '../Icon'; import * as Expensicons from '../Icon/Expensicons'; import themeColors from '../../styles/themes/default'; +import Tooltip from '../Tooltip'; +import UserDetailsTooltip from '../UserDetailsTooltip'; -function CheckboxListItem({item, isFocused = false, onSelectRow, onDismissError = () => {}}) { +function UserListItem({item, isFocused = false, showTooltip, onSelectRow, onDismissError = () => {}}) { const hasError = !_.isEmpty(item.errors); + const avatar = ( + + ); + + const text = ( + + {item.text} + + ); + + const alternateText = ( + + {item.alternateText} + + ); + return ( onDismissError(item)} @@ -53,29 +82,20 @@ function CheckboxListItem({item, isFocused = false, onSelectRow, onDismissError /> )} - {Boolean(item.avatar) && ( - - )} - - - {item.text} - - {Boolean(item.alternateText) && ( - - {item.alternateText} - - )} + {avatar} + + ) : ( + avatar + ))} + + {showTooltip ? {text} : text} + {Boolean(item.alternateText) && (showTooltip ? {alternateText} : alternateText)} {Boolean(item.rightElement) && item.rightElement} @@ -83,7 +103,7 @@ function CheckboxListItem({item, isFocused = false, onSelectRow, onDismissError ); } -CheckboxListItem.displayName = 'CheckboxListItem'; -CheckboxListItem.propTypes = checkboxListItemPropTypes; +UserListItem.displayName = 'UserListItem'; +UserListItem.propTypes = userListItemPropTypes; -export default CheckboxListItem; +export default UserListItem; diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index 1a4fd6c5ecab..0a3c1efdf6a3 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; import CONST from '../../CONST'; -const checkboxListItemPropTypes = { +const userListItemPropTypes = { /** The section list item */ item: PropTypes.shape({ /** Text to display */ @@ -46,6 +46,9 @@ const checkboxListItemPropTypes = { /** Whether this item is focused (for arrow key controls) */ isFocused: PropTypes.bool, + /** Whether this item should show Tooltip */ + showTooltip: PropTypes.bool.isRequired, + /** Callback to fire when the item is pressed */ onSelectRow: PropTypes.func.isRequired, @@ -90,7 +93,7 @@ const propTypes = { indexOffset: PropTypes.number, /** Array of options */ - data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape(checkboxListItemPropTypes.item), PropTypes.shape(radioListItemPropTypes.item)])), + data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape(userListItemPropTypes.item), PropTypes.shape(radioListItemPropTypes.item)])), /** Whether this section items disabled for selection */ isDisabled: PropTypes.bool, @@ -155,4 +158,4 @@ const propTypes = { showConfirmButton: PropTypes.bool, }; -export {propTypes, radioListItemPropTypes, checkboxListItemPropTypes}; +export {propTypes, radioListItemPropTypes, userListItemPropTypes}; diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js index ae8d3efdd27e..bea335840c53 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import React, {useMemo, useEffect} from 'react'; import PropTypes from 'prop-types'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import CONST from '../../CONST'; import Modal from '../Modal'; import HeaderWithBackButton from '../HeaderWithBackButton'; @@ -53,13 +54,17 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, const countryStates = useMemo( () => - _.map(translate('allStates'), (state) => ({ - value: state.stateISO, - keyForList: state.stateISO, - text: state.stateName, - isSelected: currentState === state.stateISO, - searchValue: StringUtils.sanitizeString(`${state.stateISO}${state.stateName}`), - })), + _.map(_.keys(COMMON_CONST.STATES), (state) => { + const stateName = translate(`allStates.${state}.stateName`); + const stateISO = translate(`allStates.${state}.stateISO`); + return { + value: stateISO, + keyForList: stateISO, + text: stateName, + isSelected: currentState === stateISO, + searchValue: StringUtils.sanitizeString(`${stateISO}${stateName}`), + }; + }), [translate, currentState], ); diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index a4d3ea5e8133..142654b82cd1 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -1,6 +1,8 @@ import React, {useState} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; +import _ from 'underscore'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import styles from '../../styles/styles'; import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; import useLocalize from '../../hooks/useLocalize'; @@ -35,7 +37,6 @@ const defaultProps = { function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { const {translate} = useLocalize(); - const allStates = translate('allStates'); const [isPickerVisible, setIsPickerVisible] = useState(false); const [searchValue, setSearchValue] = useState(''); @@ -52,7 +53,7 @@ function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { hidePickerModal(); }; - const title = allStates[value] ? allStates[value].stateName : ''; + const title = value && _.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js new file mode 100644 index 000000000000..25021bd817d7 --- /dev/null +++ b/src/components/TagPicker/index.js @@ -0,0 +1,90 @@ +import React, {useMemo} from 'react'; +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; +import styles from '../../styles/styles'; +import Navigation from '../../libs/Navigation/Navigation'; +import ROUTES from '../../ROUTES'; +import useLocalize from '../../hooks/useLocalize'; +import * as OptionsListUtils from '../../libs/OptionsListUtils'; +import OptionsSelector from '../OptionsSelector'; +import {propTypes, defaultProps} from './tagPickerPropTypes'; + +function TagPicker({policyTags, reportID, tag, iouType, iou}) { + const {translate} = useLocalize(); + + const selectedOptions = useMemo(() => { + if (!iou.tag) { + return []; + } + + return [ + { + name: iou.tag, + enabled: true, + }, + ]; + }, [iou.tag]); + + // Only shows one section, which will be the default behavior if there are + // less than 8 policy tags + // TODO: support sections with search + const sections = useMemo(() => { + const tagList = _.chain(lodashGet(policyTags, [tag, 'tags'], {})) + .values() + .map((t) => ({ + text: t.name, + keyForList: t.name, + tooltipText: t.name, + })) + .value(); + + return [ + { + data: tagList, + }, + ]; + }, [policyTags, tag]); + + const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, ''); + + const navigateBack = () => { + Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + }; + + const updateTag = () => { + // TODO: add logic to save the selected tag + navigateBack(); + }; + + return ( + + ); +} + +TagPicker.displayName = 'TagPicker'; +TagPicker.propTypes = propTypes; +TagPicker.defaultProps = defaultProps; + +export default withOnyx({ + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + policyRecentlyUsedTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, +})(TagPicker); diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js new file mode 100644 index 000000000000..ad57a0409f15 --- /dev/null +++ b/src/components/TagPicker/tagPickerPropTypes.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import tagPropTypes from '../tagPropTypes'; +import {iouPropTypes, iouDefaultProps} from '../../pages/iou/propTypes'; + +const propTypes = { + /** The report ID of the IOU */ + reportID: PropTypes.string.isRequired, + + /** The policyID we are getting tags for */ + policyID: PropTypes.string.isRequired, + + /** The name of tag list we are getting tags for */ + tag: PropTypes.string.isRequired, + + /** The type of IOU report, i.e. bill, request, send */ + iouType: PropTypes.string.isRequired, + + /** Callback to submit the selected tag */ + onSubmit: PropTypes.func, + + /* Onyx Props */ + /** Collection of tags attached to a policy */ + policyTags: PropTypes.objectOf( + PropTypes.shape({ + name: PropTypes.string, + tags: PropTypes.objectOf(tagPropTypes), + }), + ), + + /** List of recently used tags */ + policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), + + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: iouPropTypes, +}; + +const defaultProps = { + policyTags: {}, + policyRecentlyUsedTags: {}, + iou: iouDefaultProps, +}; + +export {propTypes, defaultProps}; diff --git a/src/components/UnreadActionIndicator.js b/src/components/UnreadActionIndicator.js index 5273ab52ec28..ff1090640570 100755 --- a/src/components/UnreadActionIndicator.js +++ b/src/components/UnreadActionIndicator.js @@ -10,6 +10,7 @@ function UnreadActionIndicator(props) { diff --git a/src/components/tagPropTypes.js b/src/components/tagPropTypes.js new file mode 100644 index 000000000000..29d913dcd035 --- /dev/null +++ b/src/components/tagPropTypes.js @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.shape({ + /** Name of a tag */ + name: PropTypes.string.isRequired, + + /** Flag that determines if a tag is active and able to be selected */ + enabled: PropTypes.bool.isRequired, + + /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ + 'GL Code': PropTypes.string, +}); diff --git a/src/languages/en.ts b/src/languages/en.ts index e54d3ff5fbca..03adaa1e66b1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -71,9 +71,20 @@ import type { SetTheRequestParams, UpdatedTheRequestParams, RemovedTheRequestParams, + TagSelectionParams, + TranslationBase, } from './types'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; +type StateValue = { + stateISO: string; + stateName: string; +}; + +type States = Record; + +type AllCountries = Record; + /* eslint-disable max-len */ export default { common: { @@ -233,6 +244,7 @@ export default { showMore: 'Show more', merchant: 'Merchant', category: 'Category', + tag: 'Tag', receipt: 'Receipt', replace: 'Replace', distance: 'Distance', @@ -534,6 +546,7 @@ export default { `changed the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, + tagSelection: ({tagName}: TagSelectionParams) => `Select a ${tagName} to add additional organization to your money`, error: { invalidSplit: 'Split amounts do not equal total amount', other: 'Unexpected error, please try again later', @@ -1661,8 +1674,8 @@ export default { createAccount: 'Create A New Account', logIn: 'Log In', }, - allStates: COMMON_CONST.STATES, - allCountries: CONST.ALL_COUNTRIES, + allStates: COMMON_CONST.STATES as States, + allCountries: CONST.ALL_COUNTRIES as AllCountries, accessibilityHints: { navigateToChatsList: 'Navigate back to chats list', chatWelcomeMessage: 'Chat welcome message', @@ -1756,4 +1769,4 @@ export default { selectSuggestedAddress: 'Please select a suggested address', }, }, -} as const; +} satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index 42ae5103b043..7315c42cebab 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -71,6 +71,8 @@ import type { SetTheRequestParams, UpdatedTheRequestParams, RemovedTheRequestParams, + TagSelectionParams, + EnglishTranslation, } from './types'; /* eslint-disable max-len */ @@ -232,6 +234,7 @@ export default { showMore: 'Mostrar más', merchant: 'Comerciante', category: 'Categoría', + tag: 'Etiqueta', receipt: 'Recibo', replace: 'Sustituir', distance: 'Distancia', @@ -536,6 +539,7 @@ export default { `cambío ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, + tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero`, error: { invalidSplit: 'La suma de las partes no equivale al monto total', other: 'Error inesperado, por favor inténtalo más tarde', @@ -2248,4 +2252,4 @@ export default { selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida', }, }, -}; +} satisfies EnglishTranslation; diff --git a/src/languages/translations.ts b/src/languages/translations.ts index a2d27baa26c9..d228394589b2 100644 --- a/src/languages/translations.ts +++ b/src/languages/translations.ts @@ -1,10 +1,50 @@ import en from './en'; import es from './es'; import esES from './es-ES'; +import type {TranslationBase, TranslationFlatObject} from './types'; + +/** + * Converts an object to it's flattened version. + * + * Ex: + * Input: { common: { yes: "Yes", no: "No" }} + * Output: { "common.yes": "Yes", "common.no": "No" } + */ +// Necessary to export so that it is accessible to the unit tests +// eslint-disable-next-line rulesdir/no-inline-named-export +export function flattenObject(obj: TranslationBase): TranslationFlatObject { + const result: Record = {}; + + const recursive = (data: TranslationBase, key: string): void => { + // If the data is a function or not a object (eg. a string or array), + // it's the final value for the key being built and there is no need + // for more recursion + if (typeof data === 'function' || Array.isArray(data) || !(typeof data === 'object' && !!data)) { + result[key] = data; + } else { + let isEmpty = true; + + // Recursive call to the keys and connect to the respective data + Object.keys(data).forEach((k) => { + isEmpty = false; + recursive(data[k] as TranslationBase, key ? `${key}.${k}` : k); + }); + + // Check for when the object is empty but a key exists, so that + // it defaults to an empty object + if (isEmpty && key) { + result[key] = ''; + } + } + }; + + recursive(obj, ''); + return result as TranslationFlatObject; +} export default { - en, - es, + en: flattenObject(en), + es: flattenObject(es), // eslint-disable-next-line @typescript-eslint/naming-convention 'es-ES': esES, }; diff --git a/src/languages/types.ts b/src/languages/types.ts index 7a698f912b14..565b5933b989 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -1,3 +1,5 @@ +import en from './en'; + type AddressLineParams = { lineNumber: number; }; @@ -190,7 +192,50 @@ type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string}; type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string}; +type TagSelectionParams = {tagName: string}; + +/* Translation Object types */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TranslationBaseValue = string | string[] | ((...args: any[]) => string); + +type TranslationBase = {[key: string]: TranslationBaseValue | TranslationBase}; + +/* Flat Translation Object types */ +// Flattens an object and returns concatenations of all the keys of nested objects +type FlattenObject = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [TKey in keyof TObject]: TObject[TKey] extends (...args: any[]) => any + ? `${TPrefix}${TKey & string}` + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + TObject[TKey] extends any[] + ? `${TPrefix}${TKey & string}` + : // eslint-disable-next-line @typescript-eslint/ban-types + TObject[TKey] extends object + ? FlattenObject + : `${TPrefix}${TKey & string}`; +}[keyof TObject]; + +// Retrieves a type for a given key path (calculated from the type above) +type TranslateType = TPath extends keyof TObject + ? TObject[TPath] + : TPath extends `${infer TKey}.${infer TRest}` + ? TKey extends keyof TObject + ? TranslateType + : never + : never; + +type EnglishTranslation = typeof en; + +type TranslationPaths = FlattenObject; + +type TranslationFlatObject = { + [TKey in TranslationPaths]: TranslateType; +}; + export type { + TranslationBase, + EnglishTranslation, + TranslationFlatObject, AddressLineParams, CharacterLimitParams, MaxParticipantsReachedParams, @@ -261,4 +306,5 @@ export type { SetTheRequestParams, UpdatedTheRequestParams, RemovedTheRequestParams, + TagSelectionParams, }; diff --git a/src/libs/Browser/index.js b/src/libs/Browser/index.ts similarity index 100% rename from src/libs/Browser/index.js rename to src/libs/Browser/index.ts diff --git a/src/libs/Browser/index.web.js b/src/libs/Browser/index.web.ts similarity index 88% rename from src/libs/Browser/index.web.js rename to src/libs/Browser/index.web.ts index 32f6392aef76..064358c6bed5 100644 --- a/src/libs/Browser/index.web.js +++ b/src/libs/Browser/index.web.ts @@ -5,12 +5,12 @@ import ROUTES from '../../ROUTES'; /** * Fetch browser name from UA string * - * @return {String} e.g. Chrome */ -function getBrowser() { +function getBrowser(): string { const {userAgent} = window.navigator; - let match = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))/i) || []; - let temp; + const match = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))/i) ?? []; + let temp: RegExpMatchArray | null; + let browserName = ''; if (/trident/i.test(match[1])) { return 'IE'; @@ -28,15 +28,14 @@ function getBrowser() { } } - match = match[1] ? match[1] : navigator.appName; - return match ? match.toLowerCase() : CONST.BROWSER.OTHER; + browserName = match[1] ?? navigator.appName; + return browserName ? browserName.toLowerCase() : CONST.BROWSER.OTHER; } /** * Whether the platform is a mobile browser. * https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent * - * @returns {Boolean} */ function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Silk|Opera Mini/i.test(navigator.userAgent); @@ -45,7 +44,6 @@ function isMobile() { /** * Checks if requesting user agent is Safari browser on a mobile device * - * @returns {Boolean} */ function isMobileSafari() { const userAgent = navigator.userAgent; @@ -55,7 +53,6 @@ function isMobileSafari() { /** * Checks if requesting user agent is Chrome browser on a mobile device * - * @returns {Boolean} */ function isMobileChrome() { const userAgent = navigator.userAgent; @@ -68,8 +65,6 @@ function isSafari() { /** * The session information needs to be passed to the Desktop app, and the only way to do that is by using query params. There is no other way to transfer the data. - * @param {String} shortLivedAuthToken - * @param {String} email */ function openRouteInDesktopApp(shortLivedAuthToken = '', email = '') { const params = new URLSearchParams(); @@ -95,8 +90,9 @@ function openRouteInDesktopApp(shortLivedAuthToken = '', email = '') { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); - iframe.contentWindow.location.href = expensifyDeeplinkUrl; - + if (iframe.contentWindow) { + iframe.contentWindow.location.href = expensifyDeeplinkUrl; + } // Since we're creating an iframe for Safari to handle deeplink, // we need to give Safari some time to open the pop-up window. // After that we can just remove the iframe. diff --git a/src/libs/CollectionUtils.js b/src/libs/CollectionUtils.js deleted file mode 100644 index 3625c68d2c39..000000000000 --- a/src/libs/CollectionUtils.js +++ /dev/null @@ -1,27 +0,0 @@ -import _ from 'underscore'; - -/** - * Return the highest item in a numbered collection - * - * e.g. {1: '1', 2: '2', 3: '3'} -> '3' - * - * @param {Object} object - * @returns {*} - */ -function lastItem(object = {}) { - const lastKey = _.last(_.keys(object)) || 0; - return object[lastKey]; -} - -/** - * Used to grab the id for a particular collection item's key. - * e.g. reportActions_1 -> 1 - * - * @param {String} key - * @returns {String} - */ -function extractCollectionItemID(key) { - return key.split('_')[1]; -} - -export {lastItem, extractCollectionItemID}; diff --git a/src/libs/CollectionUtils.ts b/src/libs/CollectionUtils.ts new file mode 100644 index 000000000000..ac47633c2e08 --- /dev/null +++ b/src/libs/CollectionUtils.ts @@ -0,0 +1,23 @@ +import {OnyxCollectionKey} from '../ONYXKEYS'; + +/** + * Return the highest item in a numbered collection + * + * e.g. {1: '1', 2: '2', 3: '3'} -> '3' + * + * Use this only for collections that are numbered in other cases it will return the last item in the object not the highest + */ +function lastItem(object: Record = {}): T | undefined { + const lastKey = Object.keys(object).pop() ?? 0; + return object[lastKey]; +} + +/** + * Used to grab the id for a particular collection item's key. + * e.g. reportActions_1 -> 1 + */ +function extractCollectionItemID(key: `${OnyxCollectionKey}${string}`): string { + return key.split('_')[1]; +} + +export {lastItem, extractCollectionItemID}; diff --git a/src/libs/IntlPolyfill/index.js b/src/libs/IntlPolyfill/index.js index 3925b98729a9..a99fc4cfed44 100644 --- a/src/libs/IntlPolyfill/index.js +++ b/src/libs/IntlPolyfill/index.js @@ -7,4 +7,5 @@ import polyfillNumberFormat from './polyfillNumberFormat'; export default function intlPolyfill() { // Just need to polyfill Intl.NumberFormat for web based platforms polyfillNumberFormat(); + require('@formatjs/intl-datetimeformat'); } diff --git a/src/libs/IntlPolyfill/index.native.js b/src/libs/IntlPolyfill/index.native.js index a628654fefea..4fb5ca585e25 100644 --- a/src/libs/IntlPolyfill/index.native.js +++ b/src/libs/IntlPolyfill/index.native.js @@ -9,6 +9,7 @@ export default function polyfill() { require('@formatjs/intl-getcanonicallocales/polyfill'); require('@formatjs/intl-locale/polyfill'); require('@formatjs/intl-pluralrules/polyfill'); + require('@formatjs/intl-datetimeformat'); polyfillNumberFormat(); polyfillListFormat(); } diff --git a/src/libs/LocaleDigitUtils.js b/src/libs/LocaleDigitUtils.ts similarity index 68% rename from src/libs/LocaleDigitUtils.js rename to src/libs/LocaleDigitUtils.ts index 23396f8d2fb6..e17620aa5427 100644 --- a/src/libs/LocaleDigitUtils.js +++ b/src/libs/LocaleDigitUtils.ts @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import _ from 'lodash'; import * as NumberFormatUtils from './NumberFormatUtils'; @@ -8,12 +8,12 @@ const INDEX_DECIMAL = 10; const INDEX_MINUS_SIGN = 11; const INDEX_GROUP = 12; -const getLocaleDigits = _.memoize((locale) => { - const localeDigits = _.clone(STANDARD_DIGITS); +const getLocaleDigits = _.memoize((locale: string): string[] => { + const localeDigits = [...STANDARD_DIGITS]; for (let i = 0; i <= 9; i++) { localeDigits[i] = NumberFormatUtils.format(locale, i); } - _.forEach(NumberFormatUtils.formatToParts(locale, 1000000.5), (part) => { + NumberFormatUtils.formatToParts(locale, 1000000.5).forEach((part) => { switch (part.type) { case 'decimal': localeDigits[INDEX_DECIMAL] = part.value; @@ -34,15 +34,13 @@ const getLocaleDigits = _.memoize((locale) => { /** * Gets the locale digit corresponding to a standard digit. * - * @param {String} locale - * @param {String} digit - Character of a single standard digit . It may be "0" ~ "9" (digits), + * @param digit - Character of a single standard digit . It may be "0" ~ "9" (digits), * "," (group separator), "." (decimal separator) or "-" (minus sign). - * @returns {String} * * @throws If `digit` is not a valid standard digit. */ -function toLocaleDigit(locale, digit) { - const index = _.indexOf(STANDARD_DIGITS, digit); +function toLocaleDigit(locale: string, digit: string): string { + const index = STANDARD_DIGITS.indexOf(digit); if (index < 0) { throw new Error(`"${digit}" must be in ${JSON.stringify(STANDARD_DIGITS)}`); } @@ -52,15 +50,13 @@ function toLocaleDigit(locale, digit) { /** * Gets the standard digit corresponding to a locale digit. * - * @param {String} locale - * @param {String} localeDigit - Character of a single locale digit. It may be **the localized version** of + * @param localeDigit - Character of a single locale digit. It may be **the localized version** of * "0" ~ "9" (digits), "," (group separator), "." (decimal separator) or "-" (minus sign). - * @returns {String} * * @throws If `localeDigit` is not a valid locale digit. */ -function fromLocaleDigit(locale, localeDigit) { - const index = _.indexOf(getLocaleDigits(locale), localeDigit); +function fromLocaleDigit(locale: string, localeDigit: string): string { + const index = getLocaleDigits(locale).indexOf(localeDigit); if (index < 0) { throw new Error(`"${localeDigit}" must be in ${JSON.stringify(getLocaleDigits(locale))}`); } diff --git a/src/libs/Localize/index.js b/src/libs/Localize/index.js index 9878873377b8..db371301f43f 100644 --- a/src/libs/Localize/index.js +++ b/src/libs/Localize/index.js @@ -38,7 +38,7 @@ function init() { * Return translated string for given locale and phrase * * @param {String} [desiredLanguage] eg 'en', 'es-ES' - * @param {String|Array} phraseKey + * @param {String} phraseKey * @param {Object} [phraseParameters] Parameters to supply if the phrase is a template literal. * @returns {String} */ @@ -47,15 +47,15 @@ function translate(desiredLanguage = CONST.LOCALES.DEFAULT, phraseKey, phrasePar let translatedPhrase; // Search phrase in full locale e.g. es-ES - const desiredLanguageDictionary = lodashGet(translations, desiredLanguage); - translatedPhrase = lodashGet(desiredLanguageDictionary, phraseKey); + const desiredLanguageDictionary = translations[desiredLanguage] || {}; + translatedPhrase = desiredLanguageDictionary[phraseKey]; if (translatedPhrase) { return Str.result(translatedPhrase, phraseParameters); } // Phrase is not found in full locale, search it in fallback language e.g. es - const fallbackLanguageDictionary = lodashGet(translations, languageAbbreviation); - translatedPhrase = lodashGet(fallbackLanguageDictionary, phraseKey); + const fallbackLanguageDictionary = translations[languageAbbreviation] || {}; + translatedPhrase = fallbackLanguageDictionary[phraseKey]; if (translatedPhrase) { return Str.result(translatedPhrase, phraseParameters); } @@ -64,8 +64,8 @@ function translate(desiredLanguage = CONST.LOCALES.DEFAULT, phraseKey, phrasePar } // Phrase is not translated, search it in default language (en) - const defaultLanguageDictionary = lodashGet(translations, CONST.LOCALES.DEFAULT, {}); - translatedPhrase = lodashGet(defaultLanguageDictionary, phraseKey); + const defaultLanguageDictionary = translations[CONST.LOCALES.DEFAULT] || {}; + translatedPhrase = defaultLanguageDictionary[phraseKey]; if (translatedPhrase) { return Str.result(translatedPhrase, phraseParameters); } diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 34dc089a621f..37b7087b6ad4 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -1,7 +1,6 @@ import React from 'react'; import Onyx, {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; -import moment from 'moment'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {View} from 'react-native'; @@ -65,7 +64,7 @@ Onyx.connect({ } timezone = lodashGet(val, [currentAccountID, 'timezone'], {}); - const currentTimezone = moment.tz.guess(true); + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // If the current timezone is different than the user's timezone, and their timezone is set to automatic // then update their timezone. diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 2adaf0397a2c..851f7aff3a8d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -90,6 +90,13 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator([ }, name: 'Money_Request_Category', }, + { + getComponent: () => { + const MoneyRequestTagPage = require('../../../pages/iou/MoneyRequestTagPage').default; + return MoneyRequestTagPage; + }, + name: 'Money_Request_Tag', + }, { getComponent: () => { const MoneyRequestMerchantPage = require('../../../pages/iou/MoneyRequestMerchantPage').default; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index ee3054e02f96..62aac8c48e25 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -265,7 +265,7 @@ export default { }, TeachersUnite: { screens: { - SaveTheWorld_Root: ROUTES.SAVE_THE_WORLD, + SaveTheWorld_Root: ROUTES.TEACHERS_UNITE, I_Know_A_Teacher: ROUTES.I_KNOW_A_TEACHER, Intro_School_Principal: ROUTES.INTRO_SCHOOL_PRINCIPAL, I_Am_A_Teacher: ROUTES.I_AM_A_TEACHER, @@ -318,6 +318,7 @@ export default { Money_Request_Currency: ROUTES.MONEY_REQUEST_CURRENCY, Money_Request_Description: ROUTES.MONEY_REQUEST_DESCRIPTION, Money_Request_Category: ROUTES.MONEY_REQUEST_CATEGORY, + Money_Request_Tag: ROUTES.MONEY_REQUEST_TAG, Money_Request_Merchant: ROUTES.MONEY_REQUEST_MERCHANT, Money_Request_Waypoint: ROUTES.MONEY_REQUEST_WAYPOINT, IOU_Send_Enable_Payments: ROUTES.IOU_SEND_ENABLE_PAYMENTS, diff --git a/src/libs/Permissions.js b/src/libs/Permissions.js index a916d52f5e0a..0294236b1cd7 100644 --- a/src/libs/Permissions.js +++ b/src/libs/Permissions.js @@ -94,6 +94,14 @@ function canUseCategories(betas) { return _.contains(betas, CONST.BETAS.NEW_DOT_CATEGORIES) || canUseAllBetas(betas); } +/** + * @param {Array} betas + * @returns {Boolean} + */ +function canUseTags(betas) { + return _.contains(betas, CONST.BETAS.NEW_DOT_TAGS) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. * @returns {Boolean} @@ -113,5 +121,6 @@ export default { canUseTasks, canUseCustomStatus, canUseCategories, + canUseTags, canUseLinkPreviews, }; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index d5442640e224..20337bd9802f 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -404,7 +404,7 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) { if (isReportMessageAttachment(message)) { return { lastMessageTranslationKey: CONST.TRANSLATION_KEYS.ATTACHMENT, - lastMessageText: CONST.TRANSLATION_KEYS.ATTACHMENT, + lastMessageText: CONST.ATTACHMENT_MESSAGE_TEXT, lastMessageHtml: CONST.TRANSLATION_KEYS.ATTACHMENT, }; } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 2dc6ca038072..3b3f7b976ba6 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2627,7 +2627,7 @@ function buildTransactionThread(reportAction, moneyRequestReportID) { participantAccountIDs, getTransactionReportName(reportAction), '', - lodashGet(getReport(reportAction.reportID), 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), + lodashGet(getReport(moneyRequestReportID), 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, '', @@ -3109,8 +3109,8 @@ function getMoneyRequestOptions(report, reportParticipants, betas) { return []; } - // Additional requests should be blocked for money request reports - if (isMoneyRequestReport(report)) { + // Additional requests should be blocked for money request reports if it is approved or reimbursed + if (isMoneyRequestReport(report) && (isReportApproved(report) || isSettled(report.reportID))) { return []; } diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 645670c880d9..fffe43b88ee9 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -1,5 +1,3 @@ -import moment from 'moment-timezone'; -import 'moment/locale/es'; import {AppState} from 'react-native'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; @@ -386,7 +384,7 @@ function openProfile(personalDetails) { if (lodashGet(oldTimezoneData, 'automatic', true)) { newTimezoneData = { automatic: true, - selected: moment.tz.guess(true), + selected: Intl.DateTimeFormat().resolvedOptions().timeZone, }; } diff --git a/src/libs/actions/CurrentDate.js b/src/libs/actions/CurrentDate.ts similarity index 74% rename from src/libs/actions/CurrentDate.js rename to src/libs/actions/CurrentDate.ts index 078b35a760cb..b19a3430e96f 100644 --- a/src/libs/actions/CurrentDate.js +++ b/src/libs/actions/CurrentDate.ts @@ -1,10 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; -/** - * @param {String} currentDate - */ -function setCurrentDate(currentDate) { +function setCurrentDate(currentDate: string) { Onyx.set(ONYXKEYS.CURRENT_DATE, currentDate); } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 5b79fb6ad4bb..c677fe8f6c28 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -599,8 +599,11 @@ function createDistanceRequest(report, participant, comment, created, transactio * @param {String} [category] */ function requestMoney(report, amount, currency, created, merchant, payeeEmail, payeeAccountID, participant, comment, receipt = undefined, category = undefined) { + // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function + const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; const {payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( - report, + currentChatReport, participant, comment, amount, @@ -636,7 +639,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p onyxData, ); resetMoneyRequestInfo(); - Navigation.dismissModal(chatReport.reportID); + Navigation.dismissModal(isMoneyRequestReport ? report.reportID : chatReport.reportID); Report.notifyNewAction(chatReport.reportID, payeeAccountID); } @@ -1090,7 +1093,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.report}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, value: iouReport, }, ]; @@ -1925,12 +1928,14 @@ function navigateToNextPage(iou, iouType, reportID, report) { // If a request is initiated on a report, skip the participants selection step and navigate to the confirmation page. if (report.reportID) { + // If the report is iou or expense report, we should get the chat report to set participant for request money + const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; // Reinitialize the participants when the money request ID in Onyx does not match the ID from params if (_.isEmpty(iou.participants) || shouldReset) { const currentUserAccountID = currentUserPersonalDetails.accountID; - const participants = ReportUtils.isPolicyExpenseChat(report) - ? [{reportID: report.reportID, isPolicyExpenseChat: true, selected: true}] - : _.chain(report.participantAccountIDs) + const participants = ReportUtils.isPolicyExpenseChat(chatReport) + ? [{reportID: chatReport.reportID, isPolicyExpenseChat: true, selected: true}] + : _.chain(chatReport.participantAccountIDs) .filter((accountID) => currentUserAccountID !== accountID) .map((accountID) => ({accountID, selected: true})) .value(); diff --git a/src/libs/actions/Modal.js b/src/libs/actions/Modal.ts similarity index 74% rename from src/libs/actions/Modal.js rename to src/libs/actions/Modal.ts index ea3bfc351593..ff09731f59a7 100644 --- a/src/libs/actions/Modal.js +++ b/src/libs/actions/Modal.ts @@ -1,25 +1,20 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; -let closeModal; -let onModalClose; +let closeModal: (isNavigating: boolean) => void; +let onModalClose: null | (() => void); /** * Allows other parts of the app to call modal close function - * - * @param {Function} [onClose] */ -function setCloseModal(onClose) { +function setCloseModal(onClose: () => void) { closeModal = onClose; } /** * Close modal in other parts of the app - * - * @param {Function} [onModalCloseCallback] - * @param {Boolean} isNavigating */ -function close(onModalCloseCallback, isNavigating = true) { +function close(onModalCloseCallback: () => void, isNavigating = true) { if (!closeModal) { // If modal is already closed, no need to wait for modal close. So immediately call callback. if (onModalCloseCallback) { @@ -42,20 +37,16 @@ function onModalDidClose() { /** * Allows other parts of the app to know when a modal has been opened or closed - * - * @param {Boolean} isVisible */ -function setModalVisibility(isVisible) { +function setModalVisibility(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {isVisible}); } /** * Allows other parts of app to know that an alert modal is about to open. * This will trigger as soon as a modal is opened but not yet visible while animation is running. - * - * @param {Boolean} isVisible */ -function willAlertModalBecomeVisible(isVisible) { +function willAlertModalBecomeVisible(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {willAlertModalBecomeVisible: isVisible}); } diff --git a/src/libs/addEncryptedAuthTokenToURL.js b/src/libs/addEncryptedAuthTokenToURL.ts similarity index 58% rename from src/libs/addEncryptedAuthTokenToURL.js rename to src/libs/addEncryptedAuthTokenToURL.ts index 827454fa2aa3..9bdb83c284ce 100644 --- a/src/libs/addEncryptedAuthTokenToURL.js +++ b/src/libs/addEncryptedAuthTokenToURL.ts @@ -1,19 +1,15 @@ -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; let encryptedAuthToken = ''; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (session) => (encryptedAuthToken = lodashGet(session, 'encryptedAuthToken', '')), + callback: (session) => (encryptedAuthToken = session?.encryptedAuthToken ?? ''), }); /** * Add encryptedAuthToken to this attachment URL - * - * @param {String} url - * @returns {String} */ -export default function (url) { +export default function (url: string) { return `${url}?encryptedAuthToken=${encodeURIComponent(encryptedAuthToken)}`; } diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index 3f9e9d2de201..c257e1db4191 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -13,14 +13,14 @@ type IsReportMessageAttachmentParams = { * @param reportActionMessage report action's message as text, html and translationKey */ export default function isReportMessageAttachment({text, html, translationKey}: IsReportMessageAttachmentParams): boolean { - if (translationKey) { - return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; - } - if (!text || !html) { return false; } + if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) { + return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; + } + const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); return text === CONST.ATTACHMENT_MESSAGE_TEXT && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/libs/tryResolveUrlFromApiRoot.js b/src/libs/tryResolveUrlFromApiRoot.ts similarity index 85% rename from src/libs/tryResolveUrlFromApiRoot.js rename to src/libs/tryResolveUrlFromApiRoot.ts index cc46f034e45b..f9aef09a00a7 100644 --- a/src/libs/tryResolveUrlFromApiRoot.js +++ b/src/libs/tryResolveUrlFromApiRoot.ts @@ -15,11 +15,10 @@ const ORIGIN_PATTERN = new RegExp(`^(${ORIGINS_TO_REPLACE.join('|')})`); * - Similarly for prod or staging URLs we replace the `https://www.expensify` * or `https://staging.expensify` part, with `https://{API_ROOT}` * - Unmatched URLs (non expensify) are returned with no modifications - * - * @param {String} url - * @returns {String} */ -export default function tryResolveUrlFromApiRoot(url) { +function tryResolveUrlFromApiRoot(url: string): string; +function tryResolveUrlFromApiRoot(url: number): number; +function tryResolveUrlFromApiRoot(url: string | number): string | number { // in native, when we import an image asset, it will have a number representation which can be used in `source` of Image // in this case we can skip the url resolving if (typeof url === 'number') { @@ -28,3 +27,5 @@ export default function tryResolveUrlFromApiRoot(url) { const apiRoot = ApiUtils.getApiRoot({shouldUseSecure: false}); return url.replace(ORIGIN_PATTERN, apiRoot); } + +export default tryResolveUrlFromApiRoot; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index b5ef85e14cbb..22cac40cf29c 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -141,7 +141,7 @@ function ProfilePage(props) { const navigateBackTo = lodashGet(props.route, 'params.backTo', ''); return ( - + Navigation.goBack(navigateBackTo)} diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index d76d10c9e52c..22133415cea8 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -224,7 +224,7 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul ({value, label}))} + items={_.map(_.keys(CONST.INCORPORATION_TYPES), (key) => ({value: key, label: translate(`companyStep.incorporationTypes.${key}`)}))} placeholder={{value: '', label: '-'}} defaultValue={getDefaultStateForField('incorporationType')} shouldSaveDraft diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 6505b604b614..dd42ed80c3d4 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {Component} from 'react'; +import React, {useCallback, useEffect, useState, useMemo} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; @@ -9,17 +9,15 @@ import * as ReportUtils from '../libs/ReportUtils'; import ONYXKEYS from '../ONYXKEYS'; import styles from '../styles/styles'; import Navigation from '../libs/Navigation/Navigation'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../components/withWindowDimensions'; import * as Report from '../libs/actions/Report'; import HeaderWithBackButton from '../components/HeaderWithBackButton'; import ScreenWrapper from '../components/ScreenWrapper'; import Timing from '../libs/actions/Timing'; import CONST from '../CONST'; -import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; -import compose from '../libs/compose'; import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; import Performance from '../libs/Performance'; +import useLocalize from '../hooks/useLocalize'; const propTypes = { /* Onyx Props */ @@ -32,11 +30,6 @@ const propTypes = { /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - - /** Window Dimensions Props */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, }; const defaultProps = { @@ -45,171 +38,157 @@ const defaultProps = { reports: {}, }; -class SearchPage extends Component { - constructor(props) { - super(props); +function SearchPage({betas, personalDetails, reports}) { + // Data for initialization (runs only on the first render) + const { + recentReports: initialRecentReports, + personalDetails: initialPersonalDetails, + userToInvite: initialUserToInvite, + // Ignoring the rule because in this case we need the data only initially + // eslint-disable-next-line react-hooks/exhaustive-deps + } = useMemo(() => OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas), []); + + const [searchValue, setSearchValue] = useState(''); + const [searchOptions, setSearchOptions] = useState({ + recentReports: initialRecentReports, + personalDetails: initialPersonalDetails, + userToInvite: initialUserToInvite, + }); + + const {translate} = useLocalize(); + + const updateOptions = useCallback(() => { + const { + recentReports: localRecentReports, + personalDetails: localPersonalDetails, + userToInvite: localUserToInvite, + } = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas); + + setSearchOptions({ + recentReports: localRecentReports, + personalDetails: localPersonalDetails, + userToInvite: localUserToInvite, + }); + }, [reports, personalDetails, searchValue, betas]); + + const debouncedUpdateOptions = useMemo(() => _.debounce(updateOptions, 75), [updateOptions]); + useEffect(() => { Timing.start(CONST.TIMING.SEARCH_RENDER); Performance.markStart(CONST.TIMING.SEARCH_RENDER); + }, []); - this.searchRendered = this.searchRendered.bind(this); - this.selectReport = this.selectReport.bind(this); - this.onChangeText = this.onChangeText.bind(this); - this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); - - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions(props.reports, props.personalDetails, '', props.betas); - - this.state = { - searchValue: '', - recentReports, - personalDetails, - userToInvite, - }; - } - - componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { - return; - } - this.updateOptions(); - } - - onChangeText(searchValue = '') { - this.setState({searchValue}, this.debouncedUpdateOptions); - } + useEffect(() => { + debouncedUpdateOptions(); + }, [searchValue, debouncedUpdateOptions]); /** * Returns the sections needed for the OptionsSelector * * @returns {Array} */ - getSections() { + const getSections = () => { const sections = []; let indexOffset = 0; - if (this.state.recentReports.length > 0) { + if (searchOptions.recentReports.length > 0) { sections.push({ - data: this.state.recentReports, + data: searchOptions.recentReports, shouldShow: true, indexOffset, }); - indexOffset += this.state.recentReports.length; + indexOffset += searchOptions.recentReports.length; } - if (this.state.personalDetails.length > 0) { + if (searchOptions.personalDetails.length > 0) { sections.push({ - data: this.state.personalDetails, + data: searchOptions.personalDetails, shouldShow: true, indexOffset, }); - indexOffset += this.state.recentReports.length; + indexOffset += searchOptions.recentReports.length; } - if (this.state.userToInvite) { + if (searchOptions.userToInvite) { sections.push({ - data: [this.state.userToInvite], + data: [searchOptions.userToInvite], shouldShow: true, indexOffset, }); } return sections; - } + }; - searchRendered() { + const searchRendered = () => { Timing.end(CONST.TIMING.SEARCH_RENDER); Performance.markEnd(CONST.TIMING.SEARCH_RENDER); - } - - updateOptions() { - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( - this.props.reports, - this.props.personalDetails, - this.state.searchValue.trim(), - this.props.betas, - ); - this.setState({ - userToInvite, - recentReports, - personalDetails, - }); - } + }; + + const onChangeText = (value = '') => { + setSearchValue(value); + }; /** * Reset the search value and redirect to the selected report * * @param {Object} option */ - selectReport(option) { + const selectReport = (option) => { if (!option) { return; } - if (option.reportID) { - this.setState( - { - searchValue: '', - }, - () => { - Navigation.dismissModal(option.reportID); - }, - ); + setSearchValue(''); + Navigation.dismissModal(option.reportID); } else { Report.navigateToAndOpenReport([option.login]); } - } - - render() { - const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); - const headerMessage = OptionsListUtils.getHeaderMessage( - this.state.recentReports.length + this.state.personalDetails.length !== 0, - Boolean(this.state.userToInvite), - this.state.searchValue, - ); - - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - - - - - )} - - ); - } + }; + + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); + const headerMessage = OptionsListUtils.getHeaderMessage( + searchOptions.recentReports.length + searchOptions.personalDetails.length !== 0, + Boolean(searchOptions.userToInvite), + searchValue, + ); + return ( + + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + <> + + + + + + )} + + ); } SearchPage.propTypes = propTypes; SearchPage.defaultProps = defaultProps; - -export default compose( - withLocalize, - withWindowDimensions, - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - }), -)(SearchPage); +SearchPage.displayName = 'SearchPage'; +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + betas: { + key: ONYXKEYS.BETAS, + }, +})(SearchPage); diff --git a/src/pages/TeachersUnite/ImTeacherPage.js b/src/pages/TeachersUnite/ImTeacherPage.js index f077c7724978..dbeba700d208 100644 --- a/src/pages/TeachersUnite/ImTeacherPage.js +++ b/src/pages/TeachersUnite/ImTeacherPage.js @@ -22,7 +22,7 @@ function ImTeacherPage() { Navigation.goBack(ROUTES.SAVE_THE_WORLD)} + onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} /> Navigation.goBack(ROUTES.SAVE_THE_WORLD)} + onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} />
Navigation.goBack(ROUTES.SAVE_THE_WORLD)} + onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} /> {({insets}) => ( <> diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index cb77a832705c..b96ad4f4bfef 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -224,6 +224,11 @@ function FloatingActionButtonAndPopover(props) { text: props.translate('iou.requestMoney'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)), }, + { + icon: Expensicons.Heart, + text: props.translate('sidebarScreen.saveTheWorld'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), + }, { icon: Expensicons.Receipt, text: props.translate('iou.splitBill'), diff --git a/src/pages/iou/MoneyRequestTagPage.js b/src/pages/iou/MoneyRequestTagPage.js new file mode 100644 index 000000000000..a1795d50df8a --- /dev/null +++ b/src/pages/iou/MoneyRequestTagPage.js @@ -0,0 +1,105 @@ +import React from 'react'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; +import compose from '../../libs/compose'; +import ROUTES from '../../ROUTES'; +import Navigation from '../../libs/Navigation/Navigation'; +import useLocalize from '../../hooks/useLocalize'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; +import TagPicker from '../../components/TagPicker'; +import Text from '../../components/Text'; +import tagPropTypes from '../../components/tagPropTypes'; +import ONYXKEYS from '../../ONYXKEYS'; +import reportPropTypes from '../reportPropTypes'; +import styles from '../../styles/styles'; + +const propTypes = { + /** Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + /** Route specific parameters used on this screen via route :iouType/new/tag/:reportID? */ + params: PropTypes.shape({ + /** The type of IOU report, i.e. bill, request, send */ + iouType: PropTypes.string, + + /** The report ID of the IOU */ + reportID: PropTypes.string, + }), + }).isRequired, + + /* Onyx props */ + /** The report currently being used */ + report: reportPropTypes, + + /** Collection of tags attached to a policy */ + policyTags: PropTypes.objectOf( + PropTypes.shape({ + name: PropTypes.string, + tags: PropTypes.objectOf(tagPropTypes), + }), + ), +}; + +const defaultProps = { + report: {}, + policyTags: {}, +}; + +function MoneyRequestTagPage({route, report, policyTags}) { + const {translate} = useLocalize(); + + const iouType = lodashGet(route, 'params.iouType', ''); + + // Fetches the first tag list of the policy + const tagListKey = _.first(_.keys(policyTags)); + const tagList = lodashGet(policyTags, tagListKey, {}); + const tagListName = lodashGet(tagList, 'name', ''); + + const navigateBack = () => { + Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, lodashGet(report, 'reportID', ''))); + }; + + return ( + + + {translate('iou.tagSelection', {tagListName} || translate('common.tag'))} + + + ); +} + +MoneyRequestTagPage.displayName = 'MoneyRequestTagPage'; +MoneyRequestTagPage.propTypes = propTypes; +MoneyRequestTagPage.defaultProps = defaultProps; + +export default compose( + withOnyx({ + iou: { + key: ONYXKEYS.IOU, + }, + }), + withOnyx({ + report: { + // Fetch report ID from IOU participants if no report ID is set in route + key: ({route, iou}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '') || lodashGet(iou, 'participants.0.reportID', '')}`, + }, + }), + withOnyx({ + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + }, + }), +)(MoneyRequestTagPage); diff --git a/src/pages/iou/propTypes/index.js b/src/pages/iou/propTypes/index.js index c7559323ebc1..5ecd00d11876 100644 --- a/src/pages/iou/propTypes/index.js +++ b/src/pages/iou/propTypes/index.js @@ -18,6 +18,9 @@ const iouPropTypes = PropTypes.shape({ /** The merchant name */ merchant: PropTypes.string, + /** The tag */ + tag: PropTypes.string, + /** Date that the request was created */ created: PropTypes.string, @@ -34,6 +37,7 @@ const iouDefaultProps = { currency: CONST.CURRENCY.USD, comment: '', merchant: '', + tag: '', created: '', participants: [], receiptPath: '', diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 91847061a18b..1c653271ea7d 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -284,6 +284,7 @@ function MoneyRequestConfirmPage(props) { iouComment={props.iou.comment} iouCurrencyCode={props.iou.currency} iouCategory={props.iou.category} + iouTag={props.iou.tag} onConfirm={createTransaction} onSendMoney={sendMoney} onSelectParticipant={(option) => { diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 968faa6e5f23..b44b956ac547 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -49,6 +49,7 @@ function MoneyRequestParticipantsPage(props) { const prevMoneyRequestId = useRef(props.iou.id); const iouType = useRef(lodashGet(props.route, 'params.iouType', '')); const reportID = useRef(lodashGet(props.route, 'params.reportID', '')); + const optionsSelectorRef = useRef(); const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, props.selectedTab); const navigateToNextStep = () => { @@ -88,6 +89,7 @@ function MoneyRequestParticipantsPage(props) { optionsSelectorRef.current && optionsSelectorRef.current.focus()} > {({safeAreaPaddingBottomStyle}) => ( @@ -104,6 +106,7 @@ function MoneyRequestParticipantsPage(props) { /> ) : ( (optionsSelectorRef.current = el)} onStepComplete={navigateToNextStep} onAddParticipants={IOU.setMoneyRequestParticipants} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index ef1d6565f595..693a55b14e07 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -11,11 +11,15 @@ import compose from '../../../../libs/compose'; import CONST from '../../../../CONST'; import personalDetailsPropType from '../../../personalDetailsPropType'; import reportPropTypes from '../../../reportPropTypes'; +import refPropTypes from '../../../../components/refPropTypes'; const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), + /** A ref to forward to options selector's text input */ + forwardedRef: refPropTypes, + /** Callback to inform parent modal of success */ onStepComplete: PropTypes.func.isRequired, @@ -41,6 +45,7 @@ const propTypes = { }; const defaultProps = { + forwardedRef: undefined, safeAreaPaddingBottomStyle: {}, personalDetails: {}, reports: {}, @@ -161,6 +166,8 @@ class MoneyRequestParticipantsSelector extends Component { return ( ( + + )), +); diff --git a/src/pages/settings/Preferences/LanguagePage.js b/src/pages/settings/Preferences/LanguagePage.js index 0eb93853726a..706fa3b62db6 100644 --- a/src/pages/settings/Preferences/LanguagePage.js +++ b/src/pages/settings/Preferences/LanguagePage.js @@ -7,6 +7,7 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import * as App from '../../../libs/actions/App'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; +import CONST from '../../../CONST'; import SelectionList from '../../../components/SelectionList'; const propTypes = { @@ -17,11 +18,11 @@ const propTypes = { }; function LanguagePage(props) { - const localesToLanguages = _.map(props.translate('languagePage.languages'), (language, key) => ({ - value: key, - text: language.label, - keyForList: key, - isSelected: props.preferredLocale === key, + const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({ + value: language, + text: props.translate(`languagePage.languages.${language}.label`), + keyForList: language, + isSelected: props.preferredLocale === language, })); return ( diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.js index b8bb74295567..9e30e99ff38f 100755 --- a/src/pages/settings/Preferences/PreferencesPage.js +++ b/src/pages/settings/Preferences/PreferencesPage.js @@ -40,9 +40,6 @@ function PreferencesPage(props) { const {isProduction} = useEnvironment(); const {translate, preferredLocale} = useLocalize(); - const priorityModes = translate('priorityModePage.priorityModes'); - const languages = translate('languagePage.languages'); - return ( Navigation.navigate(ROUTES.SETTINGS_PRIORITY_MODE)} /> Navigation.navigate(ROUTES.SETTINGS_LANGUAGE)} /> diff --git a/src/pages/settings/Preferences/PriorityModePage.js b/src/pages/settings/Preferences/PriorityModePage.js index b32987e242de..0c3d28fe9d81 100644 --- a/src/pages/settings/Preferences/PriorityModePage.js +++ b/src/pages/settings/Preferences/PriorityModePage.js @@ -26,12 +26,12 @@ const defaultProps = { }; function PriorityModePage(props) { - const priorityModes = _.map(props.translate('priorityModePage.priorityModes'), (mode, key) => ({ - value: key, - text: mode.label, - alternateText: mode.description, - keyForList: key, - isSelected: props.priorityMode === key, + const priorityModes = _.map(_.values(CONST.PRIORITY_MODE), (mode) => ({ + value: mode, + text: props.translate(`priorityModePage.priorityModes.${mode}.label`), + alternateText: props.translate(`priorityModePage.priorityModes.${mode}.description`), + keyForList: mode, + isSelected: props.priorityMode === mode, })); const updateMode = useCallback( diff --git a/src/pages/settings/Preferences/ThemePage.js b/src/pages/settings/Preferences/ThemePage.js index a260caa283e3..4e41d5fc7129 100644 --- a/src/pages/settings/Preferences/ThemePage.js +++ b/src/pages/settings/Preferences/ThemePage.js @@ -27,11 +27,11 @@ const defaultProps = { }; function ThemePage(props) { - const localesToThemes = _.map(props.translate('themePage.themes'), (theme, key) => ({ - value: key, - text: theme.label, - keyForList: key, - isSelected: (props.preferredTheme || CONST.THEME.DEFAULT) === key, + const localesToThemes = _.map(_.values(_.omit(CONST.THEME, 'DEFAULT')), (theme) => ({ + value: theme, + text: props.translate(`themePage.themes.${theme}.label`), + keyForList: theme, + isSelected: (props.preferredTheme || CONST.THEME.DEFAULT) === theme, })); return ( diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 1f89756353ff..b0cbe7c9cc32 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -57,7 +57,11 @@ function ProfilePage(props) { if (pronounsKey.startsWith(CONST.PRONOUNS.PREFIX)) { pronounsKey = pronounsKey.slice(CONST.PRONOUNS.PREFIX.length); } - return lodashGet(props.translate('pronouns'), pronounsKey, props.translate('profilePage.selectYourPronouns')); + + if (!pronounsKey) { + return props.translate('profilePage.selectYourPronouns'); + } + return props.translate(`pronouns.${pronounsKey}`); }; const currentUserDetails = props.currentUserPersonalDetails || {}; const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.js index 1dd7e1e1bdb2..5f8824046d69 100644 --- a/src/pages/settings/Profile/PronounsPage.js +++ b/src/pages/settings/Profile/PronounsPage.js @@ -27,23 +27,23 @@ function PronounsPage({currentUserPersonalDetails}) { const currentPronounsKey = currentPronouns.substring(CONST.PRONOUNS.PREFIX.length); const [searchValue, setSearchValue] = useState(() => { - const currentPronounsText = _.chain(translate('pronouns')) - .find((_value, key) => key === currentPronounsKey) + const currentPronounsText = _.chain(CONST.PRONOUNS_LIST) + .find((_value) => _value === currentPronounsKey) .value(); - return currentPronounsText || ''; + return currentPronounsText ? translate(`pronouns.${currentPronounsText}`) : ''; }); const filteredPronounsList = useMemo(() => { - const pronouns = _.chain(translate('pronouns')) - .map((value, key) => { - const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${key}`; + const pronouns = _.chain(CONST.PRONOUNS_LIST) + .map((value) => { + const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`; const isCurrentPronouns = fullPronounKey === currentPronouns; return { - text: value, + text: translate(`pronouns.${value}`), value: fullPronounKey, - keyForList: key, + keyForList: value, isSelected: isCurrentPronouns, }; }) diff --git a/src/pages/settings/Profile/TimezoneInitialPage.js b/src/pages/settings/Profile/TimezoneInitialPage.js index 7dfc9fb6f9de..94cb40a3f6f3 100644 --- a/src/pages/settings/Profile/TimezoneInitialPage.js +++ b/src/pages/settings/Profile/TimezoneInitialPage.js @@ -1,7 +1,6 @@ import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; -import moment from 'moment-timezone'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import ScreenWrapper from '../../../components/ScreenWrapper'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; @@ -37,7 +36,7 @@ function TimezoneInitialPage(props) { const updateAutomaticTimezone = (isAutomatic) => { PersonalDetails.updateAutomaticTimezone({ automatic: isAutomatic, - selected: isAutomatic ? moment.tz.guess() : timezone.selected, + selected: isAutomatic ? Intl.DateTimeFormat().resolvedOptions().timeZone : timezone.selected, }); }; diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js index 58a45f729e17..228bae5cbb6b 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.js +++ b/src/pages/settings/Profile/TimezoneSelectPage.js @@ -84,6 +84,7 @@ function TimezoneSelectPage(props) { onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_TIMEZONE)} /> ({ - value: key, - text: preference, - keyForList: key, + const notificationPreferenceOptions = _.map( + _.filter(_.values(CONST.REPORT.NOTIFICATION_PREFERENCE), (pref) => pref !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN), + (preference) => ({ + value: preference, + text: props.translate(`notificationPreferencesPage.notificationPreferences.${preference}`), + keyForList: preference, - // Include the green checkmark icon to indicate the currently selected value - customIcon: key === props.report.notificationPreference ? greenCheckmark : null, + // Include the green checkmark icon to indicate the currently selected value + customIcon: preference === props.report.notificationPreference ? greenCheckmark : null, - // This property will make the currently selected value have bold text - boldStyle: key === props.report.notificationPreference, - })); + // This property will make the currently selected value have bold text + boldStyle: preference === props.report.notificationPreference, + }), + ); return ( diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 53ddd0fb5d96..5110bed598f1 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -307,6 +307,7 @@ function WorkspaceMembersPage(props) { result.push({ keyForList: accountID, + accountID: Number(accountID), isSelected: _.contains(selectedEmployees, Number(accountID)), isDisabled: accountID === props.session.accountID || details.login === props.policy.owner || policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, text: props.formatPhoneNumber(details.displayName), diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 8945bc0be058..0701adb83313 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -259,9 +259,12 @@ function getSafeAreaMargins(insets?: EdgeInsets): ViewStyle | CSSProperties { function getZoomCursorStyle(isZoomed: boolean, isDragging: boolean): ViewStyle | CSSProperties { if (!isZoomed) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return styles.cursorZoomIn; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isDragging ? styles.cursorGrabbing : styles.cursorZoomOut; } @@ -336,12 +339,16 @@ function getWidthStyle(width: number): ViewStyle | CSSProperties { */ function getAutoGrowHeightInputStyle(textInputHeight: number, maxHeight: number): ViewStyle | CSSProperties { if (textInputHeight > maxHeight) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.pr0, ...styles.overflowAuto, }; } + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.pr0, ...styles.overflowHidden, @@ -439,11 +446,17 @@ function getBackgroundColorWithOpacityStyle(backgroundColor: string, opacity: nu function getBadgeColorStyle(success: boolean, error: boolean, isPressed = false, isAdHoc = false): ViewStyle | CSSProperties { if (success) { if (isAdHoc) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isPressed ? styles.badgeAdHocSuccessPressed : styles.badgeAdHocSuccess; } + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isPressed ? styles.badgeSuccessPressed : styles.badgeSuccess; } if (error) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isPressed ? styles.badgeDangerPressed : styles.badgeDanger; } return {}; @@ -593,6 +606,8 @@ function getLoginPagePromoStyle(): ViewStyle | CSSProperties { * Generate the styles for the ReportActionItem wrapper view. */ function getReportActionItemStyle(isHovered = false, isLoading = false): ViewStyle | CSSProperties { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { display: 'flex', justifyContent: 'space-between', @@ -609,6 +624,8 @@ function getReportActionItemStyle(isHovered = false, isLoading = false): ViewSty * Generate the wrapper styles for the mini ReportActionContextMenu. */ function getMiniReportActionContextMenuWrapperStyle(isReportActionItemGrouped: boolean): ViewStyle | CSSProperties { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), ...positioning.r4, @@ -1043,6 +1060,8 @@ function displayIfTrue(condition: boolean): ViewStyle | CSSProperties { function getGoogleListViewStyle(shouldDisplayBorder: boolean): ViewStyle | CSSProperties { if (shouldDisplayBorder) { + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.borderTopRounded, ...styles.borderBottomRounded, @@ -1150,6 +1169,8 @@ function getDisabledLinkStyles(isDisabled = false): ViewStyle | CSSProperties { ...cursor.cursorDisabled, }; + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.link, ...(isDisabled ? disabledLinkStyles : {}), diff --git a/src/styles/ThemeStylesProvider.tsx b/src/styles/ThemeStylesProvider.tsx index d8a081572644..d0db784ca8ca 100644 --- a/src/styles/ThemeStylesProvider.tsx +++ b/src/styles/ThemeStylesProvider.tsx @@ -1,8 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ import React, {useMemo} from 'react'; import useTheme from './themes/useTheme'; -import StylesContext from './ThemeStylesContext'; -import defaultStyles from './styles'; +import ThemeStylesContext from './ThemeStylesContext'; +// TODO: Rename this to "styles" once the app is migrated to theme switching hooks and HOCs +import {stylesGenerator as stylesUntyped} from './styles'; + +const styles = stylesUntyped as (theme: Record) => Record; type ThemeStylesProviderProps = { children: React.ReactNode; @@ -11,23 +14,9 @@ type ThemeStylesProviderProps = { function ThemeStylesProvider({children}: ThemeStylesProviderProps) { const theme = useTheme(); - const appContentStyle = useMemo( - () => ({ - ...defaultStyles.appContent, - backgroundColor: theme.appBG, - }), - [theme.appBG], - ); - - const styles = useMemo( - () => ({ - ...defaultStyles, - appContent: appContentStyle, - }), - [appContentStyle], - ); + const themeStyles = useMemo(() => styles(theme), [theme]); - return {children}; + return {children}; } ThemeStylesProvider.displayName = 'ThemeStylesProvider'; diff --git a/src/styles/cardStyles/index.js b/src/styles/cardStyles/index.js deleted file mode 100644 index d8af8a627ffe..000000000000 --- a/src/styles/cardStyles/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Get card style for cardStyleInterpolator - * @param {Number} screenWidth - * @returns {Object} - */ -export default function getCardStyles(screenWidth) { - return { - position: 'fixed', - width: screenWidth, - height: '100%', - }; -} diff --git a/src/styles/cardStyles/index.native.js b/src/styles/cardStyles/index.native.js deleted file mode 100644 index fef33f500708..000000000000 --- a/src/styles/cardStyles/index.native.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getCardStyles() { - return {}; -} diff --git a/src/styles/cardStyles/index.native.ts b/src/styles/cardStyles/index.native.ts new file mode 100644 index 000000000000..94b8758efd05 --- /dev/null +++ b/src/styles/cardStyles/index.native.ts @@ -0,0 +1,5 @@ +import GetCardStyles from './types'; + +const getCardStyles: GetCardStyles = () => ({}); + +export default getCardStyles; diff --git a/src/styles/cardStyles/index.ts b/src/styles/cardStyles/index.ts new file mode 100644 index 000000000000..823081b62904 --- /dev/null +++ b/src/styles/cardStyles/index.ts @@ -0,0 +1,12 @@ +import GetCardStyles from './types'; + +/** + * Get card style for cardStyleInterpolator + */ +const getCardStyles: GetCardStyles = (screenWidth) => ({ + position: 'fixed', + width: screenWidth, + height: '100%', +}); + +export default getCardStyles; diff --git a/src/styles/cardStyles/types.ts b/src/styles/cardStyles/types.ts new file mode 100644 index 000000000000..e1598b7696ff --- /dev/null +++ b/src/styles/cardStyles/types.ts @@ -0,0 +1,6 @@ +import {CSSProperties} from 'react'; +import {ViewStyle} from 'react-native'; + +type GetCardStyles = (screenWidth: number) => Partial>; + +export default GetCardStyles; diff --git a/src/styles/getReportActionContextMenuStyles.ts b/src/styles/getReportActionContextMenuStyles.ts index 17f0828ab80c..9c0e159eb5fe 100644 --- a/src/styles/getReportActionContextMenuStyles.ts +++ b/src/styles/getReportActionContextMenuStyles.ts @@ -35,6 +35,8 @@ function getReportActionContextMenuStyles(isMini: boolean, isSmallScreenWidth: b return miniWrapperStyle; } + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return [ ...bigWrapperStyle, diff --git a/src/styles/styles.js b/src/styles/styles.js index 29d4541c9b4a..e81e03726c78 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2,7 +2,7 @@ import {defaultStyles as defaultPickerStyles} from 'react-native-picker-select/s import lodashClamp from 'lodash/clamp'; import fontFamily from './fontFamily'; import addOutlineWidth from './addOutlineWidth'; -import themeColors from './themes/default'; +import defaultTheme from './themes/default'; import fontWeightBold from './fontWeight/bold'; import variables from './variables'; import spacing from './utilities/spacing'; @@ -31,9 +31,9 @@ import Colors from './colors'; // touchCallout is an iOS safari only property that controls the display of the callout information when you touch and hold a target const touchCalloutNone = Browser.isMobileSafari() ? {WebkitTouchCallout: 'none'} : {}; -const picker = { - backgroundColor: themeColors.transparent, - color: themeColors.text, +const picker = (theme) => ({ + backgroundColor: theme.transparent, + color: theme.text, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeNormal, lineHeight: variables.fontSizeNormalHeight, @@ -44,27 +44,27 @@ const picker = { height: variables.inputHeight, borderWidth: 0, textAlign: 'left', -}; +}); -const link = { - color: themeColors.link, - textDecorationColor: themeColors.link, +const link = (theme) => ({ + color: theme.link, + textDecorationColor: theme.link, fontFamily: fontFamily.EXP_NEUE, -}; +}); -const baseCodeTagStyles = { +const baseCodeTagStyles = (theme) => ({ borderWidth: 1, borderRadius: 5, - borderColor: themeColors.border, - backgroundColor: themeColors.textBackground, -}; + borderColor: theme.border, + backgroundColor: theme.textBackground, +}); const headlineFont = { fontFamily: fontFamily.EXP_NEW_KANSAS_MEDIUM, fontWeight: '500', }; -const webViewStyles = { +const webViewStyles = (theme) => ({ // As of react-native-render-html v6, don't declare distinct styles for // custom renderers, the API for custom renderers has changed. Declare the // styles in the below "tagStyles" instead. If you need to reuse those @@ -86,7 +86,7 @@ const webViewStyles = { fontWeight: 'bold', }, - a: link, + a: link(theme), ul: { maxWidth: '100%', @@ -101,7 +101,7 @@ const webViewStyles = { }, blockquote: { - borderLeftColor: themeColors.border, + borderLeftColor: theme.border, borderLeftWidth: 4, paddingLeft: 12, marginTop: 4, @@ -112,7 +112,7 @@ const webViewStyles = { }, pre: { - ...baseCodeTagStyles, + ...baseCodeTagStyles(theme), paddingTop: 12, paddingBottom: 12, paddingRight: 8, @@ -123,7 +123,7 @@ const webViewStyles = { }, code: { - ...baseCodeTagStyles, + ...baseCodeTagStyles(theme), ...codeStyles.codeTextStyle, paddingLeft: 5, paddingRight: 5, @@ -132,7 +132,7 @@ const webViewStyles = { }, img: { - borderColor: themeColors.border, + borderColor: theme.border, borderRadius: variables.componentBorderRadiusNormal, borderWidth: 1, ...touchCalloutNone, @@ -149,15 +149,15 @@ const webViewStyles = { }, baseFontStyle: { - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeNormal, fontFamily: fontFamily.EXP_NEUE, flex: 1, lineHeight: variables.fontSizeNormalHeight, }, -}; +}); -const styles = { +const styles = (theme) => ({ // Add all of our utility and helper styles ...spacing, ...sizing, @@ -170,8 +170,8 @@ const styles = { ...writingDirection, ...cursor, ...userSelect, - ...themeColors, ...textUnderline, + ...theme, // TODO: Should we do this? rateCol: { margin: 0, @@ -180,10 +180,10 @@ const styles = { }, autoCompleteSuggestionsContainer: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, borderRadius: 8, borderWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, justifyContent: 'center', boxShadow: variables.popoverMenuShadow, position: 'absolute', @@ -227,7 +227,7 @@ const styles = { }, mentionSuggestionsHandle: { - color: themeColors.textSupporting, + color: theme.textSupporting, }, appIconBorderRadius: { @@ -242,30 +242,30 @@ const styles = { flexBasis: '48%', }, - webViewStyles, + webViewStyles: webViewStyles(theme), - link, + link: link(theme), linkMuted: { - color: themeColors.textSupporting, - textDecorationColor: themeColors.textSupporting, + color: theme.textSupporting, + textDecorationColor: theme.textSupporting, fontFamily: fontFamily.EXP_NEUE, }, linkMutedHovered: { - color: themeColors.textMutedReversed, + color: theme.textMutedReversed, }, highlightBG: { - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, }, appBG: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, }, h1: { - color: themeColors.heading, + color: theme.heading, fontFamily: fontFamily.EXP_NEUE_BOLD, fontSize: variables.fontSizeh1, fontWeight: fontWeightBold, @@ -305,13 +305,13 @@ const styles = { }, textLabel: { - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeLabel, lineHeight: variables.lineHeightLarge, }, mutedTextLabel: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeLabel, lineHeight: variables.lineHeightLarge, }, @@ -323,7 +323,7 @@ const styles = { }, textMicroBold: { - color: themeColors.text, + color: theme.text, fontWeight: fontWeightBold, fontFamily: fontFamily.EXP_NEUE_BOLD, fontSize: variables.fontSizeSmall, @@ -331,14 +331,14 @@ const styles = { }, textMicroSupporting: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightSmall, }, textExtraSmallSupporting: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeExtraSmall, }, @@ -382,7 +382,7 @@ const styles = { textHeadline: { ...headlineFont, ...whiteSpace.preWrap, - color: themeColors.heading, + color: theme.heading, fontSize: variables.fontSizeXLarge, lineHeight: variables.lineHeightXXLarge, }, @@ -390,7 +390,7 @@ const styles = { textHeadlineH1: { ...headlineFont, ...whiteSpace.preWrap, - color: themeColors.heading, + color: theme.heading, fontSize: variables.fontSizeh1, lineHeight: variables.lineHeightSizeh1, }, @@ -400,11 +400,11 @@ const styles = { }, textWhite: { - color: themeColors.textLight, + color: theme.textLight, }, textBlue: { - color: themeColors.link, + color: theme.link, }, textUppercase: { @@ -416,19 +416,19 @@ const styles = { }, colorReversed: { - color: themeColors.textReversed, + color: theme.textReversed, }, colorMutedReversed: { - color: themeColors.textMutedReversed, + color: theme.textMutedReversed, }, colorMuted: { - color: themeColors.textSupporting, + color: theme.textSupporting, }, colorHeading: { - color: themeColors.heading, + color: theme.heading, }, bgTransparent: { @@ -436,7 +436,7 @@ const styles = { }, bgDark: { - backgroundColor: themeColors.inverse, + backgroundColor: theme.inverse, }, opacity0: { @@ -448,7 +448,7 @@ const styles = { }, textDanger: { - color: themeColors.danger, + color: theme.danger, }, borderRadiusNormal: { @@ -456,7 +456,7 @@ const styles = { }, button: { - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, borderRadius: variables.buttonBorderRadius, minHeight: variables.componentSizeLarge, justifyContent: 'center', @@ -469,7 +469,7 @@ const styles = { }, buttonText: { - color: themeColors.text, + color: theme.text, fontFamily: fontFamily.EXP_NEUE_BOLD, fontSize: variables.fontSizeNormal, fontWeight: fontWeightBold, @@ -490,7 +490,7 @@ const styles = { paddingTop: 4, paddingHorizontal: 14, paddingBottom: 4, - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, }, buttonMedium: { @@ -500,7 +500,7 @@ const styles = { paddingRight: 16, paddingBottom: 12, paddingLeft: 16, - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, }, buttonLarge: { @@ -510,7 +510,7 @@ const styles = { paddingRight: 10, paddingBottom: 8, paddingLeft: 18, - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, }, buttonSmallText: { @@ -535,12 +535,12 @@ const styles = { }, buttonDefaultHovered: { - backgroundColor: themeColors.buttonHoveredBG, + backgroundColor: theme.buttonHoveredBG, borderWidth: 0, }, buttonSuccess: { - backgroundColor: themeColors.success, + backgroundColor: theme.success, borderWidth: 0, }, @@ -549,29 +549,29 @@ const styles = { }, buttonSuccessHovered: { - backgroundColor: themeColors.successHover, + backgroundColor: theme.successHover, borderWidth: 0, }, buttonDanger: { - backgroundColor: themeColors.danger, + backgroundColor: theme.danger, borderWidth: 0, }, buttonDangerHovered: { - backgroundColor: themeColors.dangerHover, + backgroundColor: theme.dangerHover, borderWidth: 0, }, buttonDisabled: { - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, borderWidth: 0, }, buttonDivider: { height: variables.dropDownButtonDividerHeight, borderWidth: 0.7, - borderColor: themeColors.text, + borderColor: theme.text, }, noBorderRadius: { @@ -616,19 +616,19 @@ const styles = { }, buttonSuccessText: { - color: themeColors.textLight, + color: theme.textLight, }, buttonDangerText: { - color: themeColors.textLight, + color: theme.textLight, }, hoveredComponentBG: { - backgroundColor: themeColors.hoverComponentBG, + backgroundColor: theme.hoverComponentBG, }, activeComponentBG: { - backgroundColor: themeColors.activeComponentBG, + backgroundColor: theme.activeComponentBG, }, fontWeightBold: { @@ -658,7 +658,7 @@ const styles = { height: 140, }, - pickerSmall: (backgroundColor = themeColors.highlightBG) => ({ + pickerSmall: (backgroundColor = theme.highlightBG) => ({ inputIOS: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeSmall, @@ -667,23 +667,23 @@ const styles = { paddingTop: 6, paddingBottom: 6, borderWidth: 0, - color: themeColors.text, + color: theme.text, height: 26, opacity: 1, backgroundColor: 'transparent', }, done: { - color: themeColors.text, + color: theme.text, }, doneDepressed: { fontSize: defaultPickerStyles.done.fontSize, }, modalViewMiddle: { - backgroundColor: themeColors.border, + backgroundColor: theme.border, borderTopWidth: 0, }, modalViewBottom: { - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, }, inputWeb: { fontFamily: fontFamily.EXP_NEUE, @@ -693,7 +693,7 @@ const styles = { paddingTop: 6, paddingBottom: 6, borderWidth: 0, - color: themeColors.text, + color: theme.text, appearance: 'none', height: 26, opacity: 1, @@ -708,7 +708,7 @@ const styles = { paddingTop: 6, paddingBottom: 6, borderWidth: 0, - color: themeColors.text, + color: theme.text, height: 26, opacity: 1, backgroundColor: 'transparent', @@ -724,7 +724,7 @@ const styles = { }), badge: { - backgroundColor: themeColors.border, + backgroundColor: theme.border, borderRadius: 14, height: variables.iconSizeNormal, flexDirection: 'row', @@ -733,31 +733,31 @@ const styles = { }, badgeSuccess: { - backgroundColor: themeColors.success, + backgroundColor: theme.success, }, badgeSuccessPressed: { - backgroundColor: themeColors.successHover, + backgroundColor: theme.successHover, }, badgeAdHocSuccess: { - backgroundColor: themeColors.badgeAdHoc, + backgroundColor: theme.badgeAdHoc, }, badgeAdHocSuccessPressed: { - backgroundColor: themeColors.badgeAdHocHover, + backgroundColor: theme.badgeAdHocHover, }, badgeDanger: { - backgroundColor: themeColors.danger, + backgroundColor: theme.danger, }, badgeDangerPressed: { - backgroundColor: themeColors.dangerPressed, + backgroundColor: theme.dangerPressed, }, badgeText: { - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightNormal, ...whiteSpace.noWrap, @@ -766,37 +766,37 @@ const styles = { border: { borderWidth: 1, borderRadius: variables.componentBorderRadius, - borderColor: themeColors.border, + borderColor: theme.border, }, borderColorFocus: { - borderColor: themeColors.borderFocus, + borderColor: theme.borderFocus, }, borderColorDanger: { - borderColor: themeColors.danger, + borderColor: theme.danger, }, textInputDisabled: { // Adding disabled color theme to indicate user that the field is not editable. - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, borderBottomWidth: 2, - borderColor: themeColors.borderLighter, + borderColor: theme.borderLighter, // Adding browser specefic style to bring consistency between Safari and other platforms. // Applying the Webkit styles only to browsers as it is not available in native. ...(Browser.getBrowser() ? { - WebkitTextFillColor: themeColors.textSupporting, + WebkitTextFillColor: theme.textSupporting, WebkitOpacity: 1, } : {}), - color: themeColors.textSupporting, + color: theme.textSupporting, }, uploadReceiptView: (isSmallScreenWidth) => ({ borderRadius: variables.componentBorderRadiusLarge, borderWidth: isSmallScreenWidth ? 0 : 2, - borderColor: themeColors.borderFocus, + borderColor: theme.borderFocus, borderStyle: 'dotted', marginBottom: 20, marginLeft: 20, @@ -827,14 +827,14 @@ const styles = { }, headerAnonymousFooter: { - color: themeColors.heading, + color: theme.heading, fontFamily: fontFamily.EXP_NEW_KANSAS_MEDIUM, fontSize: variables.fontSizeXLarge, lineHeight: variables.lineHeightXXLarge, }, headerText: { - color: themeColors.heading, + color: theme.heading, fontFamily: fontFamily.EXP_NEUE_BOLD, fontSize: variables.fontSizeNormal, fontWeight: fontWeightBold, @@ -859,7 +859,7 @@ const styles = { }, chatItemComposeSecondaryRowSubText: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightSmall, @@ -920,7 +920,7 @@ const styles = { }, calendarDayContainerSelected: { - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, }, /** @@ -946,7 +946,7 @@ const styles = { height: '100%', backgroundColor: 'transparent', borderBottomWidth: 2, - borderColor: themeColors.border, + borderColor: theme.border, overflow: 'hidden', }, @@ -955,7 +955,7 @@ const styles = { left: 0, top: 0, fontSize: variables.fontSizeNormal, - color: themeColors.textSupporting, + color: theme.textSupporting, fontFamily: fontFamily.EXP_NEUE, width: '100%', }, @@ -965,7 +965,7 @@ const styles = { top: 0, width: '100%', height: 23, - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, }, textInputLabelDesktop: { @@ -980,7 +980,7 @@ const styles = { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeNormal, lineHeight: variables.lineHeightXLarge, - color: themeColors.text, + color: theme.text, paddingTop: 23, paddingBottom: 8, paddingLeft: 0, @@ -1019,9 +1019,9 @@ const styles = { backgroundColor: 'transparent', borderRadius: variables.componentBorderRadiusNormal, height: variables.inputComponentSizeNormal, - borderColor: themeColors.border, + borderColor: theme.border, borderWidth: 1, - color: themeColors.text, + color: theme.text, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeNormal, paddingLeft: 12, @@ -1044,7 +1044,7 @@ const styles = { }, textInputPrefix: { - color: themeColors.text, + color: theme.text, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeNormal, textAlignVertical: 'center', @@ -1054,7 +1054,7 @@ const styles = { borderBottomWidth: 2, paddingLeft: 0, borderStyle: 'solid', - borderColor: themeColors.border, + borderColor: theme.border, justifyContent: 'center', backgroundColor: 'transparent', height: variables.inputHeight, @@ -1072,7 +1072,7 @@ const styles = { zIndex: 1, }, - picker: (disabled = false, backgroundColor = themeColors.appBG) => ({ + picker: (disabled = false, backgroundColor = theme.appBG) => ({ iconContainer: { top: Math.round(variables.inputHeight * 0.5) - 11, right: 0, @@ -1082,63 +1082,63 @@ const styles = { inputWeb: { appearance: 'none', ...(disabled ? cursor.cursorDisabled : cursor.cursorPointer), - ...picker, + ...picker(theme), backgroundColor, }, inputIOS: { - ...picker, + ...picker(theme), }, done: { - color: themeColors.text, + color: theme.text, }, doneDepressed: { fontSize: defaultPickerStyles.done.fontSize, }, modalViewMiddle: { - backgroundColor: themeColors.border, + backgroundColor: theme.border, borderTopWidth: 0, }, modalViewBottom: { - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, }, inputAndroid: { - ...picker, + ...picker(theme), }, }), disabledText: { - color: themeColors.icon, + color: theme.icon, }, inputDisabled: { - backgroundColor: themeColors.highlightBG, - color: themeColors.icon, + backgroundColor: theme.highlightBG, + color: theme.icon, }, noOutline: addOutlineWidth({}, 0), errorOutline: { - borderColor: themeColors.danger, + borderColor: theme.danger, }, textLabelSupporting: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeLabel, - color: themeColors.textSupporting, + color: theme.textSupporting, }, textLabelError: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeLabel, - color: themeColors.textError, + color: theme.textError, }, textReceiptUpload: { ...headlineFont, fontSize: variables.fontSizeXLarge, - color: themeColors.textLight, + color: theme.textLight, textAlign: 'center', }, @@ -1146,13 +1146,13 @@ const styles = { fontFamily: fontFamily.EXP_NEUE, lineHeight: variables.lineHeightLarge, textAlign: 'center', - color: themeColors.textLight, + color: theme.textLight, }, furtherDetailsText: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeSmall, - color: themeColors.textSupporting, + color: theme.textSupporting, }, lh16: { @@ -1168,35 +1168,35 @@ const styles = { }, formHelp: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeLabel, lineHeight: variables.lineHeightLarge, marginBottom: 4, }, formError: { - color: themeColors.textError, + color: theme.textError, fontSize: variables.fontSizeLabel, lineHeight: variables.formErrorLineHeight, marginBottom: 4, }, formSuccess: { - color: themeColors.success, + color: theme.success, fontSize: variables.fontSizeLabel, lineHeight: 18, marginBottom: 4, }, desktopRedirectPage: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, minHeight: '100%', flex: 1, alignItems: 'center', }, signInPage: { - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, minHeight: '100%', flex: 1, }, @@ -1276,7 +1276,7 @@ const styles = { // Sidebar Styles sidebar: { - backgroundColor: themeColors.sidebar, + backgroundColor: theme.sidebar, height: '100%', }, @@ -1294,14 +1294,14 @@ const styles = { }, sidebarAvatar: { - backgroundColor: themeColors.icon, + backgroundColor: theme.icon, borderRadius: 20, height: variables.componentSizeNormal, width: variables.componentSizeNormal, }, - statusIndicator: (backgroundColor = themeColors.danger) => ({ - borderColor: themeColors.sidebar, + statusIndicator: (backgroundColor = theme.danger) => ({ + borderColor: theme.sidebar, backgroundColor, borderRadius: 8, borderWidth: 2, @@ -1323,7 +1323,7 @@ const styles = { }, floatingActionButton: { - backgroundColor: themeColors.success, + backgroundColor: theme.success, height: variables.componentSizeLarge, width: variables.componentSizeLarge, borderRadius: 999, @@ -1332,7 +1332,7 @@ const styles = { }, sidebarFooterUsername: { - color: themeColors.heading, + color: theme.heading, fontSize: variables.fontSizeLabel, fontWeight: '700', width: 200, @@ -1342,7 +1342,7 @@ const styles = { }, sidebarFooterLink: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeSmall, textDecorationLine: 'none', fontFamily: fontFamily.EXP_NEUE, @@ -1403,7 +1403,7 @@ const styles = { createMenuHeaderText: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeLabel, - color: themeColors.heading, + color: theme.heading, }, popoverMenuItem: { @@ -1423,7 +1423,7 @@ const styles = { popoverMenuText: { fontSize: variables.fontSizeNormal, - color: themeColors.heading, + color: theme.heading, }, popoverInnerContainer: { @@ -1453,28 +1453,28 @@ const styles = { }, sidebarLinkText: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeNormal, textDecorationLine: 'none', overflow: 'hidden', }, sidebarLinkHover: { - backgroundColor: themeColors.sidebarHover, + backgroundColor: theme.sidebarHover, }, sidebarLinkActive: { - backgroundColor: themeColors.border, + backgroundColor: theme.border, textDecorationLine: 'none', }, sidebarLinkTextBold: { fontWeight: '700', - color: themeColors.heading, + color: theme.heading, }, sidebarLinkActiveText: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeNormal, textDecorationLine: 'none', overflow: 'hidden', @@ -1525,11 +1525,11 @@ const styles = { }, optionRowSelected: { - backgroundColor: themeColors.activeComponentBG, + backgroundColor: theme.activeComponentBG, }, optionRowDisabled: { - color: themeColors.textSupporting, + color: theme.textSupporting, }, optionRowCompact: { @@ -1559,7 +1559,7 @@ const styles = { }), appContent: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, overflow: 'hidden', }, @@ -1632,7 +1632,7 @@ const styles = { }, chatItemMessageHeaderSender: { - color: themeColors.heading, + color: theme.heading, fontFamily: fontFamily.EXP_NEUE_BOLD, fontSize: variables.fontSizeNormal, fontWeight: fontWeightBold, @@ -1642,13 +1642,13 @@ const styles = { chatItemMessageHeaderTimestamp: { flexShrink: 0, - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeSmall, paddingTop: 2, }, chatItemMessage: { - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeNormal, fontFamily: fontFamily.EXP_NEUE, lineHeight: variables.lineHeightXLarge, @@ -1667,15 +1667,15 @@ const styles = { }, chatItemComposeBoxColor: { - borderColor: themeColors.border, + borderColor: theme.border, }, chatItemComposeBoxFocusedColor: { - borderColor: themeColors.borderFocus, + borderColor: theme.borderFocus, }, chatItemComposeBox: { - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, borderWidth: 1, borderRadius: variables.componentBorderRadiusRounded, minHeight: variables.componentSizeMedium, @@ -1690,7 +1690,7 @@ const styles = { paddingLeft: 20, paddingRight: 20, display: 'flex', - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, }, chatFooterFullCompose: { @@ -1718,9 +1718,9 @@ const styles = { // Make sure you run the following tests against any changes: #12669 textInputCompose: addOutlineWidth( { - backgroundColor: themeColors.componentBG, - borderColor: themeColors.border, - color: themeColors.text, + backgroundColor: theme.componentBG, + borderColor: theme.border, + color: theme.text, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeNormal, borderWidth: 0, @@ -1748,7 +1748,7 @@ const styles = { }, editInputComposeSpacing: { - backgroundColor: themeColors.transparent, + backgroundColor: theme.transparent, marginVertical: 8, }, @@ -1761,13 +1761,13 @@ const styles = { textInputComposeBorder: { borderLeftWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, }, chatItemSubmitButton: { alignSelf: 'flex-end', borderRadius: variables.componentBorderRadiusRounded, - backgroundColor: themeColors.transparent, + backgroundColor: theme.transparent, height: 40, padding: 10, margin: 3, @@ -1775,11 +1775,11 @@ const styles = { }, emojiPickerContainer: { - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, }, emojiHeaderContainer: { - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, display: 'flex', height: CONST.EMOJI_PICKER_HEADER_HEIGHT, justifyContent: 'center', @@ -1791,7 +1791,7 @@ const styles = { ...spacing.pv1, fontFamily: fontFamily.EXP_NEUE_BOLD, fontWeight: fontWeightBold, - color: themeColors.heading, + color: theme.heading, fontSize: variables.fontSizeSmall, }, @@ -1815,13 +1815,13 @@ const styles = { emojiItemHighlighted: { transition: '0.2s ease', - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, }, emojiItemKeyboardHighlighted: { transition: '0.2s ease', borderWidth: 1, - borderColor: themeColors.link, + borderColor: theme.link, borderRadius: variables.buttonBorderRadius, }, @@ -1848,7 +1848,7 @@ const styles = { }, hoveredButton: { - backgroundColor: themeColors.buttonHoveredBG, + backgroundColor: theme.buttonHoveredBG, }, composerSizeButton: { @@ -1858,13 +1858,13 @@ const styles = { padding: 6, margin: 3, borderRadius: variables.componentBorderRadiusRounded, - backgroundColor: themeColors.transparent, + backgroundColor: theme.transparent, justifyContent: 'center', }, chatItemAttachmentPlaceholder: { - backgroundColor: themeColors.sidebar, - borderColor: themeColors.border, + backgroundColor: theme.sidebar, + borderColor: theme.border, borderWidth: 1, borderRadius: variables.componentBorderRadiusNormal, height: 150, @@ -1902,7 +1902,7 @@ const styles = { exampleCheckImage: { width: '100%', height: 80, - borderColor: themeColors.border, + borderColor: theme.border, borderWidth: 1, borderRadius: variables.componentBorderRadiusNormal, }, @@ -1910,14 +1910,14 @@ const styles = { singleAvatar: { height: 24, width: 24, - backgroundColor: themeColors.icon, + backgroundColor: theme.icon, borderRadius: 24, }, singleSubscript: { height: variables.iconSizeNormal, width: variables.iconSizeNormal, - backgroundColor: themeColors.icon, + backgroundColor: theme.icon, borderRadius: 20, zIndex: 1, }, @@ -1925,7 +1925,7 @@ const styles = { singleAvatarSmall: { height: 18, width: 18, - backgroundColor: themeColors.icon, + backgroundColor: theme.icon, borderRadius: 18, }, @@ -1981,8 +1981,8 @@ const styles = { right: -25, borderWidth: 3, borderRadius: 18, - borderColor: themeColors.cardBorder, - backgroundColor: themeColors.appBG, + borderColor: theme.cardBorder, + backgroundColor: theme.appBG, }, avatarLarge: { @@ -2003,7 +2003,7 @@ const styles = { }, avatarInnerText: { - color: themeColors.textLight, + color: theme.textLight, fontSize: variables.fontSizeSmall, lineHeight: undefined, marginLeft: -3, @@ -2011,7 +2011,7 @@ const styles = { }, avatarInnerTextSmall: { - color: themeColors.textLight, + color: theme.textLight, fontSize: variables.fontSizeExtraSmall, lineHeight: undefined, marginLeft: -2, @@ -2024,13 +2024,13 @@ const styles = { }, avatar: { - backgroundColor: themeColors.sidebar, - borderColor: themeColors.sidebar, + backgroundColor: theme.sidebar, + borderColor: theme.sidebar, }, focusedAvatar: { - backgroundColor: themeColors.border, - borderColor: themeColors.border, + backgroundColor: theme.border, + borderColor: theme.border, }, emptyAvatar: { @@ -2086,26 +2086,26 @@ const styles = { borderTop: { borderTopWidth: variables.borderTopWidth, - borderColor: themeColors.border, + borderColor: theme.border, }, borderTopRounded: { borderTopWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, borderTopLeftRadius: variables.componentBorderRadiusNormal, borderTopRightRadius: variables.componentBorderRadiusNormal, }, borderBottomRounded: { borderBottomWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, borderBottomLeftRadius: variables.componentBorderRadiusNormal, borderBottomRightRadius: variables.componentBorderRadiusNormal, }, borderBottom: { borderBottomWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, }, borderNone: { @@ -2115,12 +2115,12 @@ const styles = { borderRight: { borderRightWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, }, borderLeft: { borderLeftWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, }, pointerEventsNone, @@ -2145,7 +2145,7 @@ const styles = { imageModalPDF: { flex: 1, - backgroundColor: themeColors.modalBackground, + backgroundColor: theme.modalBackground, }, PDFView: { @@ -2153,7 +2153,7 @@ const styles = { // It's being used on Web/Desktop only to vertically center short PDFs, // while preventing the overflow of the top of long PDF files. display: 'grid', - backgroundColor: themeColors.modalBackground, + backgroundColor: theme.modalBackground, width: '100%', height: '100%', justifyContent: 'center', @@ -2178,7 +2178,7 @@ const styles = { flexDirection: 'column', justifyContent: 'center', alignItems: 'center', - backgroundColor: themeColors.modalBackdrop, + backgroundColor: theme.modalBackdrop, }, centeredModalStyles: (isSmallScreenWidth, isFullScreenWhenSmall) => ({ @@ -2194,10 +2194,10 @@ const styles = { }, defaultAttachmentView: { - backgroundColor: themeColors.sidebar, + backgroundColor: theme.sidebar, borderRadius: variables.componentBorderRadiusNormal, borderWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, flexDirection: 'row', padding: 20, alignItems: 'center', @@ -2205,7 +2205,7 @@ const styles = { notFoundSafeArea: { flex: 1, - backgroundColor: themeColors.heading, + backgroundColor: theme.heading, }, notFoundView: { @@ -2227,7 +2227,7 @@ const styles = { notFoundTextHeader: { ...headlineFont, - color: themeColors.heading, + color: theme.heading, fontSize: variables.fontSizeXLarge, lineHeight: variables.lineHeightXXLarge, marginTop: 20, @@ -2236,14 +2236,14 @@ const styles = { }, notFoundTextBody: { - color: themeColors.componentBG, + color: theme.componentBG, fontFamily: fontFamily.EXP_NEUE_BOLD, fontWeight: fontWeightBold, fontSize: 15, }, notFoundButtonText: { - color: themeColors.link, + color: theme.link, fontFamily: fontFamily.EXP_NEUE_BOLD, fontWeight: fontWeightBold, fontSize: 15, @@ -2254,8 +2254,8 @@ const styles = { }, defaultModalContainer: { - backgroundColor: themeColors.componentBG, - borderColor: themeColors.transparent, + backgroundColor: theme.componentBG, + borderColor: theme.transparent, }, reportActionContextMenuMiniButton: { @@ -2312,7 +2312,7 @@ const styles = { }, twoFactorAuthSection: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, padding: 0, }, @@ -2330,7 +2330,7 @@ const styles = { return { alignItems: 'center', justifyContent: 'center', - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, paddingVertical: 28, borderRadius: 16, marginTop: 32, @@ -2381,7 +2381,7 @@ const styles = { justifyContent: 'space-between', }), padding: 20, - backgroundColor: themeColors.sidebar, + backgroundColor: theme.sidebar, borderRadius: variables.componentBorderRadiusLarge, overflow: 'hidden', }), @@ -2401,7 +2401,7 @@ const styles = { anonymousRoomFooterLogoTaglineText: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeMedium, - color: themeColors.textLight, + color: theme.textLight, }, signInButtonAvatar: { width: 80, @@ -2417,9 +2417,9 @@ const styles = { }, roomHeaderAvatar: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, borderRadius: 100, - borderColor: themeColors.componentBG, + borderColor: theme.componentBG, borderWidth: 4, }, @@ -2429,7 +2429,7 @@ const styles = { right: 0, bottom: 0, left: 0, - backgroundColor: themeColors.overlay, + backgroundColor: theme.overlay, opacity: variables.overlayOpacity, borderRadius: 88, }, @@ -2438,7 +2438,7 @@ const styles = { RHPNavigatorContainerNavigatorContainerStyles: (isSmallScreenWidth) => ({marginLeft: isSmallScreenWidth ? 0 : variables.sideBarWidth, flex: 1}), avatarInnerTextChat: { - color: themeColors.textLight, + color: theme.textLight, fontSize: variables.fontSizeXLarge, fontFamily: fontFamily.EXP_NEW_KANSAS_MEDIUM, textAlign: 'center', @@ -2457,7 +2457,7 @@ const styles = { fontSize: variables.fontSizeLarge, fontFamily: fontFamily.EXP_NEUE_BOLD, fontWeight: fontWeightBold, - color: themeColors.heading, + color: theme.heading, }, pageWrapper: { @@ -2482,12 +2482,12 @@ const styles = { selectCircle: { width: variables.componentSizeSmall, height: variables.componentSizeSmall, - borderColor: themeColors.border, + borderColor: theme.border, borderWidth: 1, borderRadius: variables.componentSizeSmall / 2, justifyContent: 'center', alignItems: 'center', - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, marginLeft: 8, }, @@ -2506,7 +2506,7 @@ const styles = { unreadIndicatorLine: { height: 1, - backgroundColor: themeColors.unreadIndicator, + backgroundColor: theme.unreadIndicator, flexGrow: 1, marginRight: 8, opacity: 0.5, @@ -2514,13 +2514,13 @@ const styles = { threadDividerLine: { height: 1, - backgroundColor: themeColors.border, + backgroundColor: theme.border, flexGrow: 1, marginHorizontal: 20, }, unreadIndicatorText: { - color: themeColors.unreadIndicator, + color: theme.unreadIndicator, fontFamily: fontFamily.EXP_NEUE_BOLD, fontSize: variables.fontSizeSmall, fontWeight: fontWeightBold, @@ -2532,11 +2532,11 @@ const styles = { }, navigationSceneContainer: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, }, navigationScreenCardStyle: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, height: '100%', }, @@ -2592,7 +2592,7 @@ const styles = { detailsPageSectionVersion: { alignSelf: 'center', - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeSmall, height: 24, lineHeight: 20, @@ -2604,11 +2604,11 @@ const styles = { justifyContent: 'center', borderRadius: 20, padding: 15, - backgroundColor: themeColors.success, + backgroundColor: theme.success, }, switchInactive: { - backgroundColor: themeColors.border, + backgroundColor: theme.border, }, switchThumb: { @@ -2617,7 +2617,7 @@ const styles = { borderRadius: 11, position: 'absolute', left: 4, - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, }, switchThumbTransformation: (translateX) => ({ @@ -2625,11 +2625,11 @@ const styles = { }), radioButtonContainer: { - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, borderRadius: 10, height: 20, width: 20, - borderColor: themeColors.icon, + borderColor: theme.icon, borderWidth: 1, justifyContent: 'center', alignItems: 'center', @@ -2643,7 +2643,7 @@ const styles = { }, checkedContainer: { - backgroundColor: themeColors.checkBox, + backgroundColor: theme.checkBox, }, magicCodeInputContainer: { @@ -2654,7 +2654,7 @@ const styles = { magicCodeInput: { fontSize: variables.fontSizeXLarge, - color: themeColors.heading, + color: theme.heading, lineHeight: variables.inputHeight, }, @@ -2678,7 +2678,7 @@ const styles = { iouAmountText: { ...headlineFont, fontSize: variables.iouAmountTextSize, - color: themeColors.heading, + color: theme.heading, lineHeight: variables.inputHeight, }, @@ -2686,7 +2686,7 @@ const styles = { { ...headlineFont, fontSize: variables.iouAmountTextSize, - color: themeColors.heading, + color: theme.heading, padding: 0, lineHeight: undefined, }, @@ -2713,7 +2713,7 @@ const styles = { }, moneyRequestPreviewBox: { - backgroundColor: themeColors.cardBG, + backgroundColor: theme.cardBG, borderRadius: variables.componentBorderRadiusLarge, maxWidth: variables.sideBarWidth, width: '100%', @@ -2739,7 +2739,7 @@ const styles = { moneyRequestPreviewAmount: { ...headlineFont, ...whiteSpace.preWrap, - color: themeColors.heading, + color: theme.heading, }, defaultCheckmarkWrapper: { @@ -2785,7 +2785,7 @@ const styles = { }, fullScreenLoading: { - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, opacity: 0.8, justifyContent: 'center', alignItems: 'center', @@ -2793,12 +2793,12 @@ const styles = { }, navigatorFullScreenLoading: { - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, opacity: 1, }, reimbursementAccountFullScreenLoading: { - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, opacity: 0.8, justifyContent: 'flex-start', alignItems: 'center', @@ -2846,12 +2846,12 @@ const styles = { }), growlNotificationBox: { - backgroundColor: themeColors.inverse, + backgroundColor: theme.inverse, borderRadius: variables.componentBorderRadiusNormal, alignItems: 'center', flexDirection: 'row', justifyContent: 'space-between', - shadowColor: themeColors.shadow, + shadowColor: theme.shadow, ...spacing.p5, }, @@ -2860,12 +2860,12 @@ const styles = { fontFamily: fontFamily.EXP_NEUE, width: '90%', lineHeight: variables.fontSizeNormalHeight, - color: themeColors.textReversed, + color: theme.textReversed, ...spacing.ml4, }, blockquote: { - borderLeftColor: themeColors.border, + borderLeftColor: theme.border, borderLeftWidth: 4, paddingLeft: 12, marginVertical: 4, @@ -2917,11 +2917,11 @@ const styles = { smallEditIcon: { alignItems: 'center', - backgroundColor: themeColors.buttonHoveredBG, - borderColor: themeColors.textReversed, + backgroundColor: theme.buttonHoveredBG, + borderColor: theme.textReversed, borderRadius: 14, borderWidth: 3, - color: themeColors.textReversed, + color: theme.textReversed, height: 28, width: 28, justifyContent: 'center', @@ -2938,7 +2938,7 @@ const styles = { height: 400, borderRadius: variables.componentBorderRadiusCard, overflow: 'hidden', - backgroundColor: themeColors.heroCard, + backgroundColor: theme.heroCard, }, workspaceCardMobile: { @@ -2981,18 +2981,18 @@ const styles = { }, peopleRowBorderBottom: { - borderColor: themeColors.border, + borderColor: theme.border, borderBottomWidth: 1, ...spacing.pb2, }, peopleBadge: { - backgroundColor: themeColors.icon, + backgroundColor: theme.icon, ...spacing.ph3, }, peopleBadgeText: { - color: themeColors.textReversed, + color: theme.textReversed, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightNormal, ...whiteSpace.noWrap, @@ -3018,7 +3018,7 @@ const styles = { flex: 1, }, text: { - color: themeColors.textSupporting, + color: theme.textSupporting, textAlignVertical: 'center', fontSize: variables.fontSizeLabel, }, @@ -3038,7 +3038,7 @@ const styles = { }, cardOverlay: { - backgroundColor: themeColors.overlay, + backgroundColor: theme.overlay, position: 'absolute', top: 0, left: 0, @@ -3055,18 +3055,18 @@ const styles = { shortTermsBorder: { borderWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, }, shortTermsHorizontalRule: { borderBottomWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, ...spacing.mh3, }, shortTermsLargeHorizontalRule: { borderWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, ...spacing.mh3, }, @@ -3089,7 +3089,7 @@ const styles = { shortTermsHeadline: { ...headlineFont, ...whiteSpace.preWrap, - color: themeColors.heading, + color: theme.heading, fontSize: variables.fontSizeXXXLarge, lineHeight: variables.lineHeightXXXLarge, }, @@ -3101,7 +3101,7 @@ const styles = { collapsibleSectionBorder: { borderBottomWidth: 2, - borderBottomColor: themeColors.border, + borderBottomColor: theme.border, }, communicationsLinkHeight: { @@ -3152,11 +3152,11 @@ const styles = { googleSearchSeparator: { height: 1, - backgroundColor: themeColors.border, + backgroundColor: theme.border, }, googleSearchText: { - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeNormal, lineHeight: variables.fontSizeNormalHeight, fontFamily: fontFamily.EXP_NEUE, @@ -3194,7 +3194,7 @@ const styles = { keyboardShortcutTableContainer: { display: 'flex', width: '100%', - borderColor: themeColors.border, + borderColor: theme.border, height: 'auto', borderRadius: variables.componentBorderRadius, borderWidth: 1, @@ -3203,7 +3203,7 @@ const styles = { keyboardShortcutTableRow: { flex: 1, flexDirection: 'row', - borderColor: themeColors.border, + borderColor: theme.border, flexBasis: 'auto', alignSelf: 'stretch', borderTopWidth: 1, @@ -3212,7 +3212,7 @@ const styles = { keyboardShortcutTablePrefix: { width: '30%', borderRightWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, }, keyboardShortcutTableFirstRow: { @@ -3220,18 +3220,18 @@ const styles = { }, iPhoneXSafeArea: { - backgroundColor: themeColors.inverse, + backgroundColor: theme.inverse, flex: 1, }, transferBalancePayment: { borderWidth: 1, borderRadius: variables.componentBorderRadiusNormal, - borderColor: themeColors.border, + borderColor: theme.border, }, transferBalanceSelectedPayment: { - borderColor: themeColors.iconSuccessFill, + borderColor: theme.iconSuccessFill, }, transferBalanceBalance: { @@ -3246,7 +3246,7 @@ const styles = { overflow: 'hidden', alignItems: 'center', justifyContent: 'center', - backgroundColor: themeColors.imageCropBackgroundColor, + backgroundColor: theme.imageCropBackgroundColor, ...cursor.cursorMove, }, @@ -3257,7 +3257,7 @@ const styles = { }, sliderKnob: { - backgroundColor: themeColors.success, + backgroundColor: theme.success, position: 'absolute', height: variables.sliderKnobSize, width: variables.sliderKnobSize, @@ -3267,7 +3267,7 @@ const styles = { }, sliderBar: { - backgroundColor: themeColors.border, + backgroundColor: theme.border, height: variables.sliderBarHeight, borderRadius: variables.sliderBarHeight / 2, alignSelf: 'stretch', @@ -3282,7 +3282,7 @@ const styles = { }, inlineSystemMessage: { - color: themeColors.textSupporting, + color: theme.textSupporting, fontSize: variables.fontSizeLabel, fontFamily: fontFamily.EXP_NEUE, marginLeft: 6, @@ -3297,17 +3297,17 @@ const styles = { }, invisibleOverlay: { - backgroundColor: themeColors.transparent, + backgroundColor: theme.transparent, zIndex: 1000, }, reportDropOverlay: { - backgroundColor: themeColors.dropUIBG, + backgroundColor: theme.dropUIBG, zIndex: 2, }, receiptDropOverlay: { - backgroundColor: themeColors.receiptDropUIBG, + backgroundColor: theme.receiptDropUIBG, zIndex: 2, }, @@ -3317,7 +3317,7 @@ const styles = { }), cardSection: { - backgroundColor: themeColors.cardBG, + backgroundColor: theme.cardBG, borderRadius: variables.componentBorderRadiusCard, marginBottom: 20, marginHorizontal: 16, @@ -3339,7 +3339,7 @@ const styles = { }, callRequestSection: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, paddingHorizontal: 0, paddingBottom: 0, marginHorizontal: 0, @@ -3361,7 +3361,7 @@ const styles = { flex: 1, alignItems: 'center', justifyContent: 'center', - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, }, deeplinkWrapperMessage: { @@ -3386,7 +3386,7 @@ const styles = { emojiReactionListHeader: { marginTop: 8, paddingBottom: 20, - borderBottomColor: themeColors.border, + borderBottomColor: theme.border, borderBottomWidth: 1, marginHorizontal: 20, }, @@ -3394,7 +3394,7 @@ const styles = { paddingVertical: 2, paddingHorizontal: 8, borderRadius: 28, - backgroundColor: themeColors.border, + backgroundColor: theme.border, alignItems: 'center', justifyContent: 'center', flexDirection: 'row', @@ -3407,7 +3407,7 @@ const styles = { paddingHorizontal: 20, }, reactionListHeaderText: { - color: themeColors.textSupporting, + color: theme.textSupporting, marginLeft: 8, alignSelf: 'center', }, @@ -3438,7 +3438,7 @@ const styles = { }, textReactionSenders: { - color: themeColors.dark, + color: theme.dark, ...wordBreak.breakWord, }, @@ -3460,7 +3460,7 @@ const styles = { }, validateCodeDigits: { - color: themeColors.text, + color: theme.text, fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeXXLarge, letterSpacing: 4, @@ -3481,14 +3481,14 @@ const styles = { footerTitle: { fontSize: variables.fontSizeLarge, - color: themeColors.success, + color: theme.success, marginBottom: 16, }, footerRow: { paddingVertical: 4, marginBottom: 8, - color: themeColors.textLight, + color: theme.textLight, fontSize: variables.fontSizeMedium, }, @@ -3499,7 +3499,7 @@ const styles = { listPickerSeparator: { height: 1, - backgroundColor: themeColors.buttonDefaultBG, + backgroundColor: theme.buttonDefaultBG, }, datePickerRoot: { @@ -3508,7 +3508,7 @@ const styles = { }, datePickerPopover: { - backgroundColor: themeColors.appBG, + backgroundColor: theme.appBG, width: '100%', alignSelf: 'center', zIndex: 100, @@ -3517,7 +3517,7 @@ const styles = { loginHeroHeader: { fontFamily: fontFamily.EXP_NEW_KANSAS_MEDIUM, - color: themeColors.success, + color: theme.success, fontWeight: '500', textAlign: 'center', }, @@ -3531,14 +3531,14 @@ const styles = { loginHeroBody: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeSignInHeroBody, - color: themeColors.textLight, + color: theme.textLight, textAlign: 'center', }, linkPreviewWrapper: { marginTop: 16, borderLeftWidth: 4, - borderLeftColor: themeColors.border, + borderLeftColor: theme.border, paddingLeft: 12, }, @@ -3560,7 +3560,7 @@ const styles = { }, whisper: { - backgroundColor: themeColors.cardBG, + backgroundColor: theme.cardBG, }, contextMenuItemPopoverMaxWidth: { @@ -3595,7 +3595,7 @@ const styles = { taskTitleDescription: { fontFamily: fontFamily.EXP_NEUE, fontSize: variables.fontSizeLabel, - color: themeColors.textSupporting, + color: theme.textSupporting, lineHeight: variables.lineHeightNormal, ...spacing.mb1, }, @@ -3607,7 +3607,7 @@ const styles = { reportHorizontalRule: { borderBottomWidth: 1, - borderColor: themeColors.border, + borderColor: theme.border, ...spacing.mh5, ...spacing.mv2, }, @@ -3638,13 +3638,13 @@ const styles = { paddingVertical: 20, borderRadius: 20, overflow: 'hidden', - borderColor: themeColors.borderFocus, + borderColor: theme.borderFocus, borderWidth: 2, - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, }, splashScreenHider: { - backgroundColor: themeColors.splashBG, + backgroundColor: theme.splashBG, alignItems: 'center', justifyContent: 'center', }, @@ -3740,7 +3740,7 @@ const styles = { marginLeft: 8, fontFamily: isSelected ? fontFamily.EXP_NEUE_BOLD : fontFamily.EXP_NEUE, fontWeight: isSelected ? fontWeightBold : 400, - color: isSelected ? themeColors.textLight : themeColors.textSupporting, + color: isSelected ? theme.textLight : theme.textSupporting, }), /** @@ -3781,7 +3781,7 @@ const styles = { emojiPickerButtonDropdown: { justifyContent: 'center', - backgroundColor: themeColors.activeComponentBG, + backgroundColor: theme.activeComponentBG, width: 86, height: 52, borderRadius: 26, @@ -3803,15 +3803,15 @@ const styles = { }, reportPreviewBox: { - backgroundColor: themeColors.cardBG, + backgroundColor: theme.cardBG, borderRadius: variables.componentBorderRadiusLarge, maxWidth: variables.sideBarWidth, width: '100%', }, reportPreviewBoxHoverBorder: { - borderColor: themeColors.border, - backgroundColor: themeColors.border, + borderColor: theme.border, + backgroundColor: theme.border, }, reportPreviewBoxBody: { @@ -3821,7 +3821,7 @@ const styles = { reportActionItemImages: { flexDirection: 'row', borderWidth: 4, - borderColor: themeColors.transparent, + borderColor: theme.transparent, borderTopLeftRadius: variables.componentBorderRadiusLarge, borderTopRightRadius: variables.componentBorderRadiusLarge, borderBottomLeftRadius: variables.componentBorderRadiusLarge, @@ -3841,13 +3841,13 @@ const styles = { reportActionItemImageBorder: { borderRightWidth: 2, - borderColor: themeColors.cardBG, + borderColor: theme.cardBG, }, reportActionItemImagesMore: { position: 'absolute', borderRadius: 18, - backgroundColor: themeColors.cardBG, + backgroundColor: theme.cardBG, width: 36, height: 36, display: 'flex', @@ -3862,7 +3862,7 @@ const styles = { display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: themeColors.border, + backgroundColor: theme.border, marginRight: 12, }, @@ -3885,7 +3885,7 @@ const styles = { sidebarStatusAvatarContainer: { height: 44, width: 84, - backgroundColor: themeColors.componentBG, + backgroundColor: theme.componentBG, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', @@ -3905,7 +3905,7 @@ const styles = { ...spacing.mv3, overflow: 'hidden', borderWidth: 2, - borderColor: themeColors.cardBG, + borderColor: theme.cardBG, borderRadius: variables.componentBorderRadiusLarge, height: 200, maxWidth: 400, @@ -3947,7 +3947,7 @@ const styles = { }, mapPendingView: { - backgroundColor: themeColors.highlightBG, + backgroundColor: theme.highlightBG, ...flex.flex1, borderRadius: variables.componentBorderRadiusLarge, }, @@ -3959,6 +3959,14 @@ const styles = { height: 30, width: '100%', }, -}; +}); + +// For now we need to export the styles function that takes the theme as an argument +// as something named different than "styles", because a lot of files import the "defaultStyles" +// as "styles", which causes ESLint to throw an error. +// TODO: Remove "stylesGenerator" and instead only return "styles" once the app is migrated to theme switching hooks and HOCs and "styles/theme/default.js" is not used anywhere anymore (GH issue: https://github.com/Expensify/App/issues/27337) +const stylesGenerator = styles; +const defaultStyles = styles(defaultTheme); -export default styles; +export default defaultStyles; +export {stylesGenerator}; diff --git a/src/styles/useThemeStyles.ts b/src/styles/useThemeStyles.ts index 77ee0edb6f95..a5b3baebbaec 100644 --- a/src/styles/useThemeStyles.ts +++ b/src/styles/useThemeStyles.ts @@ -8,6 +8,8 @@ function useThemeStyles() { throw new Error('StylesContext was null! Are you sure that you wrapped the component under a ?'); } + // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return themeStyles; } diff --git a/src/styles/utilities/positioning.ts b/src/styles/utilities/positioning.ts index fc505e4cb608..651d2a12f2ea 100644 --- a/src/styles/utilities/positioning.ts +++ b/src/styles/utilities/positioning.ts @@ -47,4 +47,7 @@ export default { b0: { bottom: 0, }, + b2: { + bottom: 8, + }, } satisfies Record; diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 66f85c98d70c..7151bb84d1f1 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -19,6 +19,7 @@ type IOU = { receiptSource?: string; transactionID?: string; participants?: Participant[]; + tag?: string; }; export default IOU; diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts new file mode 100644 index 000000000000..fe6bee3a1f31 --- /dev/null +++ b/src/types/onyx/PolicyTag.ts @@ -0,0 +1,13 @@ +type PolicyTag = { + /** Name of a Tag */ + name: string; + + /** Flag that determines if a tag is active and able to be selected */ + enabled: boolean; + + /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': string; +}; + +export default PolicyTag; diff --git a/src/types/onyx/RecentlyUsedTags.ts b/src/types/onyx/RecentlyUsedTags.ts new file mode 100644 index 000000000000..1d6112514609 --- /dev/null +++ b/src/types/onyx/RecentlyUsedTags.ts @@ -0,0 +1,3 @@ +type RecentlyUsedTags = Record; + +export default RecentlyUsedTags; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 9612c06d75a6..a980e086aff5 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -46,6 +46,8 @@ import Transaction from './Transaction'; import Form, {AddDebitCardForm} from './Form'; import RecentWaypoints from './RecentWaypoints'; import RecentlyUsedCategories from './RecentlyUsedCategories'; +import RecentlyUsedTags from './RecentlyUsedTags'; +import PolicyTag from './PolicyTag'; export type { Account, @@ -97,4 +99,6 @@ export type { OnyxUpdatesFromServer, RecentWaypoints, RecentlyUsedCategories, + RecentlyUsedTags, + PolicyTag, }; diff --git a/tests/unit/TranslateTest.js b/tests/unit/TranslateTest.js index 9ac23d6b81b0..45d4a12b61f5 100644 --- a/tests/unit/TranslateTest.js +++ b/tests/unit/TranslateTest.js @@ -7,19 +7,19 @@ const CONST = require('../../src/CONST').default; const originalTranslations = _.clone(translations); translations.default = { - [CONST.LOCALES.EN]: { + [CONST.LOCALES.EN]: translations.flattenObject({ testKey1: 'English', testKey2: 'Test Word 2', testKey3: 'Test Word 3', testKeyGroup: { testFunction: ({testVariable}) => `With variable ${testVariable}`, }, - }, - [CONST.LOCALES.ES]: { + }), + [CONST.LOCALES.ES]: translations.flattenObject({ testKey1: 'Spanish', testKey2: 'Spanish Word 2', - }, - [CONST.LOCALES.ES_ES]: {testKey1: 'Spanish ES'}, + }), + [CONST.LOCALES.ES_ES]: translations.flattenObject({testKey1: 'Spanish ES'}), }; describe('translate', () => { @@ -38,14 +38,12 @@ describe('translate', () => { test('Test when key is not found in default', () => { expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toThrow(Error); - expect(() => Localize.translate(CONST.LOCALES.ES_ES, ['a', 'b', 'c'])).toThrow(Error); }); test('Test when key is not found in default (Production Mode)', () => { const ORIGINAL_IS_IN_PRODUCTION = CONFIG.default.IS_IN_PRODUCTION; CONFIG.default.IS_IN_PRODUCTION = true; expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toBe('testKey4'); - expect(Localize.translate(CONST.LOCALES.ES_ES, ['a', 'b', 'c'])).toBe('a.b.c'); CONFIG.default.IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION; }); @@ -53,7 +51,6 @@ describe('translate', () => { const expectedValue = 'With variable Test Variable'; const testVariable = 'Test Variable'; expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction', {testVariable})).toBe(expectedValue); - expect(Localize.translate(CONST.LOCALES.EN, ['testKeyGroup', 'testFunction'], {testVariable})).toBe(expectedValue); }); }); @@ -97,3 +94,45 @@ describe('Translation Keys', () => { }); }); }); + +describe('flattenObject', () => { + it('It should work correctly', () => { + const func = ({content}) => `This is the content: ${content}`; + const simpleObject = { + common: { + yes: 'Yes', + no: 'No', + }, + complex: { + activity: { + none: 'No Activity', + some: 'Some Activity', + }, + report: { + title: { + expense: 'Expense', + task: 'Task', + }, + description: { + none: 'No description', + }, + content: func, + messages: ['Hello', 'Hi', 'Sup!'], + }, + }, + }; + + const result = translations.flattenObject(simpleObject); + expect(result).toStrictEqual({ + 'common.yes': 'Yes', + 'common.no': 'No', + 'complex.activity.none': 'No Activity', + 'complex.activity.some': 'Some Activity', + 'complex.report.title.expense': 'Expense', + 'complex.report.title.task': 'Task', + 'complex.report.description.none': 'No description', + 'complex.report.content': func, + 'complex.report.messages': ['Hello', 'Hi', 'Sup!'], + }); + }); +}); diff --git a/workflow_tests/README.md b/workflow_tests/README.md new file mode 100644 index 000000000000..f5975a421120 --- /dev/null +++ b/workflow_tests/README.md @@ -0,0 +1,422 @@ +# Testing GitHub Actions workflows locally + +## Components +The workflow testing framework consists mainly of 3 components: +- [Jest](https://jestjs.io/) - testing framework, also used for [application tests](https://github.com/Expensify/App/tree/main/tests) +- [Mock-github](https://github.com/kiegroup/mock-github) - package allowing for creation of local repositories, which can be used to make sure that the workflow tests have access only to these files that they should and that they won't modify the actual repository +- [Act-js](https://github.com/kiegroup/act-js) - JS wrapper around [Act](https://github.com/nektos/act). Act is a tool that allows to run GitHub Actions workflows locally, and Act-js allows to configure and run Act from JS code, like Jest tests. It also provides additional tools like mocking workflow steps and retrieving the workflow output a JSON, which allows for comparison of the actual output with expected values + +## Setup +- Install dependencies from `package.json` file with `npm install` +- Make sure you have fulfilled the [prerequisites](https://github.com/nektos/act#necessary-prerequisites-for-running-act) for running `Act` +- Install `Act` with `brew install act` and follow the documentation on [first Act run](https://github.com/nektos/act#first-act-run) +- Set the environment variable `ACT_BINARY` to the path to your `Act` executable (`which act` if you're not sure what the path is) +- You should be ready to run the tests now with `npm run workflow-test` +- You can pre-generate new mocks/assertions/test files for a given workflow by running `npm run workflow-test:generate ` + +## Running +- To run the workflow tests simply use + - `npm run workflow-test` + - this will run all the tests sequentially, which can take some time +- To run a specific test suite you can use + - `npm run workflow-test -- -i ` + - this will run only the test from that specific test file +- To run a specific test or subset of tests use + - `npm run workflow-test -- -t ""` + - this will run only the tests having `` in their name/description +- You can combine these like `npm run workflow-test -- -i workflow_tests/preDeploy.test.js -t "single specific test"` +- You can also use all other options which are normally usable with `jest` + +## Limitations +Not all workflows can always be tested this way, for example: +- Act and Act-js do not support all the runner types available in GitHub Actions, like `macOS` runners or some specific version of `Ubuntu` runners like `ubuntu-20.04-64core`. In these cases the job will be omitted entirely and cannot be tested +- Testing more complex workflows in their entirety can be extremely time-consuming and cumbersome. It is often optimal to mock most of the steps with expressions printing the input and output conditions +- Due to the way `Act` and `Act-js` handle workflow output, not much can be checked in the test. What is available, namely whether the job/step executed or not, whether it was successful or not and what its printed output was, should be enough in most scenarios +- `Act` does not seem to support the conditions set on event parameters when determining whether to run the workflow or not, namely for a workflow defined with: +```yaml +on: + pull_request: + types: [opened, edited, reopened] +``` +running `act pull_request -e event_data.json` with `event_data.json` having `{"action": "opened"}` will execute the workflow (as expected), running for example `act push` will not execute it (as expected), but running `act pull_request -e event_data.json` with `event_data.json` having for example `{"action": "assigned"}` **will still execute the workflow** even though it should only be executed with `action` being `opened`, `edited` or `reopened`. This only applies to running the workflow with `Act`, in the GitHub environment it still works as expected + +## File structure +The testing framework file structure within the repository is as follows: +- `App/` - main application folder + - `.github/` - GitHub Actions folder + - `workflows/` - workflows folder + - `.yml` - workflow file + - `...` - other workflow files + - `...` - other GitHub Actions files + - `workflow_tests/` - workflow testing folder + - `jest.config.ts` - `Jest` configuration file + - `README.md` - this readme file + - `utils.js` - various utility functions used in multiple tests + - `.test.js` - test suite file for a GitHub Actions workflow named `` + - `mocks/` - folder with step mock definitions + - `Mocks.js` - file with step mock definitions for the `../.test.js` suite, or for the `` workflow + - `...` - other step mock definition files + - `assertions/` - folder with output assertions + - `Assertions.js` - file with output assertions for the `../.test.js` suite, or for the `` workflow + - `...` - other output assertion files + - `...` - other test suites + - `...` - other application files + +## Utility helpers +`utils.js` file provides several helper methods to speed up the tests development and maintenance + +### `setUpActParams` +`setUpActParams` allows for initiating the context in which Act will execute the workflow + +Parameters: +- `act` - instance of previously created `Act` object that will be updated with new params +- `event` - the name of the event, this can be any event name used by GitHub Actions, like `pull_request`, `push`, `workflow_dispatch`, etc. +- `event_options` - object with options of the event, allowing for customising it for different scenarios, for example `push` event can be customised for pushing to different branches with options `{head: {ref: ''}}` +- `secrets` - object with secret values provided, like `{: , ...}` +- `github_token` - value of the GitHub token, analogous to providing `GITHUB_TOKEN` secret + +Returns an updated `Act` object instance + +Example: +```javascript +let act = new kieActJs.Act(repoPath, workflowPath); +act = utils.setUpActParams( + act, + 'push', + {head: {ref: 'main'}}, + {OS_BOTIFY_TOKEN: 'dummy_token', GITHUB_ACTOR: 'Dummy Tester', SLACK_WEBHOOK: 'dummy_slack_webhook', LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_s3cr3t_p455word'}, + 'dummy_github_token', +); +``` + +### `getMockStep` +`getMockStep` allows for creating uniform mock step definitions compatible with `Act-js` and reduces time required, as well as possibility of errors/typos slipping in while developing tests. More complex behaviours have to be mocked manually + +Parameters: +- `name` - name of the step that **must correspond to the `name` in the `.yml` file**, otherwise the step cannot be found +- `message` - the message to be printed to default output when mock gets executed +- `job_id` - an optional id of the job that will be printed in `[]` square brackets along the `message`, useful when assessing the output with many steps from many jobs +- `inputs` - a list of input parameters to be printed, useful when checking if the step had been executed with expected inputs +- `in_envs` - a list of input environment variables, to be printed, useful when checking if the step had been executed in expected environment +- `outputs` - an object with values which should be printed by the mock to `$GITHUB_OUTPUT`, useful for setting the step output +- `out_envs` - an objects with values of environment variables set by the step in `$GITHUB_ENV`, useful for modifying the environment by the mock +- `isSuccessful` - a boolean value indicating whether the step succeeds or not, exits with status `0` (if successful) or `1` (if not) + +Returns an object with step mock definition, ready to be provided to the `Act` object instance + +Example: +```javascript +let mockStep = utils.getMockStep( + 'Name of the step from .yml', + 'Message to be printed', + 'TEST_JOB', + ['INPUT_1', 'INPUT_2'], + ['ENV_1', 'ENV_2'], + {output_1: true, output_2: 'Some Result'}, + {OUT_ENV: 'ENV_VALUE'}, + false, +); +``` +results in +```javascript +{ + name: 'Name of the step from .yml', + mockWith: 'echo [MOCK]' + + ' [TEST_JOB]' + + ' Message to be printed' + + ', INPUT_1="{{ inputs.INPUT_1 }}' + + ', INPUT_2="{{ inputs.INPUT_2 }}' + + ', ENV_1="{{ env.ENV_1 }}' + + ', ENV_1="{{ env.ENV_1 }}' + + '\necho "output_1=true" >> "$GITHUB_OUTPUT"', + + '\necho "output_2=Some Result" >> "$GITHUB_OUTPUT"', + + '\necho "OUT_ENV=ENV_VALUE" >> "$GITHUB_ENV"', + + '\nexit 1', +} +``` + +### `getStepAssertion` +`getStepAssertion` allows for creating uniform assertions for output from executed step, compatible with step mocks provided by `getMockStep` + +Parameters: +- `name` - name of the step, **has to correspond to the name from `.yml` file**, and the name in the step mock if applicable +- `isSuccessful` - boolean value for checking if the step should have exited successfully +- `expectedOutput` - an output that is expected from the step, compared directly - if provided the subsequent parameters are ignored +- `jobId` - an optional expected job identifier +- `message` - expected message printed by the step +- `inputs` - expected input values provided to the step +- `envs` - expected input environment variables for the step + +Returns an object with step expected output definition ready to be provided to `expect()` matcher + +Example: +```javascript +utils.getStepAssertion( + 'Name of the step from .yml', + false, + null, + 'TEST_JOB', + 'Message to be printed', + [{key: 'INPUT_1', value: true}, {key: 'INPUT_2', value: 'Some value'}], + [{key: 'PLAIN_ENV_VAR', value: 'Value'}, {key: 'SECRET_ENV_VAR', value: '***'}], +) +``` +results in +```javascript +{ + name: 'Name of the step from .yml', + status: 1, + output: '[MOCK]' + + ' [TEST_JOB]' + + ' Message to be printed' + + ', INPUT_1=true' + + ', INPUT_2=Some value' + + ', PLAIN_ENV_VAR=Value' + + ', SECRET_ENV_VAR=***', +} +``` + +### `setJobRunners` +`setJobRunners` overwrites the runner types for given jobs, helpful when the runner type in the workflow is not supported by `Act` + +Parameters: +- `act` - instance of previously created `Act` object +- `jobs` - object with keys being the IDs of the workflow jobs to be modified and values being the names of runners that should be used for them in the test +- `workflowPath` - path to the workflow file to be updated, **NOTE**: this will modify the file, use the one from the local test repo, not from `App/.github/workflows`! + +Returns an `Act` object instance + +Let's say you have a workflow with a job using `macos-12` runner, which is unsupported by `Act` - in this case that job will simply be skipped altogether, not allowing you to test it in any way. +```yaml +iOS: + name: Build and deploy iOS + needs: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} + runs-on: macos-12 + steps: +``` +You can use this method to change the runner to something that is supported, like +```javascript +act = utils.setJobRunners( + act, + { + iOS: 'ubuntu-latest', + }, + workflowPath, +); +``` +Now the test workflow will look as follows, which will allow you to run the job and do at least limited testing +```yaml +iOS: + name: Build and deploy iOS + needs: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} + runs-on: ubuntu-latest + steps: +``` + +## Typical test file +The following is the typical test file content, which will be followed by a detailed breakdown +```javascript +const path = require('path'); +const kieActJs = require('@kie/act-js'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils'); +const assertions = require('./assertions/Assertions'); +const mocks = require('./mocks/Mocks'); + +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + { + src: path.resolve(__dirname, '..', '.github', 'actions'), + dest: '.github/actions', + }, + { + src: path.resolve(__dirname, '..', '.github', 'libs'), + dest: '.github/libs', + }, + { + src: path.resolve(__dirname, '..', '.github', 'scripts'), + dest: '.github/scripts', + }, + { + src: path.resolve(__dirname, '..', '.github', 'workflows', '.yml'), + dest: '.github/workflows/.yml', + }, +]; + +beforeEach(async () => { + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testWorkflowsRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); +}); + +afterEach(async () => { + await mockGithub.teardown(); +}); + +describe('test some general behaviour', () => { + test('something happens - test if expected happened next', async () => { + // get path to the local test repo + const repoPath = mockGithub.repo.getPath('testWorkflowsRepo') || ''; + + // get path to the workflow file under test + const workflowPath = path.join(repoPath, '.github', 'workflows', '.yml'); + + // instantiate Act in the context of the test repo and given workflow file + let act = new kieActJs.Act(repoPath, workflowPath); + + // set run parameters + act = utils.setUpActParams( + act, + '', + {head: {ref: ''}}, + {'': '', + ); + + // set up mocks + const testMockSteps = { + '': [ + { + name: '', + mockWith: '', + }, + { + name: '', + mockWith: '', + }, + ], + '': [ + utils.getMockStep('', ''), + utils.getMockStep('', ''), + ], + }; + + // run an event and get the result + const result = await act + .runEvent('', { + workflowFile: path.join(repoPath, '.github', 'workflows'), + mockSteps: testMockSteps, + }); + + // assert results (some steps can run in parallel to each other so the order is not assured + // therefore we can check which steps have been executed, but not the set job order + assertions.assertSomethingHappened(result); + assertions.assertSomethingDidNotHappen(result, false); + }, timeout); +); +``` + +### Breakdown +Define which files should be copied into the test repo. In this case we copy `actions`, `libs`, `scripts` folders in their entirety and just the one workflow file we want to test +```javascript +const FILES_TO_COPY_INTO_TEST_REPO = [ + { + src: path.resolve(__dirname, '..', '.github', 'actions'), + dest: '.github/actions', + }, + { + src: path.resolve(__dirname, '..', '.github', 'libs'), + dest: '.github/libs', + }, + { + src: path.resolve(__dirname, '..', '.github', 'scripts'), + dest: '.github/scripts', + }, + { + src: path.resolve(__dirname, '..', '.github', 'workflows', '.yml'), + dest: '.github/workflows/.yml', + }, +]; +``` +`beforeEach` gets executed before each test. Here we create the local test repository with the files defined in the `FILES_TO_COPY_INTO_TEST_REPO` variable. `testWorkflowRepo` is the name of the test repo and can be changed to whichever name you choose, just remember to use it later when accessing this repo. _Note that we can't use `beforeAll()` method, because while mocking steps `Act-js` modifies the workflow file copied into the test repo and thus mocking could persist between tests_ +```javascript +beforeEach(async () => { + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testWorkflowsRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); +}); +``` +Similarly, `afterEach` gets executed after each test. In this case we remove the test repo after the test finishes +```javascript +afterEach(async () => { + await mockGithub.teardown(); +}); +``` +Get path to the local test repo, useful to have it in a variable +```javascript +const repoPath = mockGithub.repo.getPath('testWorkflowsRepo') || ''; +``` +Get path to the workflow under test. Note that it's the path **in the test repo** +```javascript +const workflowPath = path.join(repoPath, '.github', 'workflows', '.yml'); +``` +Instantiate `Act` object instance. Here we provide the constructor with the path to the test repo (so that `Act` can execute in its context) and the path the workflow file under test (so just the workflow we want to test would be executed) +```javascript +let act = new kieActJs.Act(repoPath, workflowPath); +``` +Set up initial parameters for `Act`. This is where we can set secrets, GitHub token and options for the events (like the name of the branch to which the push has been made, etc.) +```javascript +act = utils.setUpActParams( + act, + '', + {head: {ref: ''}}, + {'': '', +); +``` +Set up step mocks. Here we configure which steps in the workflow should be mocked, and with what behaviour. This takes form of an object with keys corresponding to the names of the jobs in the workflow, and values being mock definitions for specific steps. The steps can be identified either by `id`, `name`, `uses` or `run`. Step mock can be defined either by hand (``) or with the helper method `utils.getMockStep()` (``). Not mocked steps will be executed normally - **make sure this will not have unexpected consequences** +```javascript +const testMockSteps = { + '': [ + { + name: '', + mockWith: '', + }, + { + name: '', + mockWith: '', + }, + ], + '': [ + utils.getMockStep('', ''), + utils.getMockStep('', ''), + ], +}; +``` +Most important part - actually running the event with `Act`. This executes the specified `` in the context of the local test repo created before and with the workflow under test set up. `result` stores the output of `Act` execution, which can then be compared to what was expected. Note that the `workflowFile` is actually path to _workflow folder_ and not the file itself - `Act-js` determines the name of the workflow by itself, and tries to find it in the specified `workflowFile` path, so _providing the full path to the file will fail_ +```javascript +const result = await act + .runEvent('', { + workflowFile: path.join(repoPath, '.github', 'workflows'), + mockSteps: testMockSteps, + }); +``` +Assert results are as expected. This can, for example, include using `expect()` to check if the steps that should be executed have indeed been executed, steps that shouldn't run have not been executed, compare statuses (which steps succeeded, which failed) and step outputs. Outputs can include additional information, like input values, environmental variables, secrets (although these are usually not accessible and represented by `***`, this can still be useful to check if the value exists or not). Here it's usually done with the helper assertion methods defined in the assertions file. Step assertions can be created manually or with `getStepAssertion()` helper method +```javascript +assertions.assertSomethingHappened(result); +assertions.assertSomethingDidNotHappen(result, false); +``` + +## FAQ +### I'm positive that one of the jobs should run, but it doesn't - why? +#### Check the runner type (`runs-on`) it may not be set (which `Act` does not like) or it may be set to one of the unsupported types (primarily the `macos-...` runner types). You can always overwrite the runner type with `utils.setJobRunners()` helper method +### My workflow has many jobs, each with many steps, how do I start testing it without spending hours on setup? +#### First of all, consider splitting the workflow into several smaller pieces, with the main one acting as coordinator and calling the others. Secondly, you can bootstrap the test with `npm run workflow-test:generate .yml`, which will generate mocks and assertions for you, as well as the stub of the test file +### After using `workflow-test:generate` the files are incomplete, or they have errors. Why? +#### Make sure that the workflow file you want to test, has all steps with names, as the bootstrapping script uses step names to locate and mock them - same with assertions. After you've added the `name` properties to steps, remove the previously generated files and run the command again +### I want to just run the test that I am working on, without all the others - how can I do it? +#### You can pass parameters to the `npm run workflow-test` command as you would with `jest` or `npm test` - `npm run workflow-test -- -i ` will run just the tests within `testfile`. You can also filter further with `-t ` diff --git a/workflow_tests/assertions/authorChecklistAssertions.js b/workflow_tests/assertions/authorChecklistAssertions.js new file mode 100644 index 000000000000..c57fe922c754 --- /dev/null +++ b/workflow_tests/assertions/authorChecklistAssertions.js @@ -0,0 +1,17 @@ +const utils = require('../utils/utils'); + +const assertChecklistJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('authorChecklist.js', true, null, 'CHECKLIST', 'Running authorChecklist.js', [{key: 'GITHUB_TOKEN', value: '***'}], [])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertChecklistJobExecuted, +}; diff --git a/workflow_tests/assertions/cherryPickAssertions.js b/workflow_tests/assertions/cherryPickAssertions.js new file mode 100644 index 000000000000..42ecc3d64262 --- /dev/null +++ b/workflow_tests/assertions/cherryPickAssertions.js @@ -0,0 +1,119 @@ +const utils = require('../utils/utils'); + +const assertValidateActorJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Check if user is deployer', true, null, 'VALIDATEACTOR', 'Checking if user is a deployer', [], [{key: 'GITHUB_TOKEN', value: '***'}])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertCreateNewVersionJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Create new version', true, null, 'CREATENEWVERSION', 'Creating new version', [], [])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertCherryPickJobExecuted = (workflowResult, user = 'Dummy Author', pullRequestNumber = '1234', didExecute = true, hasConflicts = false, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion( + 'Checkout staging branch', + true, + null, + 'CHERRYPICK', + 'Checking out staging branch', + [ + {key: 'ref', value: 'staging'}, + {key: 'token', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Set up git for OSBotify', true, null, 'CHERRYPICK', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}], []), + utils.createStepAssertion('Get previous app version', true, null, 'CHERRYPICK', 'Get previous app version', [{key: 'SEMVER_LEVEL', value: 'PATCH'}]), + utils.createStepAssertion('Fetch history of relevant refs', true, null, 'CHERRYPICK', 'Fetch history of relevant refs'), + utils.createStepAssertion('Get version bump commit', true, null, 'CHERRYPICK', 'Get version bump commit', [], []), + utils.createStepAssertion( + 'Get merge commit for pull request to CP', + true, + null, + 'CHERRYPICK', + 'Get merge commit for pull request to CP', + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'USER', value: user}, + {key: 'PULL_REQUEST_NUMBER', value: pullRequestNumber}, + ], + [], + ), + utils.createStepAssertion('Cherry-pick the version-bump to staging', true, null, 'CHERRYPICK', 'Cherry-picking the version-bump to staging', [], []), + utils.createStepAssertion('Cherry-pick the merge commit of target PR', true, null, 'CHERRYPICK', 'Cherry-picking the merge commit of target PR', [], []), + utils.createStepAssertion('Push changes', true, null, 'CHERRYPICK', 'Pushing changes', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const conflictSteps = [ + utils.createStepAssertion( + 'Create Pull Request to manually finish CP', + true, + null, + 'CHERRYPICK', + 'Creating Pull Request to manually finish CP', + [], + [{key: 'GITHUB_TOKEN', value: '***'}], + ), + ]; + + conflictSteps.forEach((step) => { + if (didExecute && hasConflicts) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + }); + + const failedSteps = [ + utils.createStepAssertion( + 'Announces a CP failure in the #announce Slack room', + true, + null, + 'CHERRYPICK', + 'Announcing a CP failure', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + ]; + + failedSteps.forEach((step) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + }); +}; + +module.exports = { + assertValidateActorJobExecuted, + assertCreateNewVersionJobExecuted, + assertCherryPickJobExecuted, +}; diff --git a/workflow_tests/assertions/claAssertions.js b/workflow_tests/assertions/claAssertions.js new file mode 100644 index 000000000000..b85eb263d838 --- /dev/null +++ b/workflow_tests/assertions/claAssertions.js @@ -0,0 +1,73 @@ +const utils = require('../utils/utils'); + +const assertCLAJobExecuted = (workflowResult, commentBody = '', githubRepository = '', didExecute = true, runAssistant = true) => { + const steps = [ + utils.createStepAssertion( + 'CLA comment check', + true, + null, + 'CLA', + 'CLA comment check', + [ + {key: 'text', value: commentBody}, + {key: 'regex', value: '\\s*I have read the CLA Document and I hereby sign the CLA\\s*'}, + ], + [], + ), + utils.createStepAssertion( + 'CLA comment re-check', + true, + null, + 'CLA', + 'CLA comment re-check', + [ + {key: 'text', value: commentBody}, + {key: 'regex', value: '\\s*recheck\\s*'}, + ], + [], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const assistantSteps = [ + utils.createStepAssertion( + 'CLA Assistant', + true, + null, + 'CLA', + 'CLA Assistant', + [ + {key: 'path-to-signatures', value: `${githubRepository}/cla.json`}, + {key: 'path-to-document', value: `https://github.com/${githubRepository}/blob/main/contributingGuides/CLA.md`}, + {key: 'branch', value: 'main'}, + {key: 'remote-organization-name', value: 'Expensify'}, + {key: 'remote-repository-name', value: 'CLA'}, + {key: 'lock-pullrequest-aftermerge', value: false}, + {key: 'allowlist', value: 'OSBotify,snyk-bot'}, + ], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'PERSONAL_ACCESS_TOKEN', value: '***'}, + ], + ), + ]; + + assistantSteps.forEach((step) => { + if (didExecute && runAssistant) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + }); +}; + +module.exports = { + assertCLAJobExecuted, +}; diff --git a/workflow_tests/assertions/createNewVersionAssertions.js b/workflow_tests/assertions/createNewVersionAssertions.js new file mode 100644 index 000000000000..e4526ae59be2 --- /dev/null +++ b/workflow_tests/assertions/createNewVersionAssertions.js @@ -0,0 +1,72 @@ +const utils = require('../utils/utils'); + +const assertValidateActorJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Get user permissions', true, null, 'VALIDATEACTOR', 'Get user permissions', [], [{key: 'GITHUB_TOKEN', value: '***'}])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertCreateNewVersionJobExecuted = (workflowResult, semverLevel = 'BUILD', didExecute = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion('Run turnstyle', true, null, 'CREATENEWVERSION', 'Run turnstyle', [{key: 'poll-interval-seconds', value: '10'}], [{key: 'GITHUB_TOKEN', value: '***'}]), + utils.createStepAssertion( + 'Check out', + true, + null, + 'CREATENEWVERSION', + 'Check out', + [ + {key: 'ref', value: 'main'}, + {key: 'token', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'CREATENEWVERSION', 'Setup git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}], []), + utils.createStepAssertion( + 'Generate version', + true, + null, + 'CREATENEWVERSION', + 'Generate version', + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SEMVER_LEVEL', value: semverLevel}, + ], + [], + ), + utils.createStepAssertion('Commit new version', true, null, 'CREATENEWVERSION', 'Commit new version', [], []), + utils.createStepAssertion('Update main branch', true, null, 'CREATENEWVERSION', 'Update main branch', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + if (isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failedSteps = [ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CREATENEWVERSION', 'Announce failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + failedSteps.forEach((step) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + }); +}; + +module.exports = { + assertValidateActorJobExecuted, + assertCreateNewVersionJobExecuted, +}; diff --git a/workflow_tests/assertions/deployAssertions.js b/workflow_tests/assertions/deployAssertions.js new file mode 100644 index 000000000000..bff99298bde5 --- /dev/null +++ b/workflow_tests/assertions/deployAssertions.js @@ -0,0 +1,63 @@ +const utils = require('../utils/utils'); + +const assertDeployStagingJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout staging branch', true, null, 'DEPLOY_STAGING', 'Checking out staging branch', [ + {key: 'ref', value: 'staging'}, + {key: 'token', value: '***'}, + ]), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_STAGING', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Tag version', true, null, 'DEPLOY_STAGING', 'Tagging new version'), + utils.createStepAssertion('🚀 Push tags to trigger staging deploy 🚀', true, null, 'DEPLOY_STAGING', 'Pushing tag to trigger staging deploy'), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertDeployProductionJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'DEPLOY_PRODUCTION', 'Checking out', [ + {key: 'ref', value: 'production'}, + {key: 'token', value: '***'}, + ]), + utils.createStepAssertion('Setup git for OSBotify', true, null, 'DEPLOY_PRODUCTION', 'Setting up git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Get current app version', true, null, 'DEPLOY_PRODUCTION', 'Getting current app version'), + utils.createStepAssertion('Get Release Pull Request List', true, null, 'DEPLOY_PRODUCTION', 'Getting release PR list', [ + {key: 'TAG', value: '1.2.3'}, + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'IS_PRODUCTION_DEPLOY', value: 'true'}, + ]), + utils.createStepAssertion('Generate Release Body', true, null, 'DEPLOY_PRODUCTION', 'Generating release body', [{key: 'PR_LIST', value: '[1.2.1, 1.2.2]'}]), + utils.createStepAssertion( + '🚀 Create release to trigger production deploy 🚀', + true, + null, + 'DEPLOY_PRODUCTION', + 'Creating release to trigger production deploy', + [ + {key: 'tag_name', value: '1.2.3'}, + {key: 'body', value: 'Release body'}, + ], + [{key: 'GITHUB_TOKEN', value: '***'}], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertDeployStagingJobExecuted, + assertDeployProductionJobExecuted, +}; diff --git a/workflow_tests/assertions/deployBlockerAssertions.js b/workflow_tests/assertions/deployBlockerAssertions.js new file mode 100644 index 000000000000..8d2d6039960e --- /dev/null +++ b/workflow_tests/assertions/deployBlockerAssertions.js @@ -0,0 +1,98 @@ +const utils = require('../utils/utils'); + +const assertDeployBlockerJobExecuted = (workflowResult, issueTitle, issueNumber, didExecute = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'DEPLOYBLOCKER', 'Checkout', [{key: 'token', value: '***'}], []), + utils.createStepAssertion( + 'Get URL, title, & number of new deploy blocker (issue)', + true, + null, + 'DEPLOYBLOCKER', + 'Get URL, title and number of new deploy blocker - issue', + [], + [{key: 'TITLE', value: issueTitle}], + ), + utils.createStepAssertion( + 'Update StagingDeployCash with new deploy blocker', + true, + null, + 'DEPLOYBLOCKER', + 'Update StagingDeployCash with new deploy blocker', + [{key: 'GITHUB_TOKEN', value: '***'}], + [], + ), + utils.createStepAssertion( + 'Give the issue/PR the Hourly, Engineering labels', + true, + null, + 'DEPLOYBLOCKER', + 'Give the issue/PR the Hourly, Engineering labels', + [ + {key: 'add-labels', value: 'Hourly, Engineering'}, + {key: 'remove-labels', value: 'Daily, Weekly, Monthly'}, + ], + [], + ), + utils.createStepAssertion( + 'Comment on deferred PR', + true, + null, + 'DEPLOYBLOCKER', + 'Comment on deferred PR', + [ + {key: 'github_token', value: '***'}, + {key: 'number', value: issueNumber}, + ], + [], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + if (isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const successSteps = [ + utils.createStepAssertion( + 'Post the issue in the #expensify-open-source slack room', + true, + null, + 'DEPLOYBLOCKER', + 'Post the issue in the expensify-open-source slack room', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + ]; + + successSteps.forEach((step) => { + if (didExecute && isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + }); + + const failedSteps = [ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'DEPLOYBLOCKER', 'Announce failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + failedSteps.forEach((step) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + }); +}; + +module.exports = { + assertDeployBlockerJobExecuted, +}; diff --git a/workflow_tests/assertions/finishReleaseCycleAssertions.js b/workflow_tests/assertions/finishReleaseCycleAssertions.js new file mode 100644 index 000000000000..12a8b9f2b05d --- /dev/null +++ b/workflow_tests/assertions/finishReleaseCycleAssertions.js @@ -0,0 +1,186 @@ +const utils = require('../utils/utils'); + +const assertValidateJobExecuted = (workflowResult, issueNumber = '', didExecute = true, isTeamMember = true, hasBlockers = false, isSuccessful = true) => { + const steps = [utils.createStepAssertion('Validate actor is deployer', true, null, 'VALIDATE', 'Validating if actor is deployer', [], [{key: 'GITHUB_TOKEN', value: '***'}])]; + if (isTeamMember) { + steps.push( + utils.createStepAssertion( + 'Check for any deploy blockers', + true, + null, + 'VALIDATE', + 'Checking for deploy blockers', + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'ISSUE_NUMBER', value: issueNumber}, + ], + [], + ), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + // eslint-disable-next-line rulesdir/no-negated-variables + const notTeamMemberSteps = [ + utils.createStepAssertion( + 'Reopen and comment on issue (not a team member)', + true, + null, + 'VALIDATE', + 'Reopening issue - not a team member', + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'ISSUE_NUMBER', value: issueNumber}, + {key: 'COMMENT', value: 'Sorry, only members of @Expensify/Mobile-Deployers can close deploy checklists.\nReopening!'}, + ], + [], + ), + ]; + + notTeamMemberSteps.forEach((expectedStep) => { + if (didExecute && !isTeamMember) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const blockerSteps = [ + utils.createStepAssertion( + 'Reopen and comment on issue (has blockers)', + true, + null, + 'VALIDATE', + 'Reopening issue - blockers', + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'ISSUE_NUMBER', value: issueNumber}, + ], + [], + ), + ]; + + blockerSteps.forEach((expectedStep) => { + if (didExecute && hasBlockers) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failedSteps = [ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'VALIDATE', 'Announce failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + failedSteps.forEach((expectedStep) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertUpdateProductionJobExecuted = (workflowResult, didExecute = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion( + 'Checkout', + true, + null, + 'UPDATEPRODUCTION', + 'Checkout', + [ + {key: 'ref', value: 'staging'}, + {key: 'token', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Setup Git for OSBotify', true, null, 'UPDATEPRODUCTION', 'Setup Git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}], []), + utils.createStepAssertion('Update production branch', true, null, 'UPDATEPRODUCTION', 'Updating production branch', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failedSteps = [ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'UPDATEPRODUCTION', 'Announce failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + failedSteps.forEach((expectedStep) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertCreateNewPatchVersionJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Create new version', true, null, 'CREATENEWPATCHVERSION', 'Creating new version', [{key: 'SEMVER_LEVEL', value: 'PATCH'}], [])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertUpdateStagingJobExecuted = (workflowResult, didExecute = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion( + 'Checkout', + true, + null, + 'UPDATESTAGING', + 'Checkout', + [ + {key: 'ref', value: 'main'}, + {key: 'token', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Setup Git for OSBotify', true, null, 'UPDATESTAGING', 'Setup Git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}], []), + utils.createStepAssertion('Update staging branch to trigger staging deploy', true, null, 'UPDATESTAGING', 'Updating staging branch', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failedSteps = [ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'UPDATESTAGING', 'Announce failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + failedSteps.forEach((expectedStep) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertValidateJobExecuted, + assertUpdateProductionJobExecuted, + assertCreateNewPatchVersionJobExecuted, + assertUpdateStagingJobExecuted, +}; diff --git a/workflow_tests/assertions/lintAssertions.js b/workflow_tests/assertions/lintAssertions.js new file mode 100644 index 000000000000..938f9b383464 --- /dev/null +++ b/workflow_tests/assertions/lintAssertions.js @@ -0,0 +1,22 @@ +const utils = require('../utils/utils'); + +const assertLintJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'LINT', 'Checkout', [], []), + utils.createStepAssertion('Setup Node', true, null, 'LINT', 'Setup Node', [], []), + utils.createStepAssertion('Lint JavaScript and Typescript with ESLint', true, null, 'LINT', 'Lint JavaScript with ESLint', [], [{key: 'CI', value: 'true'}]), + utils.createStepAssertion('Lint shell scripts with ShellCheck', true, null, 'LINT', 'Lint shell scripts with ShellCheck', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertLintJobExecuted, +}; diff --git a/workflow_tests/assertions/lockDeploysAssertions.js b/workflow_tests/assertions/lockDeploysAssertions.js new file mode 100644 index 000000000000..874076dc3a28 --- /dev/null +++ b/workflow_tests/assertions/lockDeploysAssertions.js @@ -0,0 +1,84 @@ +const utils = require('../utils/utils'); + +const assertlockStagingDeploysJobExecuted = (workflowResult, didExecute = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion( + 'Checkout', + true, + null, + 'LOCKSTAGINGDEPLOYS', + 'Checking out', + [ + {key: 'ref', value: 'main'}, + {key: 'token', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Wait for staging deploys to finish', true, null, 'LOCKSTAGINGDEPLOYS', 'Waiting for staging deploys to finish', [{key: 'GITHUB_TOKEN', value: '***'}], []), + utils.createStepAssertion( + 'Comment in StagingDeployCash to give Applause the 🟢 to begin QA', + true, + null, + 'LOCKSTAGINGDEPLOYS', + 'Commenting in StagingDeployCash', + [], + [{key: 'GITHUB_TOKEN', value: '***'}], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failProdSteps = [ + utils.createStepAssertion('Announce failed workflow', true, null, 'LOCKSTAGINGDEPLOYS', 'Announcing failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + failProdSteps.forEach((expectedStep) => { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertlockStagingDeploysJobFailedAfterFirstStep = (workflowResult) => { + const steps = [ + utils.createStepAssertion( + 'Checkout', + true, + null, + 'LOCKSTAGINGDEPLOYS', + 'Checking out', + [ + {key: 'ref', value: 'main'}, + {key: 'token', value: '***'}, + ], + [], + ), + utils.createStepAssertion( + 'Wait for staging deploys to finish', + false, + null, + 'LOCKSTAGINGDEPLOYS', + 'Waiting for staging deploys to finish', + [{key: 'GITHUB_TOKEN', value: '***'}], + [], + ), + utils.createStepAssertion('Announce failed workflow', true, null, 'LOCKSTAGINGDEPLOYS', 'Announcing failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}], []), + ]; + + steps.forEach((expectedStep) => { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + }); +}; + +module.exports = { + assertlockStagingDeploysJobExecuted, + assertlockStagingDeploysJobFailedAfterFirstStep, +}; diff --git a/workflow_tests/assertions/platformDeployAssertions.js b/workflow_tests/assertions/platformDeployAssertions.js new file mode 100644 index 000000000000..35242cd24d31 --- /dev/null +++ b/workflow_tests/assertions/platformDeployAssertions.js @@ -0,0 +1,371 @@ +const utils = require('../utils/utils'); + +const assertVerifyActorJobExecuted = (workflowResult, username, didExecute = true) => { + const steps = [utils.createStepAssertion('Check if user is deployer', true, null, 'VALIDATE_ACTOR', 'Checking if the user is a deployer', [], [{key: 'GITHUB_TOKEN', value: '***'}])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertDeployChecklistJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'DEPLOY_CHECKLIST', 'Checkout'), + utils.createStepAssertion('Setup Node', true, null, 'DEPLOY_CHECKLIST', 'Setup Node'), + utils.createStepAssertion('Set version', true, null, 'DEPLOY_CHECKLIST', 'Set version'), + utils.createStepAssertion('Create or update staging deploy', true, null, 'DEPLOY_CHECKLIST', 'Create or update staging deploy', [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'NPM_VERSION', value: '1.2.3'}, + ]), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertAndroidJobExecuted = (workflowResult, didExecute = true, isProduction = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'ANDROID', 'Checking out'), + utils.createStepAssertion('Configure MapBox SDK', true, null, 'ANDROID', 'Configure MapBox SDK'), + utils.createStepAssertion('Setup Node', true, null, 'ANDROID', 'Setting up Node'), + utils.createStepAssertion('Setup Ruby', true, null, 'ANDROID', 'Setting up Ruby', [ + {key: 'ruby-version', value: '2.7'}, + {key: 'bundler-cache', value: 'true'}, + ]), + utils.createStepAssertion('Decrypt keystore', true, null, 'ANDROID', 'Decrypting keystore', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Decrypt json key', true, null, 'ANDROID', 'Decrypting JSON key', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Set version in ENV', true, null, 'ANDROID', 'Setting version in ENV'), + ]; + if (!isProduction) { + steps.push( + utils.createStepAssertion('Run Fastlane beta', true, null, 'ANDROID', 'Running Fastlane beta', null, [ + {key: 'MYAPP_UPLOAD_STORE_PASSWORD', value: '***'}, + {key: 'MYAPP_UPLOAD_KEY_PASSWORD', value: '***'}, + ]), + ); + } else { + steps.push(utils.createStepAssertion('Run Fastlane production', true, null, 'ANDROID', 'Running Fastlane production', null, [{key: 'VERSION', value: '1.2.3'}])); + } + steps.push( + utils.createStepAssertion('Archive Android sourcemaps', true, null, 'ANDROID', 'Archiving Android sourcemaps', [ + {key: 'name', value: 'android-sourcemap'}, + {key: 'path', value: 'android/app/build/generated/sourcemaps/react/release/*.map'}, + ]), + ); + if (!isProduction) { + steps.push( + utils.createStepAssertion('Upload Android version to Browser Stack', true, null, 'ANDROID', 'Uploading Android version to Browser Stack', null, [ + {key: 'BROWSERSTACK', value: '***'}, + ]), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failProdSteps = [ + utils.createStepAssertion( + 'Warn deployers if Android production deploy failed', + true, + null, + 'ANDROID', + 'Warning deployers of failed production deploy', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + ]; + + failProdSteps.forEach((expectedStep) => { + if (didExecute && isProduction && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertDesktopJobExecuted = (workflowResult, didExecute = true, isProduction = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'DESKTOP', 'Checking out'), + utils.createStepAssertion('Setup Node', true, null, 'DESKTOP', 'Setting up Node'), + utils.createStepAssertion('Decrypt Developer ID Certificate', true, null, 'DESKTOP', 'Decrypting developer id certificate', null, [ + {key: 'DEVELOPER_ID_SECRET_PASSPHRASE', value: '***'}, + ]), + ]; + if (isProduction) { + steps.push( + utils.createStepAssertion('Build production desktop app', true, null, 'DESKTOP', 'Building production desktop app', null, [ + {key: 'CSC_LINK', value: '***'}, + {key: 'CSC_KEY_PASSWORD', value: '***'}, + {key: 'APPLE_ID', value: '***'}, + {key: 'APPLE_APP_SPECIFIC_PASSWORD', value: '***'}, + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ]), + ); + } else { + steps.push( + utils.createStepAssertion('Build staging desktop app', true, null, 'DESKTOP', 'Building staging desktop app', null, [ + {key: 'CSC_LINK', value: '***'}, + {key: 'CSC_KEY_PASSWORD', value: '***'}, + {key: 'APPLE_ID', value: '***'}, + {key: 'APPLE_APP_SPECIFIC_PASSWORD', value: '***'}, + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ]), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertIOSJobExecuted = (workflowResult, didExecute = true, isProduction = true, isSuccessful = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'IOS', 'Checking out'), + utils.createStepAssertion('Configure MapBox SDK', true, null, 'IOS', 'Configure MapBox SDK'), + utils.createStepAssertion('Setup Node', true, null, 'IOS', 'Setting up Node'), + utils.createStepAssertion('Setup Ruby', true, null, 'IOS', 'Setting up Ruby', [ + {key: 'ruby-version', value: '2.7'}, + {key: 'bundler-cache', value: 'true'}, + ]), + utils.createStepAssertion('Cache Pod dependencies', true, null, 'IOS', 'Cache Pod dependencies', [ + {key: 'path', value: 'ios/Pods'}, + {key: 'key', value: 'Linux-pods-cache-'}, + {key: 'restore-keys', value: 'Linux-pods-cache-'}, + ]), + utils.createStepAssertion('Compare Podfile.lock and Manifest.lock', true, null, 'IOS', 'Compare Podfile.lock and Manifest.lock'), + utils.createStepAssertion('Install cocoapods', true, null, 'IOS', 'Installing cocoapods', [ + {key: 'timeout_minutes', value: '10'}, + {key: 'max_attempts', value: '5'}, + {key: 'command', value: 'cd ios && bundle exec pod install'}, + ]), + utils.createStepAssertion('Decrypt profile', true, null, 'IOS', 'Decrypting profile', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Decrypt certificate', true, null, 'IOS', 'Decrypting certificate', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Decrypt App Store Connect API key', true, null, 'IOS', 'Decrypting App Store API key', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + ]; + if (!isProduction) { + steps.push( + utils.createStepAssertion('Run Fastlane', true, null, 'IOS', 'Running Fastlane', null, [ + {key: 'APPLE_CONTACT_EMAIL', value: '***'}, + {key: 'APPLE_CONTACT_PHONE', value: '***'}, + {key: 'APPLE_DEMO_EMAIL', value: '***'}, + {key: 'APPLE_DEMO_PASSWORD', value: '***'}, + ]), + ); + } + steps.push( + utils.createStepAssertion('Archive iOS sourcemaps', true, null, 'IOS', 'Archiving sourcemaps', [ + {key: 'name', value: 'ios-sourcemap'}, + {key: 'path', value: 'main.jsbundle.map'}, + ]), + ); + if (!isProduction) { + steps.push(utils.createStepAssertion('Upload iOS version to Browser Stack', true, null, 'IOS', 'Uploading version to Browser Stack', null, [{key: 'BROWSERSTACK', value: '***'}])); + } else { + steps.push( + utils.createStepAssertion('Set iOS version in ENV', true, null, 'IOS', 'Setting iOS version'), + utils.createStepAssertion('Run Fastlane for App Store release', true, null, 'IOS', 'Running Fastlane for release', null, [{key: 'VERSION', value: '1.2.3'}]), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); + + const failProdSteps = [ + utils.createStepAssertion( + 'Warn deployers if iOS production deploy failed', + true, + null, + 'IOS', + 'Warning developers of failed deploy', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + ]; + + failProdSteps.forEach((expectedStep) => { + if (didExecute && isProduction && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertWebJobExecuted = (workflowResult, didExecute = true, isProduction = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'WEB', 'Checking out'), + utils.createStepAssertion('Setup Node', true, null, 'WEB', 'Setting up Node'), + utils.createStepAssertion('Setup Cloudflare CLI', true, null, 'WEB', 'Setting up Cloudflare CLI'), + utils.createStepAssertion('Configure AWS Credentials', true, null, 'WEB', 'Configuring AWS credentials', [ + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ]), + ]; + if (isProduction) { + steps.push( + utils.createStepAssertion('Build web for production', true, null, 'WEB', 'Building web for production'), + utils.createStepAssertion('Build storybook docs for production', true, null, 'WEB', 'Build storybook docs for production'), + utils.createStepAssertion('Deploy production to S3', true, null, 'WEB', 'Deploying production to S3'), + utils.createStepAssertion('Purge production Cloudflare cache', true, null, 'WEB', 'Purging production Cloudflare cache', null, [{key: 'CF_API_KEY', value: '***'}]), + ); + } else { + steps.push( + utils.createStepAssertion('Build web for staging', true, null, 'WEB', 'Building web for staging'), + utils.createStepAssertion('Build storybook docs for staging', true, null, 'WEB', 'Build storybook docs for staging'), + utils.createStepAssertion('Deploy staging to S3', true, null, 'WEB', 'Deploying staging to S3'), + utils.createStepAssertion('Purge staging Cloudflare cache', true, null, 'WEB', 'Purging staging Cloudflare cache', null, [{key: 'CF_API_KEY', value: '***'}]), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertPostSlackOnFailureJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Post Slack message on failure', true, null, 'POST_SLACK_FAIL', 'Posting Slack message on platform deploy failure', [{key: 'SLACK_WEBHOOK', value: '***'}]), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertPostSlackOnSuccessJobExecuted = (workflowResult, didExecute = true, isProduction = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'POST_SLACK_SUCCESS', 'Checking out'), + utils.createStepAssertion('Set version', true, null, 'POST_SLACK_SUCCESS', 'Setting version'), + utils.createStepAssertion( + 'Announces the deploy in the #announce Slack room', + true, + null, + 'POST_SLACK_SUCCESS', + 'Posting message to #announce channel', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + utils.createStepAssertion( + 'Announces the deploy in the #deployer Slack room', + true, + null, + 'POST_SLACK_SUCCESS', + 'Posting message to #deployer channel', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + ]; + if (isProduction) { + steps.push( + utils.createStepAssertion( + 'Announces the deploy in the #expensify-open-source Slack room', + true, + null, + 'POST_SLACK_SUCCESS', + 'Posting message to #expensify-open-source channel', + [{key: 'status', value: 'custom'}], + [ + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'SLACK_WEBHOOK_URL', value: '***'}, + ], + ), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertPostGithubCommentJobExecuted = (workflowResult, didExecute = true, isProduction = true, didDeploy = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'POST_GITHUB_COMMENT', 'Checking out'), + utils.createStepAssertion('Setup Node', true, null, 'POST_GITHUB_COMMENT', 'Setting up Node'), + utils.createStepAssertion('Set version', true, null, 'POST_GITHUB_COMMENT', 'Setting version'), + utils.createStepAssertion('Get Release Pull Request List', true, null, 'POST_GITHUB_COMMENT', 'Getting release pull request list', [ + {key: 'TAG', value: '1.2.3'}, + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'IS_PRODUCTION_DEPLOY', value: isProduction ? 'true' : 'false'}, + ]), + utils.createStepAssertion('Comment on issues', true, null, 'POST_GITHUB_COMMENT', 'Commenting on issues', [ + {key: 'PR_LIST', value: '[1.2.1, 1.2.2]'}, + {key: 'IS_PRODUCTION_DEPLOY', value: isProduction ? 'true' : 'false'}, + {key: 'DEPLOY_VERSION', value: '1.2.3'}, + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'ANDROID', value: didDeploy ? 'success' : ''}, + {key: 'DESKTOP', value: didDeploy ? 'success' : ''}, + {key: 'IOS', value: didDeploy ? 'success' : ''}, + {key: 'WEB', value: didDeploy ? 'success' : ''}, + ]), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertVerifyActorJobExecuted, + assertDeployChecklistJobExecuted, + assertAndroidJobExecuted, + assertDesktopJobExecuted, + assertIOSJobExecuted, + assertWebJobExecuted, + assertPostSlackOnFailureJobExecuted, + assertPostSlackOnSuccessJobExecuted, + assertPostGithubCommentJobExecuted, +}; diff --git a/workflow_tests/assertions/preDeployAssertions.js b/workflow_tests/assertions/preDeployAssertions.js new file mode 100644 index 000000000000..90d6f9febb75 --- /dev/null +++ b/workflow_tests/assertions/preDeployAssertions.js @@ -0,0 +1,219 @@ +const utils = require('../utils/utils'); + +const assertTypecheckJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Run typecheck workflow', true, null, 'TYPECHECK', 'Running typecheck workflow')]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertLintJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Run lint workflow', true, null, 'LINT', 'Running lint workflow')]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertTestJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Run test workflow', true, null, 'TEST', 'Running test workflow')]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertIsExpensifyEmployeeJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Get merged pull request', true, null, 'IS_EXPENSIFY_EMPLOYEE', 'Getting merged pull request', [{key: 'github_token', value: '***'}]), + utils.createStepAssertion( + 'Check whether the PR author is member of Expensify/expensify team', + true, + null, + 'IS_EXPENSIFY_EMPLOYEE', + 'Checking actors Expensify membership', + [], + [{key: 'GITHUB_TOKEN', value: '***'}], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertNewContributorWelcomeMessageJobExecuted = (workflowResult, didExecute = true, isOsBotify = false, isFirstPr = false) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', 'Checking out', [{key: 'token', value: '***'}]), + utils.createStepAssertion('Get merged pull request', true, null, 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', 'Getting merged pull request', [{key: 'github_token', value: '***'}]), + utils.createStepAssertion(isOsBotify ? 'Get PR count for OSBotify' : 'Get PR count for Dummy Author', true, null, 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', 'Getting PR count', [ + {key: 'GITHUB_TOKEN', value: '***'}, + ]), + ]; + const osBotifyBody = + '@OSBotify, Great job getting your first Expensify/App pull request over the finish line! ' + + ":tada:\n\nI know there's a lot of information in our " + + '[contributing guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md), ' + + 'so here are some points to take note of :memo::\n\n1. Now that your first PR has been merged, you can be ' + + "hired for another issue. Once you've completed a few issues, you may be eligible to work on more than one " + + 'job at a time.\n2. Once your PR is deployed to our staging servers, it will undergo quality assurance (QA) ' + + "testing. If we find that it doesn't work as expected or causes a regression, you'll be responsible for " + + 'fixing it. Typically, we would revert this PR and give you another chance to create a similar PR without ' + + 'causing a regression.\n3. Once your PR is deployed to _production_, we start a 7-day timer :alarm_clock:. ' + + 'After it has been on production for 7 days without causing any regressions, then we pay out the Upwork job. ' + + ":moneybag:\n\nSo it might take a while before you're paid for your work, but we typically post multiple " + + "new jobs every day, so there's plenty of opportunity. I hope you've had a positive experience " + + 'contributing to this repo! :blush:'; + const userBody = + '@Dummy Author, Great job getting your first Expensify/App pull request over the finish ' + + "line! :tada:\n\nI know there's a lot of information in our " + + '[contributing guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md), ' + + 'so here are some points to take note of :memo::\n\n1. Now that your first PR has been merged, you can be ' + + "hired for another issue. Once you've completed a few issues, you may be eligible to work on more than one " + + 'job at a time.\n2. Once your PR is deployed to our staging servers, it will undergo quality assurance (QA) ' + + "testing. If we find that it doesn't work as expected or causes a regression, you'll be responsible for " + + 'fixing it. Typically, we would revert this PR and give you another chance to create a similar PR without ' + + 'causing a regression.\n3. Once your PR is deployed to _production_, we start a 7-day timer :alarm_clock:. ' + + 'After it has been on production for 7 days without causing any regressions, then we pay out the Upwork ' + + "job. :moneybag:\n\nSo it might take a while before you're paid for your work, but we typically post " + + "multiple new jobs every day, so there's plenty of opportunity. I hope you've had a positive experience " + + 'contributing to this repo! :blush:'; + if (isFirstPr) { + steps.push( + utils.createStepAssertion( + isOsBotify ? "Comment on OSBotify\\'s first pull request!" : "Comment on Dummy Author\\'s first pull request!", + true, + null, + 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', + 'Creating comment', + [ + {key: 'github_token', value: '***'}, + {key: 'number', value: '12345'}, + {key: 'body', value: isOsBotify ? osBotifyBody : userBody}, + ], + ), + ); + } + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertChooseDeployActionsJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Get merged pull request', true, null, 'CHOOSE_DEPLOY_ACTIONS', 'Getting merged pull request', [{key: 'github_token', value: '***'}]), + utils.createStepAssertion('Check if StagingDeployCash is locked', true, null, 'CHOOSE_DEPLOY_ACTIONS', 'Checking StagingDeployCash', [{key: 'GITHUB_TOKEN', value: '***'}]), + utils.createStepAssertion('Check if merged pull request should trigger a deploy', true, ''), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertSkipDeployJobExecuted = (workflowResult, didExecute = true) => { + const body = ':hand: This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.'; + const steps = [ + utils.createStepAssertion('Comment on deferred PR', true, null, 'SKIP_DEPLOY', 'Skipping deploy', [ + {key: 'github_token', value: '***'}, + {key: 'number', value: '123'}, + {key: 'body', value: body}, + ]), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertCreateNewVersionJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Create new version', true, null, 'CREATE_NEW_VERSION', 'Creating new version')]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertUpdateStagingJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Run turnstyle', true, null, 'UPDATE_STAGING', 'Running turnstyle', [ + {key: 'poll-interval-seconds', value: '10'}, + {key: 'GITHUB_TOKEN', value: '***'}, + ]), + utils.createStepAssertion('Checkout main', true, null, 'UPDATE_STAGING', 'Checkout main', [ + {key: 'ref', value: 'main'}, + {key: 'token', value: '***'}, + ]), + utils.createStepAssertion('Setup Git for OSBotify', true, null, 'UPDATE_STAGING', 'Setup Git for OSBotify', [{key: 'GPG_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Update staging branch from main', true, null, 'UPDATE_STAGING', 'Update staging branch from main'), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertUpdateStagingJobFailed = (workflowResult, didFail = false) => { + const steps = [ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'UPDATE_STAGING', 'Announcing failed workflow in Slack', [{key: 'SLACK_WEBHOOK', value: '***'}]), + ]; + + steps.forEach((expectedStep) => { + if (didFail) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertTypecheckJobExecuted, + assertLintJobExecuted, + assertTestJobExecuted, + assertIsExpensifyEmployeeJobExecuted, + assertNewContributorWelcomeMessageJobExecuted, + assertChooseDeployActionsJobExecuted, + assertSkipDeployJobExecuted, + assertCreateNewVersionJobExecuted, + assertUpdateStagingJobExecuted, + assertUpdateStagingJobFailed, +}; diff --git a/workflow_tests/assertions/reviewerChecklistAssertions.js b/workflow_tests/assertions/reviewerChecklistAssertions.js new file mode 100644 index 000000000000..6154b1cd28ca --- /dev/null +++ b/workflow_tests/assertions/reviewerChecklistAssertions.js @@ -0,0 +1,17 @@ +const utils = require('../utils/utils'); + +const assertChecklistJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('reviewerChecklist.js', true, null, 'CHECKLIST', 'reviewerChecklist.js', [{key: 'GITHUB_TOKEN', value: '***'}], [])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertChecklistJobExecuted, +}; diff --git a/workflow_tests/assertions/testAssertions.js b/workflow_tests/assertions/testAssertions.js new file mode 100644 index 000000000000..cee379ad5903 --- /dev/null +++ b/workflow_tests/assertions/testAssertions.js @@ -0,0 +1,58 @@ +const utils = require('../utils/utils'); + +const assertJestJobExecuted = (workflowResult, didExecute = true, timesExecuted = 3) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'JEST', 'Checkout', [], []), + utils.createStepAssertion('Setup Node', true, null, 'JEST', 'Setup Node', [], []), + utils.createStepAssertion('Get number of CPU cores', true, null, 'JEST', 'Get number of CPU cores', [], []), + utils.createStepAssertion( + 'Cache Jest cache', + true, + null, + 'JEST', + 'Cache Jest cache', + [ + {key: 'path', value: '.jest-cache'}, + {key: 'key', value: 'Linux-jest'}, + ], + [], + ), + utils.createStepAssertion('Jest tests', true, null, 'JEST', 'Jest tests', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + let cnt = 0; + workflowResult.forEach((executedStep) => { + if (executedStep.name !== expectedStep.name || executedStep.output !== expectedStep.output || executedStep.status !== expectedStep.status) { + return; + } + cnt += 1; + }); + expect(cnt).toEqual(timesExecuted); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertShellTestsJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'SHELLTESTS', 'Checkout', [], []), + utils.createStepAssertion('Setup Node', true, null, 'SHELLTESTS', 'Setup Node', [], []), + utils.createStepAssertion('Test CI git logic', true, null, 'SHELLTESTS', 'Test CI git logic', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertJestJobExecuted, + assertShellTestsJobExecuted, +}; diff --git a/workflow_tests/assertions/testBuildAssertions.js b/workflow_tests/assertions/testBuildAssertions.js new file mode 100644 index 000000000000..a4f7a46259ad --- /dev/null +++ b/workflow_tests/assertions/testBuildAssertions.js @@ -0,0 +1,413 @@ +const utils = require('../utils/utils'); + +const assertValidateActorJobExecuted = (workflowResult, pullRequestNumber = '1234', didExecute = true) => { + const steps = [ + utils.createStepAssertion('Is Expensify employee', true, null, 'VALIDATEACTOR', 'Is Expensify employee', [], [{key: 'GITHUB_TOKEN', value: '***'}]), + utils.createStepAssertion( + 'Set HAS_READY_TO_BUILD_LABEL flag', + true, + null, + 'VALIDATEACTOR', + 'Set HAS_READY_TO_BUILD_LABEL flag', + [], + [ + {key: 'PULL_REQUEST_NUMBER', value: pullRequestNumber}, + {key: 'GITHUB_TOKEN', value: '***'}, + ], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertGetBranchRefJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'GETBRANCHREF', 'Checkout', [], []), + utils.createStepAssertion( + 'Check if pull request number is correct', + true, + null, + 'GETBRANCHREF', + 'Check if pull request number is correct', + [], + [{key: 'GITHUB_TOKEN', value: '***'}], + ), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertAndroidJobExecuted = (workflowResult, ref = '', didExecute = true, failsAt = -1) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'ANDROID', 'Checkout', [{key: 'ref', value: ref}], []), + utils.createStepAssertion( + 'Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', + true, + null, + 'ANDROID', + 'Creating .env.adhoc file based on staging', + [], + [], + ), + utils.createStepAssertion('Setup Node', true, null, 'ANDROID', 'Setup Node', [], []), + utils.createStepAssertion( + 'Setup Ruby', + true, + null, + 'ANDROID', + 'Setup Ruby', + [ + {key: 'ruby-version', value: '2.7'}, + {key: 'bundler-cache', value: true}, + ], + [], + ), + utils.createStepAssertion('Decrypt keystore', true, null, 'ANDROID', 'Decrypt keystore', [], [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Decrypt json key', true, null, 'ANDROID', 'Decrypt json key', [], [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion( + 'Configure AWS Credentials', + true, + null, + 'ANDROID', + 'Configure AWS Credentials', + [ + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Configure MapBox SDK', true, null, 'ANDROID', 'Configure MapBox SDK'), + utils.createStepAssertion( + 'Run Fastlane beta test', + true, + null, + 'ANDROID', + 'Run Fastlane beta test', + [], + [ + {key: 'S3_ACCESS_KEY', value: '***'}, + {key: 'S3_SECRET_ACCESS_KEY', value: '***'}, + {key: 'S3_BUCKET', value: 'ad-hoc-expensify-cash'}, + {key: 'S3_REGION', value: 'us-east-1'}, + {key: 'MYAPP_UPLOAD_STORE_PASSWORD', value: '***'}, + {key: 'MYAPP_UPLOAD_KEY_PASSWORD', value: '***'}, + ], + ), + utils.createStepAssertion( + 'Upload Artifact', + true, + null, + 'ANDROID', + 'Upload Artifact', + [ + {key: 'name', value: 'android'}, + {key: 'path', value: './android_paths.json'}, + ], + [], + ), + ]; + + steps.forEach((expectedStep, i) => { + if (didExecute) { + if (failsAt === -1 || i < failsAt) { + // either whole job is successful, or steps up to this point are successful + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else if (i === failsAt) { + // this is the failing step + steps[i].status = 1; + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + // steps after failed one do not execute + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertIOSJobExecuted = (workflowResult, ref = '', didExecute = true, failsAt = -1) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'IOS', 'Checkout', [{key: 'ref', value: ref}], []), + utils.createStepAssertion('Configure MapBox SDK', true, null, 'IOS', 'Configure MapBox SDK'), + utils.createStepAssertion('Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', true, null, 'IOS', 'Creating .env.adhoc file based on staging', [], []), + utils.createStepAssertion('Setup Node', true, null, 'IOS', 'Setup Node', [], []), + utils.createStepAssertion('Setup XCode', true, null, 'IOS', 'Setup XCode', [], []), + utils.createStepAssertion( + 'Setup Ruby', + true, + null, + 'IOS', + 'Setup Ruby', + [ + {key: 'ruby-version', value: '2.7'}, + {key: 'bundler-cache', value: true}, + ], + [], + ), + utils.createStepAssertion('Cache Pod dependencies', true, null, 'IOS', 'Cache Pod dependencies', [ + {key: 'path', value: 'ios/Pods'}, + {key: 'key', value: 'Linux-pods-cache-'}, + {key: 'restore-keys', value: 'Linux-pods-cache-'}, + ]), + utils.createStepAssertion('Compare Podfile.lock and Manifest.lock', true, null, 'IOS', 'Compare Podfile.lock and Manifest.lock'), + utils.createStepAssertion( + 'Install cocoapods', + true, + null, + 'IOS', + 'Install cocoapods', + [ + {key: 'timeout_minutes', value: '10'}, + {key: 'max_attempts', value: '5'}, + {key: 'command', value: 'cd ios && bundle exec pod install'}, + ], + [], + ), + utils.createStepAssertion('Decrypt profile', true, null, 'IOS', 'Decrypt profile', [], [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion('Decrypt certificate', true, null, 'IOS', 'Decrypt certificate', [], [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion( + 'Configure AWS Credentials', + true, + null, + 'IOS', + 'Configure AWS Credentials', + [ + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ], + [], + ), + utils.createStepAssertion( + 'Run Fastlane', + true, + null, + 'IOS', + 'Run Fastlane', + [], + [ + {key: 'S3_ACCESS_KEY', value: '***'}, + {key: 'S3_SECRET_ACCESS_KEY', value: '***'}, + {key: 'S3_BUCKET', value: 'ad-hoc-expensify-cash'}, + {key: 'S3_REGION', value: 'us-east-1'}, + ], + ), + utils.createStepAssertion( + 'Upload Artifact', + true, + null, + 'IOS', + 'Upload Artifact', + [ + {key: 'name', value: 'ios'}, + {key: 'path', value: './ios_paths.json'}, + ], + [], + ), + ]; + + steps.forEach((expectedStep, i) => { + if (didExecute) { + if (failsAt === -1 || i < failsAt) { + // either whole job is successful, or steps up to this point are successful + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else if (i === failsAt) { + // this is the failing step + steps[i].status = 1; + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + // steps after failed one do not execute + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertDesktopJobExecuted = (workflowResult, ref = '', didExecute = true, failsAt = -1) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'DESKTOP', 'Checkout', [{key: 'ref', value: ref}], []), + utils.createStepAssertion( + 'Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', + true, + null, + 'DESKTOP', + 'Creating .env.adhoc file based on staging', + [], + [], + ), + utils.createStepAssertion('Setup Node', true, null, 'DESKTOP', 'Setup Node', [], []), + utils.createStepAssertion('Decrypt Developer ID Certificate', true, null, 'DESKTOP', 'Decrypt Developer ID Certificate', [], [{key: 'DEVELOPER_ID_SECRET_PASSPHRASE', value: '***'}]), + utils.createStepAssertion( + 'Configure AWS Credentials', + true, + null, + 'DESKTOP', + 'Configure AWS Credentials', + [ + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ], + [], + ), + utils.createStepAssertion( + 'Build desktop app for testing', + true, + null, + 'DESKTOP', + 'Build desktop app for testing', + [], + [ + {key: 'CSC_LINK', value: '***'}, + {key: 'CSC_KEY_PASSWORD', value: '***'}, + {key: 'APPLE_ID', value: '***'}, + {key: 'APPLE_APP_SPECIFIC_PASSWORD', value: '***'}, + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ], + ), + ]; + + steps.forEach((expectedStep, i) => { + if (didExecute) { + if (failsAt === -1 || i < failsAt) { + // either whole job is successful, or steps up to this point are successful + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else if (i === failsAt) { + // this is the failing step + steps[i].status = 1; + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + // steps after failed one do not execute + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; +const assertWebJobExecuted = (workflowResult, ref = '', didExecute = true, failsAt = -1) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'WEB', 'Checkout', [{key: 'ref', value: ref}], []), + utils.createStepAssertion('Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', true, null, 'WEB', 'Creating .env.adhoc file based on staging', [], []), + utils.createStepAssertion('Setup Node', true, null, 'WEB', 'Setup Node', [], []), + utils.createStepAssertion( + 'Configure AWS Credentials', + true, + null, + 'WEB', + 'Configure AWS Credentials', + [ + {key: 'AWS_ACCESS_KEY_ID', value: '***'}, + {key: 'AWS_SECRET_ACCESS_KEY', value: '***'}, + ], + [], + ), + utils.createStepAssertion('Build web for testing', true, null, 'WEB', 'Build web for testing', [], []), + utils.createStepAssertion('Build docs', true, null, 'WEB', 'Build docs', [], []), + utils.createStepAssertion('Deploy to S3 for internal testing', true, null, 'WEB', 'Deploy to S3 for internal testing', [], []), + ]; + + steps.forEach((expectedStep, i) => { + if (didExecute) { + if (failsAt === -1 || i < failsAt) { + // either whole job is successful, or steps up to this point are successful + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else if (i === failsAt) { + // this is the failing step + steps[i].status = 1; + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + // steps after failed one do not execute + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +const assertPostGithubCommentJobExecuted = ( + workflowResult, + ref = '', + pullRequestNumber = '1234', + didExecute = true, + androidStatus = 'success', + iOSStatus = 'success', + desktopStatus = 'success', + webStatus = 'success', +) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'POSTGITHUBCOMMENT', 'Checkout', [{key: 'ref', value: ref}], []), + utils.createStepAssertion('Download Artifact', true, null, 'POSTGITHUBCOMMENT', 'Download Artifact', [], []), + ]; + if (androidStatus === 'success') { + steps.push(utils.createStepAssertion('Read JSONs with android paths', true, null, 'POSTGITHUBCOMMENT', 'Read JSONs with android paths', [], [])); + } + if (iOSStatus === 'success') { + steps.push(utils.createStepAssertion('Read JSONs with iOS paths', true, null, 'POSTGITHUBCOMMENT', 'Read JSONs with iOS paths', [], [])); + } + steps.push( + utils.createStepAssertion( + 'maintain-comment', + true, + null, + 'POSTGITHUBCOMMENT', + 'maintain-comment', + [ + {key: 'token', value: '***'}, + {key: 'body-include', value: 'Use the links below to test this build in android and iOS. Happy testing!'}, + {key: 'number', value: pullRequestNumber}, + {key: 'delete', value: true}, + ], + [], + ), + utils.createStepAssertion( + 'Publish links to apps for download', + true, + null, + 'POSTGITHUBCOMMENT', + 'Publish links to apps for download', + [ + {key: 'PR_NUMBER', value: pullRequestNumber}, + {key: 'GITHUB_TOKEN', value: '***'}, + {key: 'ANDROID', value: androidStatus}, + {key: 'DESKTOP', value: desktopStatus}, + {key: 'IOS', value: iOSStatus}, + {key: 'WEB', value: webStatus}, + {key: 'ANDROID_LINK', value: androidStatus === 'success' ? 'http://dummy.android.link' : ''}, + {key: 'DESKTOP_LINK', value: `https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/${pullRequestNumber}/NewExpensify.dmg`}, + {key: 'IOS_LINK', value: iOSStatus === 'success' ? 'http://dummy.ios.link' : ''}, + {key: 'WEB_LINK', value: `https://${pullRequestNumber}.pr-testing.expensify.com`}, + ], + [], + ), + ); + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertValidateActorJobExecuted, + assertGetBranchRefJobExecuted, + assertAndroidJobExecuted, + assertIOSJobExecuted, + assertDesktopJobExecuted, + assertWebJobExecuted, + assertPostGithubCommentJobExecuted, +}; diff --git a/workflow_tests/assertions/validateGithubActionsAssertions.js b/workflow_tests/assertions/validateGithubActionsAssertions.js new file mode 100644 index 000000000000..fb5f58d2b5ed --- /dev/null +++ b/workflow_tests/assertions/validateGithubActionsAssertions.js @@ -0,0 +1,22 @@ +const utils = require('../utils/utils'); + +const assertVerifyJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'VERIFY', 'Checkout'), + utils.createStepAssertion('Setup Node', true, null, 'VERIFY', 'Setup Node', [], []), + utils.createStepAssertion('Verify Javascript Action Builds', true, null, 'VERIFY', 'Verify Javascript Action Builds', [], []), + utils.createStepAssertion('Validate actions and workflows', true, null, 'VERIFY', 'Validate actions and workflows', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertVerifyJobExecuted, +}; diff --git a/workflow_tests/assertions/verifyPodfileAssertions.js b/workflow_tests/assertions/verifyPodfileAssertions.js new file mode 100644 index 000000000000..e0cc50570fec --- /dev/null +++ b/workflow_tests/assertions/verifyPodfileAssertions.js @@ -0,0 +1,21 @@ +const utils = require('../utils/utils'); + +const assertVerifyJobExecuted = (workflowResult, didExecute = true) => { + const steps = [ + utils.createStepAssertion('Checkout', true, null, 'VERIFY', 'Checkout'), + utils.createStepAssertion('Setup Node', true, null, 'VERIFY', 'Setup Node', [], []), + utils.createStepAssertion('Verify podfile', true, null, 'VERIFY', 'Verify podfile', [], []), + ]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertVerifyJobExecuted, +}; diff --git a/workflow_tests/assertions/verifySignedCommitsAssertions.js b/workflow_tests/assertions/verifySignedCommitsAssertions.js new file mode 100644 index 000000000000..458bc684363f --- /dev/null +++ b/workflow_tests/assertions/verifySignedCommitsAssertions.js @@ -0,0 +1,17 @@ +const utils = require('../utils/utils'); + +const assertVerifySignedCommitsJobExecuted = (workflowResult, didExecute = true) => { + const steps = [utils.createStepAssertion('Verify signed commits', true, null, 'VERIFYSIGNEDCOMMITS', 'Verify signed commits', [{key: 'GITHUB_TOKEN', value: '***'}], [])]; + + steps.forEach((expectedStep) => { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + }); +}; + +module.exports = { + assertVerifySignedCommitsJobExecuted, +}; diff --git a/workflow_tests/authorChecklist.test.js b/workflow_tests/authorChecklist.test.js new file mode 100644 index 000000000000..64a25c8fe1b8 --- /dev/null +++ b/workflow_tests/authorChecklist.test.js @@ -0,0 +1,181 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/authorChecklistAssertions'); +const mocks = require('./mocks/authorChecklistMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'authorChecklist.yml'), + dest: '.github/workflows/authorChecklist.yml', + }, +]; + +describe('test workflow authorChecklist', () => { + const githubToken = 'dummy_github_token'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testAuthorChecklistWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('pull request opened', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'opened', + }; + describe('actor is not OSBotify', () => { + const actor = 'Dummy Author'; + it('executes workflow', async () => { + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('authorChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result); + }); + }); + describe('actor is OSBotify', () => { + const actor = 'OSBotify'; + it('does not execute workflow', async () => { + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('authorChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result, false); + }); + }); + }); + describe('pull request edited', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'edited', + }; + describe('actor is not OSBotify', () => { + const actor = 'Dummy Author'; + it('executes workflow', async () => { + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('authorChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result); + }); + }); + describe('actor is OSBotify', () => { + const actor = 'OSBotify'; + it('does not execute workflow', async () => { + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('authorChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result, false); + }); + }); + }); + describe('pull request reopened', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'reopened', + }; + describe('actor is not OSBotify', () => { + const actor = 'Dummy Author'; + it('executes workflow', async () => { + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('authorChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result); + }); + }); + describe('actor is OSBotify', () => { + const actor = 'OSBotify'; + it('does not execute workflow', async () => { + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('authorChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result, false); + }); + }); + }); +}); diff --git a/workflow_tests/cherryPick.test.js b/workflow_tests/cherryPick.test.js new file mode 100644 index 000000000000..592170a7c64e --- /dev/null +++ b/workflow_tests/cherryPick.test.js @@ -0,0 +1,391 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/cherryPickAssertions'); +const mocks = require('./mocks/cherryPickMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'cherryPick.yml'), + dest: '.github/workflows/cherryPick.yml', + }, +]; + +describe('test workflow cherryPick', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testCherryPickWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('manual workflow dispatch', () => { + const event = 'workflow_dispatch'; + describe('actor is not deployer', () => { + const actor = 'Dummy Author'; + it('workflow ends after validate job', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, + cherryPick: mocks.getCherryPickMockSteps(true, false), + }; + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertCherryPickJobExecuted(result, actor, '1234', false); + }); + }); + describe('actor is OSBotify', () => { + const actor = 'OSBotify'; + const mergeConflicts = false; + const versionsMatch = true; + it('behaviour is the same as with actor being the deployer', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, + cherryPick: mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts), + }; + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertCherryPickJobExecuted(result, actor, '1234', true); + }); + }); + describe('actor is a deployer', () => { + const actor = 'Dummy Author'; + describe('no merge conflicts', () => { + const mergeConflicts = false; + describe('version match', () => { + const versionsMatch = true; + it('workflow executes, PR approved and merged automatically', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, + }; + testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertCherryPickJobExecuted(result, actor, '1234', true); + }); + }); + describe('version does not match', () => { + const versionsMatch = false; + it('workflow executes, PR auto-assigned and commented, approved and merged automatically', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, + }; + testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertCherryPickJobExecuted(result, actor, '1234', true); + }); + }); + }); + describe('merge conflicts', () => { + const mergeConflicts = true; + describe('version match', () => { + const versionsMatch = true; + it('workflow executes, PR auto-assigned and commented, not merged automatically', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, + }; + testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertCherryPickJobExecuted(result, actor, '1234', true, true); + }); + }); + describe('version does not match', () => { + const versionsMatch = false; + it('workflow executes, PR auto-assigned and commented, not merged automatically', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, + }; + testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertCherryPickJobExecuted(result, actor, '1234', true, true); + }); + }); + }); + }); + }); + describe('automatic trigger', () => { + const event = 'pull_request'; + it('workflow does not execute', async () => { + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + event, + null, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + null, + { + PULL_REQUEST_NUMBER: '1234', + }, + ); + const testMockSteps = { + validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, + cherryPick: mocks.getCherryPickMockSteps(true, false), + }; + const testMockJobs = { + createNewVersion: { + steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('cherryPick', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + assertions.assertValidateActorJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertCherryPickJobExecuted(result, 'Dummy Author', '1234', false); + }); + }); +}); diff --git a/workflow_tests/cla.test.js b/workflow_tests/cla.test.js new file mode 100644 index 000000000000..301aa7587f53 --- /dev/null +++ b/workflow_tests/cla.test.js @@ -0,0 +1,196 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/claAssertions'); +const mocks = require('./mocks/claMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'cla.yml'), + dest: '.github/workflows/cla.yml', + }, +]; + +describe('test workflow cla', () => { + const secrets = { + CLA_BOTIFY_TOKEN: 'dummy_cla_botify_token', + }; + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Author'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testClaWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('event is issue_comment', () => { + const event = 'issue_comment'; + describe('no regex matches', () => { + const commentBody = 'Comment body'; + const eventData = { + action: 'created', + issue: { + pull_request: { + number: 1234, + }, + }, + comment: { + body: commentBody, + }, + }; + it('workflow executes, CLA assistant step not run', async () => { + const repoPath = mockGithub.repo.getPath('testClaWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cla.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventData, secrets, githubToken); + const testMockSteps = { + CLA: mocks.CLA__CLA__NO_MATCHES__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cla.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cla', expect.getState().currentTestName), + }); + + assertions.assertCLAJobExecuted(result, commentBody, `${repoPath}/remote/origin`, true, false); + }); + }); + describe('check regex matches', () => { + const commentBody = 'I have read the CLA Document and I hereby sign the CLA'; + const eventData = { + action: 'created', + issue: { + pull_request: { + number: 1234, + }, + }, + comment: { + body: commentBody, + }, + }; + it('workflow executes, CLA assistant step run', async () => { + const repoPath = mockGithub.repo.getPath('testClaWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cla.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventData, secrets, githubToken); + const testMockSteps = { + CLA: mocks.CLA__CLA__CHECK_MATCH__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cla.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cla', expect.getState().currentTestName), + }); + + assertions.assertCLAJobExecuted(result, commentBody, `${repoPath}/remote/origin`, true, true); + }); + }); + describe('re-check regex matches', () => { + const commentBody = 'recheck'; + const eventData = { + action: 'created', + issue: { + pull_request: { + number: 1234, + }, + }, + comment: { + body: commentBody, + }, + }; + it('workflow executes, CLA assistant step run', async () => { + const repoPath = mockGithub.repo.getPath('testClaWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cla.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventData, secrets, githubToken); + const testMockSteps = { + CLA: mocks.CLA__CLA__RECHECK_MATCH__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cla.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cla', expect.getState().currentTestName), + }); + + assertions.assertCLAJobExecuted(result, commentBody, `${repoPath}/remote/origin`, true, true); + }); + }); + }); + describe('event is pull_request_target', () => { + const event = 'pull_request_target'; + describe("no regex matches - there's no comment", () => { + const eventData = { + action: 'opened', + issue: { + pull_request: { + number: 1234, + }, + }, + }; + it('workflow executes, CLA assistant step still run', async () => { + const repoPath = mockGithub.repo.getPath('testClaWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cla.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventData, secrets, githubToken); + const testMockSteps = { + CLA: mocks.CLA__CLA__NO_MATCHES__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cla.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cla', expect.getState().currentTestName), + }); + + assertions.assertCLAJobExecuted(result, '', `${repoPath}/remote/origin`, true, true); + }); + }); + }); + describe('different event', () => { + const event = 'push'; + it('workflow does not execute', async () => { + const eventData = { + ref: 'main', + }; + const repoPath = mockGithub.repo.getPath('testClaWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'cla.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventData, secrets, githubToken); + const testMockSteps = { + CLA: mocks.CLA__CLA__NO_MATCHES__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'cla.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('cla', expect.getState().currentTestName), + }); + + assertions.assertCLAJobExecuted(result, '', `${repoPath}/remote/origin`, false); + }); + }); +}); diff --git a/workflow_tests/createNewVersion.test.js b/workflow_tests/createNewVersion.test.js new file mode 100644 index 000000000000..259e06450325 --- /dev/null +++ b/workflow_tests/createNewVersion.test.js @@ -0,0 +1,166 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/createNewVersionAssertions'); +const mocks = require('./mocks/createNewVersionMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); // 90 sec +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'createNewVersion.yml'), + dest: '.github/workflows/createNewVersion.yml', + }, +]; + +describe('test workflow createNewVersion', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testCreateNewVersionWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + + describe('event is workflow_call', () => { + const event = 'workflow_call'; + const inputs = { + SEMVER_LEVEL: 'BUILD', + }; + const secrets = { + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + OS_BOTIFY_TOKEN: 'dummy_osbotify_token', + SLACK_WEBHOOK: 'dummy_webhook', + }; + const githubToken = 'dummy_github_token'; + + describe('actor is admin', () => { + const validateActorMockSteps = mocks.CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS; + it('executes full workflow', async () => { + const repoPath = mockGithub.repo.getPath('testCreateNewVersionWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {createNewVersion: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: validateActorMockSteps, + createNewVersion: mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('createNewVersion', expect.getState().currentTestName), + }); + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + }); + }); + + describe('actor is writer', () => { + const validateActorMockSteps = mocks.CREATENEWVERSION__VALIDATEACTOR__WRITER__STEP_MOCKS; + it('executes full workflow', async () => { + const repoPath = mockGithub.repo.getPath('testCreateNewVersionWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {createNewVersion: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: validateActorMockSteps, + createNewVersion: mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('createNewVersion', expect.getState().currentTestName), + }); + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result); + }); + }); + + describe('actor is reader', () => { + const validateActorMockSteps = mocks.CREATENEWVERSION__VALIDATEACTOR__NO_PERMISSION__STEP_MOCKS; + it('stops after validation', async () => { + const repoPath = mockGithub.repo.getPath('testCreateNewVersionWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {createNewVersion: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: validateActorMockSteps, + createNewVersion: mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('createNewVersion', expect.getState().currentTestName), + }); + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result, 'BUILD', false); + }); + }); + + describe('one step fails', () => { + it('announces failure on Slack', async () => { + const repoPath = mockGithub.repo.getPath('testCreateNewVersionWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {createNewVersion: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS, + createNewVersion: utils.deepCopy(mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS), + }; + testMockSteps.createNewVersion[5] = utils.createMockStep('Commit new version', 'Commit new version', 'CREATENEWVERSION', [], [], [], [], false); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('createNewVersion', expect.getState().currentTestName), + }); + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result, 'BUILD', true, false); + }); + }); + + it('chooses source branch depending on the SEMVER_LEVEL', async () => { + const repoPath = mockGithub.repo.getPath('testCreateNewVersionWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, {SEMVER_LEVEL: 'MAJOR'}); + act = utils.setJobRunners(act, {createNewVersion: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS, + createNewVersion: mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'createNewVersion.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('createNewVersion', expect.getState().currentTestName), + }); + assertions.assertValidateActorJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result, 'MAJOR'); + }); + }); +}); diff --git a/workflow_tests/deploy.test.js b/workflow_tests/deploy.test.js new file mode 100644 index 000000000000..a2ccdebc0b31 --- /dev/null +++ b/workflow_tests/deploy.test.js @@ -0,0 +1,183 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/deployAssertions'); +const mocks = require('./mocks/deployMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'deploy.yml'), + dest: '.github/workflows/deploy.yml', + }, +]; + +describe('test workflow deploy', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testDeployWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + pushedBranches: ['staging', 'production'], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('push', () => { + it('to main - nothing triggered', async () => { + const repoPath = mockGithub.repo.getPath('testDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + { + ref: 'refs/heads/main', + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + deployStaging: mocks.DEPLOY_STAGING_STEP_MOCKS, + deployProduction: mocks.DEPLOY_PRODUCTION_STEP_MOCKS, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('deploy', expect.getState().currentTestName), + }); + assertions.assertDeployStagingJobExecuted(result, false); + assertions.assertDeployProductionJobExecuted(result, false); + }); + + it('to staging - deployStaging triggered', async () => { + const repoPath = mockGithub.repo.getPath('testDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + { + ref: 'refs/heads/staging', + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + deployStaging: mocks.DEPLOY_STAGING_STEP_MOCKS, + deployProduction: mocks.DEPLOY_PRODUCTION_STEP_MOCKS, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('deploy', expect.getState().currentTestName), + }); + assertions.assertDeployStagingJobExecuted(result); + assertions.assertDeployProductionJobExecuted(result, false); + }); + + it('to production - deployProduction triggered', async () => { + const repoPath = mockGithub.repo.getPath('testDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + { + ref: 'refs/heads/production', + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + deployStaging: mocks.DEPLOY_STAGING_STEP_MOCKS, + deployProduction: mocks.DEPLOY_PRODUCTION_STEP_MOCKS, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('deploy', expect.getState().currentTestName), + }); + assertions.assertDeployStagingJobExecuted(result, false); + assertions.assertDeployProductionJobExecuted(result); + }); + }); + + it('different event than push - workflow does not execute', async () => { + const repoPath = mockGithub.repo.getPath('testDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + deployStaging: mocks.DEPLOY_STAGING_STEP_MOCKS, + deployProduction: mocks.DEPLOY_PRODUCTION_STEP_MOCKS, + }; + + // pull_request + act = utils.setUpActParams( + act, + 'pull_request', + {head: {ref: 'main'}}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + let result = await act.runEvent('pull_request', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('deploy', expect.getState().currentTestName), + }); + assertions.assertDeployStagingJobExecuted(result, false); + assertions.assertDeployProductionJobExecuted(result, false); + + // workflow_dispatch + act = utils.setUpActParams( + act, + 'workflow_dispatch', + {}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + result = await act.runEvent('workflow_dispatch', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('deploy', expect.getState().currentTestName), + }); + assertions.assertDeployStagingJobExecuted(result, false); + assertions.assertDeployProductionJobExecuted(result, false); + }); +}); diff --git a/workflow_tests/deployBlocker.test.js b/workflow_tests/deployBlocker.test.js new file mode 100644 index 000000000000..1cfca11c90f5 --- /dev/null +++ b/workflow_tests/deployBlocker.test.js @@ -0,0 +1,135 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/deployBlockerAssertions'); +const mocks = require('./mocks/deployBlockerMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'deployBlocker.yml'), + dest: '.github/workflows/deployBlocker.yml', + }, +]; + +describe('test workflow deployBlocker', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Author'; + const secrets = { + OS_BOTIFY_TOKEN: 'dummy_osbotify_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testDeployBlockerWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + + // if any branches besides main are need add: pushedBranches: ['staging', 'production'], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('issue labeled', () => { + const event = 'issues'; + const eventOptions = { + action: 'labeled', + label: { + name: 'DeployBlockerCash', + }, + issue: { + title: 'Labeled issue title', + number: '1234', + html_url: 'http://issue.html.url', + }, + }; + describe('label is DeployBlockerCash', () => { + const testEventOptions = utils.deepCopy(eventOptions); + testEventOptions.label = {name: 'DeployBlockerCash'}; + it('runs the workflow and announces success on Slack', async () => { + const repoPath = mockGithub.repo.getPath('testDeployBlockerWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, testEventOptions, secrets, githubToken, {}, {}); + const testMockSteps = { + deployBlocker: mocks.DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('deployBlocker', expect.getState().currentTestName), + }); + + assertions.assertDeployBlockerJobExecuted(result, 'Labeled issue title', '1234'); + }); + describe('one step fails', () => { + it('announces failure on Slack', async () => { + const repoPath = mockGithub.repo.getPath('testDeployBlockerWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, testEventOptions, secrets, githubToken, {}, {}); + const testMockSteps = { + deployBlocker: utils.deepCopy(mocks.DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS), + }; + testMockSteps.deployBlocker[2] = utils.createMockStep( + 'Update StagingDeployCash with new deploy blocker', + 'Update StagingDeployCash with new deploy blocker', + 'DEPLOYBLOCKER', + ['GITHUB_TOKEN'], + [], + {}, + {}, + false, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('deployBlocker', expect.getState().currentTestName), + }); + + assertions.assertDeployBlockerJobExecuted(result, 'Labeled issue title', '1234', true, false); + }); + }); + }); + describe('label is different', () => { + const testEventOptions = utils.deepCopy(eventOptions); + testEventOptions.label = {name: 'Different Label'}; + it('does not run workflow', async () => { + const repoPath = mockGithub.repo.getPath('testDeployBlockerWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, testEventOptions, secrets, githubToken, {}, {}); + const testMockSteps = { + deployBlocker: mocks.DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('deployBlocker', expect.getState().currentTestName), + }); + + assertions.assertDeployBlockerJobExecuted(result, '', '', false); + }); + }); + }); +}); diff --git a/workflow_tests/finishReleaseCycle.test.js b/workflow_tests/finishReleaseCycle.test.js new file mode 100644 index 000000000000..7c17ca8d4122 --- /dev/null +++ b/workflow_tests/finishReleaseCycle.test.js @@ -0,0 +1,247 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/finishReleaseCycleAssertions'); +const mocks = require('./mocks/finishReleaseCycleMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'finishReleaseCycle.yml'), + dest: '.github/workflows/finishReleaseCycle.yml', + }, +]; + +describe('test workflow finishReleaseCycle', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testFinishReleaseCycleWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('issue closed', () => { + describe('issue has StagingDeployCash', () => { + describe('actor is a team member', () => { + describe('no deploy blockers', () => { + it('production updated, new version created', async () => { + const repoPath = mockGithub.repo.getPath('testFinishReleaseCycleWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'closed', + type: 'closed', + issue: { + labels: [{name: 'StagingDeployCash'}], + number: '1234', + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + const testMockSteps = { + validate: mocks.FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, + updateProduction: mocks.FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS, + updateStaging: mocks.FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS, + }; + const testMockJobs = { + createNewPatchVersion: { + steps: mocks.FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('finishReleaseCycle', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertValidateJobExecuted(result, '1234'); + assertions.assertUpdateProductionJobExecuted(result); + assertions.assertCreateNewPatchVersionJobExecuted(result); + assertions.assertUpdateStagingJobExecuted(result); + }); + }); + describe('deploy blockers', () => { + it('production not updated, new version not created, issue reopened', async () => { + const repoPath = mockGithub.repo.getPath('testFinishReleaseCycleWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'closed', + type: 'closed', + issue: { + labels: [{name: 'StagingDeployCash'}], + number: '1234', + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + const testMockSteps = { + validate: mocks.FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS, + updateProduction: mocks.FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS, + updateStaging: mocks.FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS, + }; + const testMockJobs = { + createNewPatchVersion: { + steps: mocks.FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('finishReleaseCycle', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertValidateJobExecuted(result, '1234', true, true, true); + assertions.assertUpdateProductionJobExecuted(result, false); + assertions.assertCreateNewPatchVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + }); + }); + describe('actor is not a team member', () => { + it('production not updated, new version not created, issue reopened', async () => { + const repoPath = mockGithub.repo.getPath('testFinishReleaseCycleWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'closed', + type: 'closed', + issue: { + labels: [{name: 'StagingDeployCash'}], + number: '1234', + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + const testMockSteps = { + validate: mocks.FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, + updateProduction: mocks.FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS, + updateStaging: mocks.FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS, + }; + const testMockJobs = { + createNewPatchVersion: { + steps: mocks.FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('finishReleaseCycle', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertValidateJobExecuted(result, '1234', true, false, false); + assertions.assertUpdateProductionJobExecuted(result, false); + assertions.assertCreateNewPatchVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + }); + }); + describe('issue does not have StagingDeployCash', () => { + it('validate job not run', async () => { + const repoPath = mockGithub.repo.getPath('testFinishReleaseCycleWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'closed', + type: 'closed', + issue: { + labels: [{name: 'Some'}, {name: 'Other'}, {name: 'Labels'}], + number: '1234', + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + const testMockSteps = { + validate: mocks.FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, + updateProduction: mocks.FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS, + updateStaging: mocks.FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS, + }; + const testMockJobs = { + createNewPatchVersion: { + steps: mocks.FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'finishReleaseCycle.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('finishReleaseCycle', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertValidateJobExecuted(result, '1234', false); + assertions.assertUpdateProductionJobExecuted(result, false); + assertions.assertCreateNewPatchVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + }); + }); +}); diff --git a/workflow_tests/jest.config.js b/workflow_tests/jest.config.js new file mode 100644 index 000000000000..c8a4534764e3 --- /dev/null +++ b/workflow_tests/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + verbose: true, + transform: { + '^.+\\.jsx?$': 'babel-jest', + }, + clearMocks: true, + resetMocks: true, +}; diff --git a/workflow_tests/lint.test.js b/workflow_tests/lint.test.js new file mode 100644 index 000000000000..bc51f31b657c --- /dev/null +++ b/workflow_tests/lint.test.js @@ -0,0 +1,154 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/lintAssertions'); +const mocks = require('./mocks/lintMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'lint.yml'), + dest: '.github/workflows/lint.yml', + }, +]; + +describe('test workflow lint', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testLintWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + + // if any branches besides main are need add: pushedBranches: ['staging', 'production'], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('event is workflow_call', () => { + const event = 'workflow_call'; + const eventOptions = {}; + it('runs the lint', async () => { + const repoPath = mockGithub.repo.getPath('testLintWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lint.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + lint: mocks.LINT__LINT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lint.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('lint', expect.getState().currentTestName), + }); + + assertions.assertLintJobExecuted(result); + }); + describe('actor is OSBotify', () => { + const testActor = 'OSBotify'; + it('runs the lint', async () => { + const repoPath = mockGithub.repo.getPath('testLintWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lint.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + lint: mocks.LINT__LINT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lint.yml'), + mockSteps: testMockSteps, + actor: testActor, + logFile: utils.getLogFilePath('lint', expect.getState().currentTestName), + }); + + assertions.assertLintJobExecuted(result); + }); + }); + }); + describe('event is pull_request', () => { + const event = 'pull_request'; + describe('pull_request is opened', () => { + const eventOptions = { + action: 'opened', + }; + it('runs the lint', async () => { + const repoPath = mockGithub.repo.getPath('testLintWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lint.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + lint: mocks.LINT__LINT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lint.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('lint', expect.getState().currentTestName), + }); + + assertions.assertLintJobExecuted(result); + }); + describe('actor is OSBotify', () => { + const testActor = 'OSBotify'; + it('does not run the lint', async () => { + const repoPath = mockGithub.repo.getPath('testLintWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lint.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + lint: mocks.LINT__LINT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lint.yml'), + mockSteps: testMockSteps, + actor: testActor, + logFile: utils.getLogFilePath('lint', expect.getState().currentTestName), + }); + + assertions.assertLintJobExecuted(result, false); + }); + }); + }); + describe('pull_request is synchronized', () => { + const eventOptions = { + action: 'synchronize', + }; + it('runs the lint', async () => { + const repoPath = mockGithub.repo.getPath('testLintWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lint.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + lint: mocks.LINT__LINT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lint.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('lint', expect.getState().currentTestName), + }); + + assertions.assertLintJobExecuted(result); + }); + }); + }); +}); diff --git a/workflow_tests/lockDeploys.test.js b/workflow_tests/lockDeploys.test.js new file mode 100644 index 000000000000..a57ed8847fd3 --- /dev/null +++ b/workflow_tests/lockDeploys.test.js @@ -0,0 +1,458 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/lockDeploysAssertions'); +const mocks = require('./mocks/lockDeploysMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'lockDeploys.yml'), + dest: '.github/workflows/lockDeploys.yml', + }, +]; + +describe('test workflow lockDeploys', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testLockDeploysWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('issue labeled', () => { + describe('label is LockCashDeploys', () => { + describe('issue has StagingDeployCash', () => { + describe('actor is not OSBotify', () => { + it('job triggered, comment left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: '🔐 LockCashDeploys 🔐', + }, + issue: { + labels: [{name: 'StagingDeployCash'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result); + }); + + it('one step fails, comment not left in StagingDeployCash, announced failure in Slack', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: '🔐 LockCashDeploys 🔐', + }, + issue: { + labels: [{name: 'StagingDeployCash'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + testMockSteps.lockStagingDeploys[1] = utils.createMockStep( + 'Wait for staging deploys to finish', + 'Waiting for staging deploys to finish', + 'LOCKSTAGINGDEPLOYS', + ['GITHUB_TOKEN'], + [], + null, + null, + false, + ); + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobFailedAfterFirstStep(result); + }); + }); + + describe('actor is OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: '🔐 LockCashDeploys 🔐', + }, + issue: { + labels: [{name: 'StagingDeployCash'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + }); + + describe('issue does not have StagingDeployCash', () => { + describe('actor is not OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: '🔐 LockCashDeploys 🔐', + }, + issue: { + labels: [{name: 'Some'}, {name: 'Other'}, {name: 'Labels'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + + describe('actor is OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: '🔐 LockCashDeploys 🔐', + }, + issue: { + labels: [{name: 'Some'}, {name: 'Other'}, {name: 'Labels'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + }); + }); + + describe('label is not LockCashDeploys', () => { + describe('issue has StagingDeployCash', () => { + describe('actor is not OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: 'Some different label', + }, + issue: { + labels: [{name: 'StagingDeployCash'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + + describe('actor is OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: 'Some different label', + }, + issue: { + labels: [{name: 'StagingDeployCash'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + }); + + describe('issue does not have StagingDeployCash', () => { + describe('actor is not OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: 'Some other label', + }, + issue: { + labels: [{name: 'Some'}, {name: 'Other'}, {name: 'Labels'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + + describe('actor is OSBotify', () => { + it('job not triggered, comment not left in StagingDeployCash', async () => { + const repoPath = mockGithub.repo.getPath('testLockDeploysWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'issues', + { + action: 'labeled', + type: 'labeled', + label: { + name: 'Some other label', + }, + issue: { + labels: [{name: 'Some'}, {name: 'Other'}, {name: 'Labels'}], + }, + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + ); + act = utils.setJobRunners( + act, + { + lockStagingDeploys: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + lockStagingDeploys: mocks.LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, + }; + const result = await act.runEvent('issues', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'lockDeploys.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('lockDeploys', expect.getState().currentTestName), + }); + + assertions.assertlockStagingDeploysJobExecuted(result, false); + }); + }); + }); + }); + }); +}); diff --git a/workflow_tests/mocks/authorChecklistMocks.js b/workflow_tests/mocks/authorChecklistMocks.js new file mode 100644 index 000000000000..db211f5ec1e6 --- /dev/null +++ b/workflow_tests/mocks/authorChecklistMocks.js @@ -0,0 +1,9 @@ +const utils = require('../utils/utils'); + +// checklist +const AUTHORCHECKLIST__CHECKLIST__AUTHORCHECKLIST_JS__STEP_MOCK = utils.createMockStep('authorChecklist.js', 'Running authorChecklist.js', 'CHECKLIST', ['GITHUB_TOKEN'], []); +const AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS = [AUTHORCHECKLIST__CHECKLIST__AUTHORCHECKLIST_JS__STEP_MOCK]; + +module.exports = { + AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/cherryPickMocks.js b/workflow_tests/mocks/cherryPickMocks.js new file mode 100644 index 000000000000..778e6fd48ded --- /dev/null +++ b/workflow_tests/mocks/cherryPickMocks.js @@ -0,0 +1,114 @@ +const utils = require('../utils/utils'); + +// validateactor +const CHERRYPICK__VALIDATEACTOR__CHECK_IF_USER_IS_DEPLOYER_TRUE__STEP_MOCK = utils.createMockStep( + 'Check if user is deployer', + 'Checking if user is a deployer', + 'VALIDATEACTOR', + [], + ['GITHUB_TOKEN'], + {IS_DEPLOYER: true}, +); +const CHERRYPICK__VALIDATEACTOR__CHECK_IF_USER_IS_DEPLOYER_FALSE__STEP_MOCK = utils.createMockStep( + 'Check if user is deployer', + 'Checking if user is a deployer', + 'VALIDATEACTOR', + [], + ['GITHUB_TOKEN'], + {IS_DEPLOYER: false}, +); +const CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS = [CHERRYPICK__VALIDATEACTOR__CHECK_IF_USER_IS_DEPLOYER_TRUE__STEP_MOCK]; +const CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS = [CHERRYPICK__VALIDATEACTOR__CHECK_IF_USER_IS_DEPLOYER_FALSE__STEP_MOCK]; + +// createnewversion +const CHERRYPICK__CREATENEWVERSION__CREATE_NEW_VERSION__STEP_MOCK = utils.createMockStep( + 'Create new version', + 'Creating new version', + 'CREATENEWVERSION', + [], + [], + {NEW_VERSION: '1.2.3'}, + null, + true, + 'createNewVersion', +); +const CHERRYPICK__CREATENEWVERSION__STEP_MOCKS = [CHERRYPICK__CREATENEWVERSION__CREATE_NEW_VERSION__STEP_MOCK]; + +// cherrypick +const CHERRYPICK__CHERRYPICK__CHECKOUT_STAGING_BRANCH__STEP_MOCK = utils.createMockStep('Checkout staging branch', 'Checking out staging branch', 'CHERRYPICK', ['ref', 'token'], []); +const CHERRYPICK__CHERRYPICK__SET_UP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep('Set up git for OSBotify', 'Setting up git for OSBotify', 'CHERRYPICK', ['GPG_PASSPHRASE'], []); +const CHERRYPICK__CHERRYPICK__GET_PREVIOUS_APP_VERSION__STEP_MOCK = utils.createMockStep('Get previous app version', 'Get previous app version', 'CHERRYPICK', ['SEMVER_LEVEL']); +const CHERRYPICK__CHERRYPICK__FETCH_HISTORY_OF_RELEVANT_REFS__STEP_MOCK = utils.createMockStep('Fetch history of relevant refs', 'Fetch history of relevant refs', 'CHERRYPICK'); +const CHERRYPICK__CHERRYPICK__GET_VERSION_BUMP_COMMIT__STEP_MOCK = utils.createMockStep('Get version bump commit', 'Get version bump commit', 'CHERRYPICK', [], [], { + VERSION_BUMP_SHA: 'version_bump_sha', +}); +const CHERRYPICK__CHERRYPICK__GET_MERGE_COMMIT_FOR_PULL_REQUEST_TO_CP__STEP_MOCK = utils.createMockStep( + 'Get merge commit for pull request to CP', + 'Get merge commit for pull request to CP', + 'CHERRYPICK', + ['GITHUB_TOKEN', 'USER', 'PULL_REQUEST_NUMBER'], + [], + {MERGE_ACTOR: '@dummyauthor'}, +); +const CHERRYPICK__CHERRYPICK__CHERRY_PICK_THE_VERSION_BUMP_TO_STAGING__STEP_MOCK = utils.createMockStep( + 'Cherry-pick the version-bump to staging', + 'Cherry-picking the version-bump to staging', + 'CHERRYPICK', + [], + [], +); +const CHERRYPICK__CHERRYPICK__CHERRY_PICK_THE_MERGE_COMMIT_OF_TARGET_PR_TO_NEW_BRANCH__HAS_NO_CONFLICTS__STEP_MOCK = utils.createMockStep( + 'Cherry-pick the merge commit of target PR', + 'Cherry-picking the merge commit of target PR', + 'CHERRYPICK', + [], + [], + {HAS_CONFLICTS: false}, +); +// eslint-disable-next-line rulesdir/no-negated-variables +const CHERRYPICK__CHERRYPICK__CHERRY_PICK_THE_MERGE_COMMIT_OF_TARGET_PR_TO_NEW_BRANCH__HAS_CONFLICTS__STEP_MOCK = utils.createMockStep( + 'Cherry-pick the merge commit of target PR', + 'Cherry-picking the merge commit of target PR', + 'CHERRYPICK', + [], + [], + {HAS_CONFLICTS: true}, +); +const CHERRYPICK__CHERRYPICK__PUSH_CHANGES__STEP_MOCK = utils.createMockStep('Push changes', 'Pushing changes', 'CHERRYPICK', [], []); +const CHERRYPICK__CHERRYPICK__CREATE_PULL_REQUEST_TO_MANUALLY_FINISH_CP__STEP_MOCK = utils.createMockStep( + 'Create Pull Request to manually finish CP', + 'Creating Pull Request to manually finish CP', + 'CHERRYPICK', + [], + ['GITHUB_TOKEN'], +); +const CHERRYPICK__CHERRYPICK__ANNOUNCES_A_CP_FAILURE_IN_THE_ANNOUNCE_SLACK_ROOM__STEP_MOCK = utils.createMockStep( + 'Announces a CP failure in the #announce Slack room', + 'Announcing a CP failure', + 'CHERRYPICK', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); + +const getCherryPickMockSteps = (upToDate, hasConflicts) => [ + CHERRYPICK__CHERRYPICK__CHECKOUT_STAGING_BRANCH__STEP_MOCK, + CHERRYPICK__CHERRYPICK__SET_UP_GIT_FOR_OSBOTIFY__STEP_MOCK, + CHERRYPICK__CHERRYPICK__GET_PREVIOUS_APP_VERSION__STEP_MOCK, + CHERRYPICK__CHERRYPICK__FETCH_HISTORY_OF_RELEVANT_REFS__STEP_MOCK, + CHERRYPICK__CHERRYPICK__GET_VERSION_BUMP_COMMIT__STEP_MOCK, + CHERRYPICK__CHERRYPICK__GET_MERGE_COMMIT_FOR_PULL_REQUEST_TO_CP__STEP_MOCK, + CHERRYPICK__CHERRYPICK__CHERRY_PICK_THE_VERSION_BUMP_TO_STAGING__STEP_MOCK, + hasConflicts + ? CHERRYPICK__CHERRYPICK__CHERRY_PICK_THE_MERGE_COMMIT_OF_TARGET_PR_TO_NEW_BRANCH__HAS_CONFLICTS__STEP_MOCK + : CHERRYPICK__CHERRYPICK__CHERRY_PICK_THE_MERGE_COMMIT_OF_TARGET_PR_TO_NEW_BRANCH__HAS_NO_CONFLICTS__STEP_MOCK, + CHERRYPICK__CHERRYPICK__PUSH_CHANGES__STEP_MOCK, + CHERRYPICK__CHERRYPICK__CREATE_PULL_REQUEST_TO_MANUALLY_FINISH_CP__STEP_MOCK, + CHERRYPICK__CHERRYPICK__ANNOUNCES_A_CP_FAILURE_IN_THE_ANNOUNCE_SLACK_ROOM__STEP_MOCK, +]; + +module.exports = { + CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, + CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, + CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, + getCherryPickMockSteps, +}; diff --git a/workflow_tests/mocks/claMocks.js b/workflow_tests/mocks/claMocks.js new file mode 100644 index 000000000000..d0a6793b93e6 --- /dev/null +++ b/workflow_tests/mocks/claMocks.js @@ -0,0 +1,25 @@ +const utils = require('../utils/utils'); + +// cla +const CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK = utils.createMockStep('CLA comment check', 'CLA comment check', 'CLA', ['text', 'regex'], [], {match: ''}); +const CLA__CLA__CLA_COMMENT_CHECK__MATCH__STEP_MOCK = utils.createMockStep('CLA comment check', 'CLA comment check', 'CLA', ['text', 'regex'], [], { + match: 'I have read the CLA Document and I hereby sign the CLA', +}); +const CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK = utils.createMockStep('CLA comment re-check', 'CLA comment re-check', 'CLA', ['text', 'regex'], [], {match: ''}); +const CLA__CLA__CLA_COMMENT_RE_CHECK__MATCH__STEP_MOCK = utils.createMockStep('CLA comment re-check', 'CLA comment re-check', 'CLA', ['text', 'regex'], [], {match: 'recheck'}); +const CLA__CLA__CLA_ASSISTANT__STEP_MOCK = utils.createMockStep( + 'CLA Assistant', + 'CLA Assistant', + 'CLA', + ['path-to-signatures', 'path-to-document', 'branch', 'remote-organization-name', 'remote-repository-name', 'lock-pullrequest-aftermerge', 'allowlist'], + ['GITHUB_TOKEN', 'PERSONAL_ACCESS_TOKEN'], +); +const CLA__CLA__NO_MATCHES__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK]; +const CLA__CLA__CHECK_MATCH__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK]; +const CLA__CLA__RECHECK_MATCH__STEP_MOCKS = [CLA__CLA__CLA_COMMENT_CHECK__NO_MATCH__STEP_MOCK, CLA__CLA__CLA_COMMENT_RE_CHECK__MATCH__STEP_MOCK, CLA__CLA__CLA_ASSISTANT__STEP_MOCK]; + +module.exports = { + CLA__CLA__NO_MATCHES__STEP_MOCKS, + CLA__CLA__CHECK_MATCH__STEP_MOCKS, + CLA__CLA__RECHECK_MATCH__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/createNewVersionMocks.js b/workflow_tests/mocks/createNewVersionMocks.js new file mode 100644 index 000000000000..a1f601aef47f --- /dev/null +++ b/workflow_tests/mocks/createNewVersionMocks.js @@ -0,0 +1,58 @@ +const utils = require('../utils/utils'); + +// validateactor +const CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__ADMIN__STEP_MOCK = utils.createMockStep('Get user permissions', 'Get user permissions', 'VALIDATEACTOR', [], ['GITHUB_TOKEN'], { + PERMISSION: 'admin', +}); +const CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__WRITE__STEP_MOCK = utils.createMockStep('Get user permissions', 'Get user permissions', 'VALIDATEACTOR', [], ['GITHUB_TOKEN'], { + PERMISSION: 'write', +}); +const CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__NONE__STEP_MOCK = utils.createMockStep('Get user permissions', 'Get user permissions', 'VALIDATEACTOR', [], ['GITHUB_TOKEN'], { + PERMISSION: 'read', +}); +const CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__ADMIN__STEP_MOCK]; +const CREATENEWVERSION__VALIDATEACTOR__WRITER__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__WRITE__STEP_MOCK]; +const CREATENEWVERSION__VALIDATEACTOR__NO_PERMISSION__STEP_MOCKS = [CREATENEWVERSION__VALIDATEACTOR__GET_USER_PERMISSIONS__NONE__STEP_MOCK]; + +// createnewversion +const CREATENEWVERSION__CREATENEWVERSION__RUN_TURNSTYLE__STEP_MOCK = utils.createMockStep('Run turnstyle', 'Run turnstyle', 'CREATENEWVERSION', ['poll-interval-seconds'], ['GITHUB_TOKEN']); +const CREATENEWVERSION__CREATENEWVERSION__CHECK_OUT__STEP_MOCK = utils.createMockStep('Check out', 'Check out', 'CREATENEWVERSION', ['ref', 'token'], []); +const CREATENEWVERSION__CREATENEWVERSION__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep( + 'Setup git for OSBotify', + 'Setup git for OSBotify', + 'CREATENEWVERSION', + ['GPG_PASSPHRASE'], + [], +); +const CREATENEWVERSION__CREATENEWVERSION__GENERATE_VERSION__STEP_MOCK = utils.createMockStep( + 'Generate version', + 'Generate version', + 'CREATENEWVERSION', + ['GITHUB_TOKEN', 'SEMVER_LEVEL'], + [], +); +const CREATENEWVERSION__CREATENEWVERSION__COMMIT_NEW_VERSION__STEP_MOCK = utils.createMockStep('Commit new version', 'Commit new version', 'CREATENEWVERSION', [], []); +const CREATENEWVERSION__CREATENEWVERSION__UPDATE_MAIN_BRANCH__STEP_MOCK = utils.createMockStep('Update main branch', 'Update main branch', 'CREATENEWVERSION', [], []); +const CREATENEWVERSION__CREATENEWVERSION__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK = utils.createMockStep( + 'Announce failed workflow in Slack', + 'Announce failed workflow in Slack', + 'CREATENEWVERSION', + ['SLACK_WEBHOOK'], + [], +); +const CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS = [ + CREATENEWVERSION__CREATENEWVERSION__RUN_TURNSTYLE__STEP_MOCK, + CREATENEWVERSION__CREATENEWVERSION__CHECK_OUT__STEP_MOCK, + CREATENEWVERSION__CREATENEWVERSION__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, + CREATENEWVERSION__CREATENEWVERSION__GENERATE_VERSION__STEP_MOCK, + CREATENEWVERSION__CREATENEWVERSION__COMMIT_NEW_VERSION__STEP_MOCK, + CREATENEWVERSION__CREATENEWVERSION__UPDATE_MAIN_BRANCH__STEP_MOCK, + CREATENEWVERSION__CREATENEWVERSION__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; + +module.exports = { + CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS, + CREATENEWVERSION__VALIDATEACTOR__WRITER__STEP_MOCKS, + CREATENEWVERSION__VALIDATEACTOR__NO_PERMISSION__STEP_MOCKS, + CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/deployBlockerMocks.js b/workflow_tests/mocks/deployBlockerMocks.js new file mode 100644 index 000000000000..fbfc676f7701 --- /dev/null +++ b/workflow_tests/mocks/deployBlockerMocks.js @@ -0,0 +1,68 @@ +const utils = require('../utils/utils'); + +// deployblocker +const DEPLOYBLOCKER__DEPLOYBLOCKER__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'DEPLOYBLOCKER', ['token'], []); +const DEPLOYBLOCKER__DEPLOYBLOCKER__GET_URL_TITLE_AND_NUMBER_OF_NEW_DEPLOY_BLOCKER_ISSUE__STEP_MOCK = utils.createMockStep( + 'Get URL, title, & number of new deploy blocker (issue)', + 'Get URL, title and number of new deploy blocker - issue', + 'DEPLOYBLOCKER', + [], + ['TITLE'], + {}, + { + // eslint-disable-next-line no-template-curly-in-string + DEPLOY_BLOCKER_URL: '${{ github.event.issue.html_url }}', + // eslint-disable-next-line no-template-curly-in-string + DEPLOY_BLOCKER_NUMBER: '${{ github.event.issue.number }}', + // eslint-disable-next-line no-template-curly-in-string + DEPLOY_BLOCKER_TITLE: '${{ github.event.issue.title }}', + }, +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__UPDATE_STAGINGDEPLOYCASH_WITH_NEW_DEPLOY_BLOCKER__STEP_MOCK = utils.createMockStep( + 'Update StagingDeployCash with new deploy blocker', + 'Update StagingDeployCash with new deploy blocker', + 'DEPLOYBLOCKER', + ['GITHUB_TOKEN'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__GIVE_THE_ISSUE_OR_PR_THE_HOURLY_ENGINEERING_LABELS__STEP_MOCK = utils.createMockStep( + 'Give the issue/PR the Hourly, Engineering labels', + 'Give the issue/PR the Hourly, Engineering labels', + 'DEPLOYBLOCKER', + ['add-labels', 'remove-labels'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__POST_THE_ISSUE_IN_THE_EXPENSIFY_OPEN_SOURCE_SLACK_ROOM__STEP_MOCK = utils.createMockStep( + 'Post the issue in the #expensify-open-source slack room', + 'Post the issue in the expensify-open-source slack room', + 'DEPLOYBLOCKER', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__COMMENT_ON_DEFERRED_PR__STEP_MOCK = utils.createMockStep( + 'Comment on deferred PR', + 'Comment on deferred PR', + 'DEPLOYBLOCKER', + ['github_token', 'number'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK = utils.createMockStep( + 'Announce failed workflow in Slack', + 'Announce failed workflow in Slack', + 'DEPLOYBLOCKER', + ['SLACK_WEBHOOK'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS = [ + DEPLOYBLOCKER__DEPLOYBLOCKER__CHECKOUT__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__GET_URL_TITLE_AND_NUMBER_OF_NEW_DEPLOY_BLOCKER_ISSUE__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__UPDATE_STAGINGDEPLOYCASH_WITH_NEW_DEPLOY_BLOCKER__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__GIVE_THE_ISSUE_OR_PR_THE_HOURLY_ENGINEERING_LABELS__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__POST_THE_ISSUE_IN_THE_EXPENSIFY_OPEN_SOURCE_SLACK_ROOM__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__COMMENT_ON_DEFERRED_PR__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; + +module.exports = { + DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/deployMocks.js b/workflow_tests/mocks/deployMocks.js new file mode 100644 index 000000000000..dfec48ca7dc3 --- /dev/null +++ b/workflow_tests/mocks/deployMocks.js @@ -0,0 +1,44 @@ +const utils = require('../utils/utils'); + +const DEPLOY_STAGING__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout staging branch', 'Checking out staging branch', 'DEPLOY_STAGING', ['ref', 'token']); +const DEPLOY_STAGING__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_STAGING', ['GPG_PASSPHRASE']); +const DEPLOY_STAGING__TAG_VERSION__STEP_MOCK = utils.createMockStep('Tag version', 'Tagging new version', 'DEPLOY_STAGING'); +const DEPLOY_STAGING__PUSH_TAG__STEP_MOCK = utils.createMockStep('🚀 Push tags to trigger staging deploy 🚀', 'Pushing tag to trigger staging deploy', 'DEPLOY_STAGING'); +const DEPLOY_STAGING_STEP_MOCKS = [DEPLOY_STAGING__CHECKOUT__STEP_MOCK, DEPLOY_STAGING__SETUP_GIT__STEP_MOCK, DEPLOY_STAGING__TAG_VERSION__STEP_MOCK, DEPLOY_STAGING__PUSH_TAG__STEP_MOCK]; + +const DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'DEPLOY_PRODUCTION', ['ref', 'token']); +const DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK = utils.createMockStep('Setup git for OSBotify', 'Setting up git for OSBotify', 'DEPLOY_PRODUCTION', ['GPG_PASSPHRASE']); +const DEPLOY_PRODUCTION__CURRENT_APP_VERSION__STEP_MOCK = utils.createMockStep('Get current app version', 'Getting current app version', 'DEPLOY_PRODUCTION', null, null, null, { + PRODUCTION_VERSION: '1.2.3', +}); +const DEPLOY_PRODUCTION__RELEASE_PR_LIST__STEP_MOCK = utils.createMockStep( + 'Get Release Pull Request List', + 'Getting release PR list', + 'DEPLOY_PRODUCTION', + ['TAG', 'GITHUB_TOKEN', 'IS_PRODUCTION_DEPLOY'], + null, + {PR_LIST: '["1.2.1", "1.2.2"]'}, +); +const DEPLOY_PRODUCTION__GENERATE_RELEASE_BODY__STEP_MOCK = utils.createMockStep('Generate Release Body', 'Generating release body', 'DEPLOY_PRODUCTION', ['PR_LIST'], null, { + RELEASE_BODY: 'Release body', +}); +const DEPLOY_PRODUCTION__CREATE_RELEASE__STEP_MOCK = utils.createMockStep( + '🚀 Create release to trigger production deploy 🚀', + 'Creating release to trigger production deploy', + 'DEPLOY_PRODUCTION', + ['tag_name', 'body'], + ['GITHUB_TOKEN'], +); +const DEPLOY_PRODUCTION_STEP_MOCKS = [ + DEPLOY_PRODUCTION__CHECKOUT__STEP_MOCK, + DEPLOY_PRODUCTION__SETUP_GIT__STEP_MOCK, + DEPLOY_PRODUCTION__CURRENT_APP_VERSION__STEP_MOCK, + DEPLOY_PRODUCTION__RELEASE_PR_LIST__STEP_MOCK, + DEPLOY_PRODUCTION__GENERATE_RELEASE_BODY__STEP_MOCK, + DEPLOY_PRODUCTION__CREATE_RELEASE__STEP_MOCK, +]; + +module.exports = { + DEPLOY_STAGING_STEP_MOCKS, + DEPLOY_PRODUCTION_STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/finishReleaseCycleMocks.js b/workflow_tests/mocks/finishReleaseCycleMocks.js new file mode 100644 index 000000000000..e1bb0d112429 --- /dev/null +++ b/workflow_tests/mocks/finishReleaseCycleMocks.js @@ -0,0 +1,165 @@ +const utils = require('../utils/utils'); + +// validate +const FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_TRUE__STEP_MOCK = utils.createMockStep( + 'Validate actor is deployer', + 'Validating if actor is deployer', + 'VALIDATE', + [], + ['GITHUB_TOKEN'], + {IS_DEPLOYER: true}, +); +const FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_FALSE__STEP_MOCK = utils.createMockStep( + 'Validate actor is deployer', + 'Validating if actor is deployer', + 'VALIDATE', + [], + ['GITHUB_TOKEN'], + {IS_DEPLOYER: false}, +); +// eslint-disable-next-line rulesdir/no-negated-variables +const FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK = utils.createMockStep( + 'Reopen and comment on issue (not a team member)', + 'Reopening issue - not a team member', + 'VALIDATE', + ['GITHUB_TOKEN', 'ISSUE_NUMBER', 'COMMENT'], + [], +); +const FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK = utils.createMockStep( + 'Check for any deploy blockers', + 'Checking for deploy blockers', + 'VALIDATE', + ['GITHUB_TOKEN', 'ISSUE_NUMBER'], + [], + {HAS_DEPLOY_BLOCKERS: false}, +); +const FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK = utils.createMockStep( + 'Check for any deploy blockers', + 'Checking for deploy blockers', + 'VALIDATE', + ['GITHUB_TOKEN', 'ISSUE_NUMBER'], + [], + {HAS_DEPLOY_BLOCKERS: true}, +); +const FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK = utils.createMockStep( + 'Reopen and comment on issue (has blockers)', + 'Reopening issue - blockers', + 'VALIDATE', + ['GITHUB_TOKEN', 'ISSUE_NUMBER'], + [], +); +const FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK = utils.createMockStep( + 'Announce failed workflow in Slack', + 'Announce failed workflow in Slack', + 'VALIDATE', + ['SLACK_WEBHOOK'], + [], +); +const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_TRUE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; +const FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_TRUE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; +// eslint-disable-next-line rulesdir/no-negated-variables +const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_FALSE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_FALSE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; +// eslint-disable-next-line rulesdir/no-negated-variables +const FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_BLOCKERS__STEP_MOCKS = [ + FINISHRELEASECYCLE__VALIDATE__VALIDATE_ACTOR_IS_DEPLOYER_FALSE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_NOT_A_TEAM_MEMBER__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__CHECK_FOR_ANY_DEPLOY_BLOCKERS_TRUE__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__REOPEN_AND_COMMENT_ON_ISSUE_HAS_BLOCKERS__STEP_MOCK, + FINISHRELEASECYCLE__VALIDATE__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; + +// updateproduction +const FINISHRELEASECYCLE__UPDATEPRODUCTION__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'UPDATEPRODUCTION', ['ref', 'token'], []); +const FINISHRELEASECYCLE__UPDATEPRODUCTION__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep( + 'Setup Git for OSBotify', + 'Setup Git for OSBotify', + 'UPDATEPRODUCTION', + ['GPG_PASSPHRASE'], + [], +); +const FINISHRELEASECYCLE__UPDATEPRODUCTION__UPDATE_PRODUCTION_BRANCH__STEP_MOCK = utils.createMockStep('Update production branch', 'Updating production branch', 'UPDATEPRODUCTION', [], []); +const FINISHRELEASECYCLE__UPDATEPRODUCTION__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK = utils.createMockStep( + 'Announce failed workflow in Slack', + 'Announce failed workflow in Slack', + 'UPDATEPRODUCTION', + ['SLACK_WEBHOOK'], + [], +); +const FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS = [ + FINISHRELEASECYCLE__UPDATEPRODUCTION__CHECKOUT__STEP_MOCK, + FINISHRELEASECYCLE__UPDATEPRODUCTION__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, + FINISHRELEASECYCLE__UPDATEPRODUCTION__UPDATE_PRODUCTION_BRANCH__STEP_MOCK, + FINISHRELEASECYCLE__UPDATEPRODUCTION__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; + +// createnewpatchversion +const FINISHRELEASECYCLE__CREATENEWPATCHVERSION__CREATE_NEW_VERSION__STEP_MOCK = utils.createMockStep( + 'Create new version', + 'Creating new version', + 'CREATENEWPATCHVERSION', + ['SEMVER_LEVEL'], + [], + {NEW_VERSION: '1.2.3'}, + null, + true, + 'createNewVersion', +); +const FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS = [FINISHRELEASECYCLE__CREATENEWPATCHVERSION__CREATE_NEW_VERSION__STEP_MOCK]; + +// updatestaging +const FINISHRELEASECYCLE__UPDATESTAGING__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'UPDATESTAGING', ['ref', 'token'], []); +const FINISHRELEASECYCLE__UPDATESTAGING__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK = utils.createMockStep( + 'Setup Git for OSBotify', + 'Setup Git for OSBotify', + 'UPDATESTAGING', + ['GPG_PASSPHRASE'], + [], +); +const FINISHRELEASECYCLE__UPDATESTAGING__UPDATE_STAGING_BRANCH_TO_TRIGGER_STAGING_DEPLOY__STEP_MOCK = utils.createMockStep( + 'Update staging branch to trigger staging deploy', + 'Updating staging branch', + 'UPDATESTAGING', + [], + [], +); +const FINISHRELEASECYCLE__UPDATESTAGING__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK = utils.createMockStep( + 'Announce failed workflow in Slack', + 'Announce failed workflow in Slack', + 'UPDATESTAGING', + ['SLACK_WEBHOOK'], + [], +); +const FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS = [ + FINISHRELEASECYCLE__UPDATESTAGING__CHECKOUT__STEP_MOCK, + FINISHRELEASECYCLE__UPDATESTAGING__SETUP_GIT_FOR_OSBOTIFY__STEP_MOCK, + FINISHRELEASECYCLE__UPDATESTAGING__UPDATE_STAGING_BRANCH_TO_TRIGGER_STAGING_DEPLOY__STEP_MOCK, + FINISHRELEASECYCLE__UPDATESTAGING__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; + +module.exports = { + FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, + FINISHRELEASECYCLE__VALIDATE__TEAM_MEMBER_BLOCKERS__STEP_MOCKS, + FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_NO_BLOCKERS__STEP_MOCKS, + FINISHRELEASECYCLE__VALIDATE__NOT_TEAM_MEMBER_BLOCKERS__STEP_MOCKS, + FINISHRELEASECYCLE__UPDATEPRODUCTION__STEP_MOCKS, + FINISHRELEASECYCLE__CREATENEWPATCHVERSION__STEP_MOCKS, + FINISHRELEASECYCLE__UPDATESTAGING__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/lintMocks.js b/workflow_tests/mocks/lintMocks.js new file mode 100644 index 000000000000..ecf11074e20f --- /dev/null +++ b/workflow_tests/mocks/lintMocks.js @@ -0,0 +1,17 @@ +const utils = require('../utils/utils'); + +// lint +const LINT__LINT__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'LINT', [], []); +const LINT__LINT__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'LINT', [], []); +const LINT__LINT__LINT_JAVASCRIPT_WITH_ESLINT__STEP_MOCK = utils.createMockStep('Lint JavaScript and Typescript with ESLint', 'Lint JavaScript with ESLint', 'LINT', [], ['CI']); +const LINT__LINT__LINT_SHELL_SCRIPTS_WITH_SHELLCHECK__STEP_MOCK = utils.createMockStep('Lint shell scripts with ShellCheck', 'Lint shell scripts with ShellCheck', 'LINT', [], []); +const LINT__LINT__STEP_MOCKS = [ + LINT__LINT__CHECKOUT__STEP_MOCK, + LINT__LINT__SETUP_NODE__STEP_MOCK, + LINT__LINT__LINT_JAVASCRIPT_WITH_ESLINT__STEP_MOCK, + LINT__LINT__LINT_SHELL_SCRIPTS_WITH_SHELLCHECK__STEP_MOCK, +]; + +module.exports = { + LINT__LINT__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/lockDeploysMocks.js b/workflow_tests/mocks/lockDeploysMocks.js new file mode 100644 index 000000000000..bb6246a2e1d9 --- /dev/null +++ b/workflow_tests/mocks/lockDeploysMocks.js @@ -0,0 +1,35 @@ +const utils = require('../utils/utils'); + +// lockstagingdeploys +const LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'LOCKSTAGINGDEPLOYS', ['ref', 'token'], []); +const LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__WAIT_FOR_STAGING_DEPLOYS_TO_FINISH__STEP_MOCK = utils.createMockStep( + 'Wait for staging deploys to finish', + 'Waiting for staging deploys to finish', + 'LOCKSTAGINGDEPLOYS', + ['GITHUB_TOKEN'], + [], +); +const LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__COMMENT_IN_STAGINGDEPLOYCASH_TO_GIVE_APPLAUSE_THE_GREEN_LIGHT_TO_BEGIN_QA__STEP_MOCK = utils.createMockStep( + 'Comment in StagingDeployCash to give Applause the 🟢 to begin QA', + 'Commenting in StagingDeployCash', + 'LOCKSTAGINGDEPLOYS', + [], + ['GITHUB_TOKEN'], +); +const LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__ANNOUNCE_FAILED_WORKFLOW__STEP_MOCK = utils.createMockStep( + 'Announce failed workflow', + 'Announcing failed workflow in Slack', + 'LOCKSTAGINGDEPLOYS', + ['SLACK_WEBHOOK'], + [], +); +const LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS = [ + LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__CHECKOUT__STEP_MOCK, + LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__WAIT_FOR_STAGING_DEPLOYS_TO_FINISH__STEP_MOCK, + LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__COMMENT_IN_STAGINGDEPLOYCASH_TO_GIVE_APPLAUSE_THE_GREEN_LIGHT_TO_BEGIN_QA__STEP_MOCK, + LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__ANNOUNCE_FAILED_WORKFLOW__STEP_MOCK, +]; + +module.exports = { + LOCKDEPLOYS__LOCKSTAGINGDEPLOYS__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/platformDeployMocks.js b/workflow_tests/mocks/platformDeployMocks.js new file mode 100644 index 000000000000..9e0b91b29156 --- /dev/null +++ b/workflow_tests/mocks/platformDeployMocks.js @@ -0,0 +1,281 @@ +const utils = require('../utils/utils'); + +// validateActor +const PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__TEAM_MEMBER__STEP_MOCK = utils.createMockStep( + 'Check if user is deployer', + 'Checking if the user is a deployer', + 'VALIDATE_ACTOR', + [], + ['GITHUB_TOKEN'], + {IS_DEPLOYER: true}, +); +const PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__OUTSIDER__STEP_MOCK = utils.createMockStep( + 'Check if user is deployer', + 'Checking if the user is a deployer', + 'VALIDATE_ACTOR', + [], + ['GITHUB_TOKEN'], + {IS_DEPLOYER: false}, +); +const PLATFORM_DEPLOY__VALIDATE_ACTOR__TEAM_MEMBER__STEP_MOCKS = [PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__TEAM_MEMBER__STEP_MOCK]; +const PLATFORM_DEPLOY__VALIDATE_ACTOR__OUTSIDER__STEP_MOCKS = [PLATFORM_DEPLOY__VALIDATE_ACTOR__CHECK_USER_DEPLOYER__OUTSIDER__STEP_MOCK]; + +// deployChecklist +const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'DEPLOY_CHECKLIST'); +const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'DEPLOY_CHECKLIST'); +const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__SET_VERSION__STEP_MOCK = utils.createMockStep('Set version', 'Set version', 'DEPLOY_CHECKLIST', [], [], {VERSION: '1.2.3'}); +const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__CREATE_OR_UPDATE_STAGING_DEPLOY__STEP_MOCK = utils.createMockStep( + 'Create or update staging deploy', + 'Create or update staging deploy', + 'DEPLOY_CHECKLIST', + ['GITHUB_TOKEN', 'NPM_VERSION'], +); +const PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS = [ + PLATFORM_DEPLOY__DEPLOY_CHECKLIST__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__DEPLOY_CHECKLIST__SETUP_NODE__STEP_MOCK, + PLATFORM_DEPLOY__DEPLOY_CHECKLIST__SET_VERSION__STEP_MOCK, + PLATFORM_DEPLOY__DEPLOY_CHECKLIST__CREATE_OR_UPDATE_STAGING_DEPLOY__STEP_MOCK, +]; + +// android +const PLATFORM_DEPLOY__ANDROID__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'ANDROID'); +const PLATFORM_DEPLOY__ANDROID__CONFIGURE_MAPBOX_SDK__STEP_MOCK = utils.createMockStep('Configure MapBox SDK', 'Configure MapBox SDK', 'ANDROID'); +const PLATFORM_DEPLOY__ANDROID__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setting up Node', 'ANDROID'); +const PLATFORM_DEPLOY__ANDROID__SETUP_RUBY__STEP_MOCK = utils.createMockStep('Setup Ruby', 'Setting up Ruby', 'ANDROID', ['ruby-version', 'bundler-cache']); +const PLATFORM_DEPLOY__ANDROID__DECRYPT_KEYSTORE__STEP_MOCK = utils.createMockStep('Decrypt keystore', 'Decrypting keystore', 'ANDROID', null, ['LARGE_SECRET_PASSPHRASE']); +const PLATFORM_DEPLOY__ANDROID__DECRYPT_JSON_KEY__STEP_MOCK = utils.createMockStep('Decrypt json key', 'Decrypting JSON key', 'ANDROID', null, ['LARGE_SECRET_PASSPHRASE']); +const PLATFORM_DEPLOY__ANDROID__SET_VERSION__STEP_MOCK = utils.createMockStep('Set version in ENV', 'Setting version in ENV', 'ANDROID', null, null, null, {VERSION_CODE: '1.2.3'}); +const PLATFORM_DEPLOY__ANDROID__FASTLANE_BETA__STEP_MOCK = utils.createMockStep('Run Fastlane beta', 'Running Fastlane beta', 'ANDROID', null, [ + 'MYAPP_UPLOAD_STORE_PASSWORD', + 'MYAPP_UPLOAD_KEY_PASSWORD', +]); +const PLATFORM_DEPLOY__ANDROID__FASTLANE_PRODUCTION__STEP_MOCK = utils.createMockStep('Run Fastlane production', 'Running Fastlane production', 'ANDROID', null, ['VERSION']); +const PLATFORM_DEPLOY__ANDROID__ARCHIVE_SOURCEMAPS__STEP_MOCK = utils.createMockStep('Archive Android sourcemaps', 'Archiving Android sourcemaps', 'ANDROID', ['name', 'path']); +const PLATFORM_DEPLOY__ANDROID__UPLOAD_TO_BROWSER_STACK__STEP_MOCK = utils.createMockStep( + 'Upload Android version to Browser Stack', + 'Uploading Android version to Browser Stack', + 'ANDROID', + null, + ['BROWSERSTACK'], +); +const PLATFORM_DEPLOY__ANDROID__WARN_DEPLOYERS__STEP_MOCK = utils.createMockStep( + 'Warn deployers if Android production deploy failed', + 'Warning deployers of failed production deploy', + 'ANDROID', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const PLATFORM_DEPLOY__ANDROID__STEP_MOCKS = [ + PLATFORM_DEPLOY__ANDROID__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__CONFIGURE_MAPBOX_SDK__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__SETUP_NODE__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__SETUP_RUBY__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__DECRYPT_KEYSTORE__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__DECRYPT_JSON_KEY__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__SET_VERSION__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__FASTLANE_BETA__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__FASTLANE_PRODUCTION__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__ARCHIVE_SOURCEMAPS__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__UPLOAD_TO_BROWSER_STACK__STEP_MOCK, + PLATFORM_DEPLOY__ANDROID__WARN_DEPLOYERS__STEP_MOCK, +]; + +// desktop +const PLATFORM_DEPLOY__DESKTOP__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'DESKTOP'); +const PLATFORM_DEPLOY__DESKTOP__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setting up Node', 'DESKTOP'); +const PLATFORM_DEPLOY__DESKTOP__DECRYPT_ID__STEP_MOCK = utils.createMockStep('Decrypt Developer ID Certificate', 'Decrypting developer id certificate', 'DESKTOP', null, [ + 'DEVELOPER_ID_SECRET_PASSPHRASE', +]); +const PLATFORM_DEPLOY__DESKTOP__BUILD_PRODUCTION__STEP_MOCK = utils.createMockStep('Build production desktop app', 'Building production desktop app', 'DESKTOP', null, [ + 'CSC_LINK', + 'CSC_KEY_PASSWORD', + 'APPLE_ID', + 'APPLE_APP_SPECIFIC_PASSWORD', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', +]); +const PLATFORM_DEPLOY__DESKTOP__BUILD_STAGING__STEP_MOCK = utils.createMockStep('Build staging desktop app', 'Building staging desktop app', 'DESKTOP', null, [ + 'CSC_LINK', + 'CSC_KEY_PASSWORD', + 'APPLE_ID', + 'APPLE_APP_SPECIFIC_PASSWORD', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', +]); +const PLATFORM_DEPLOY__DESKTOP__STEP_MOCKS = [ + PLATFORM_DEPLOY__DESKTOP__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__DESKTOP__SETUP_NODE__STEP_MOCK, + PLATFORM_DEPLOY__DESKTOP__DECRYPT_ID__STEP_MOCK, + PLATFORM_DEPLOY__DESKTOP__BUILD_PRODUCTION__STEP_MOCK, + PLATFORM_DEPLOY__DESKTOP__BUILD_STAGING__STEP_MOCK, +]; + +// ios +const PLATFORM_DEPLOY__IOS__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'IOS'); +const PLATFORM_DEPLOY__IOS__CONFIGURE_MAPBOX_SDK__STEP_MOCK = utils.createMockStep('Configure MapBox SDK', 'Configure MapBox SDK', 'IOS'); +const PLATFORM_DEPLOY__IOS__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setting up Node', 'IOS'); +const PLATFORM_DEPLOY__IOS__SETUP_RUBY__STEP_MOCK = utils.createMockStep('Setup Ruby', 'Setting up Ruby', 'IOS', ['ruby-version', 'bundler-cache']); +const PLATFORM_DEPLOY__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK = utils.createMockStep('Cache Pod dependencies', 'Cache Pod dependencies', 'IOS', ['path', 'key', 'restore-keys'], [], { + 'cache-hit': false, +}); +const PLATFORM_DEPLOY__IOS__COMPARE_PODFILE_AND_MANIFEST__STEP_MOCK = utils.createMockStep( + 'Compare Podfile.lock and Manifest.lock', + 'Compare Podfile.lock and Manifest.lock', + 'IOS', + [], + [], + {IS_PODFILE_SAME_AS_MANIFEST: false}, +); +const PLATFORM_DEPLOY__IOS__COCOAPODS__STEP_MOCK = utils.createMockStep('Install cocoapods', 'Installing cocoapods', 'IOS', ['timeout_minutes', 'max_attempts', 'command']); +const PLATFORM_DEPLOY__IOS__DECRYPT_PROFILE__STEP_MOCK = utils.createMockStep('Decrypt profile', 'Decrypting profile', 'IOS', null, ['LARGE_SECRET_PASSPHRASE']); +const PLATFORM_DEPLOY__IOS__DECRYPT_CERTIFICATE__STEP_MOCK = utils.createMockStep('Decrypt certificate', 'Decrypting certificate', 'IOS', null, ['LARGE_SECRET_PASSPHRASE']); +const PLATFORM_DEPLOY__IOS__DECRYPT_APP_STORE_API_KEY__STEP_MOCK = utils.createMockStep('Decrypt App Store Connect API key', 'Decrypting App Store API key', 'IOS', null, [ + 'LARGE_SECRET_PASSPHRASE', +]); +const PLATFORM_DEPLOY__IOS__FASTLANE__STEP_MOCK = utils.createMockStep('Run Fastlane', 'Running Fastlane', 'IOS', null, [ + 'APPLE_CONTACT_EMAIL', + 'APPLE_CONTACT_PHONE', + 'APPLE_DEMO_EMAIL', + 'APPLE_DEMO_PASSWORD', +]); +const PLATFORM_DEPLOY__IOS__ARCHIVE_SOURCEMAPS__STEP_MOCK = utils.createMockStep('Archive iOS sourcemaps', 'Archiving sourcemaps', 'IOS', ['name', 'path']); +const PLATFORM_DEPLOY__IOS__UPLOAD_BROWSERSTACK__STEP_MOCK = utils.createMockStep('Upload iOS version to Browser Stack', 'Uploading version to Browser Stack', 'IOS', null, ['BROWSERSTACK']); +const PLATFORM_DEPLOY__IOS__SET_VERSION__STEP_MOCK = utils.createMockStep('Set iOS version in ENV', 'Setting iOS version', 'IOS', null, null, null, {IOS_VERSION: '1.2.3'}); +const PLATFORM_DEPLOY__IOS__RELEASE_FASTLANE__STEP_MOCK = utils.createMockStep('Run Fastlane for App Store release', 'Running Fastlane for release', 'IOS', null, ['VERSION']); +const PLATFORM_DEPLOY__IOS__WARN_FAIL__STEP_MOCK = utils.createMockStep( + 'Warn deployers if iOS production deploy failed', + 'Warning developers of failed deploy', + 'IOS', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const PLATFORM_DEPLOY__IOS__STEP_MOCKS = [ + PLATFORM_DEPLOY__IOS__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__IOS__CONFIGURE_MAPBOX_SDK__STEP_MOCK, + PLATFORM_DEPLOY__IOS__SETUP_NODE__STEP_MOCK, + PLATFORM_DEPLOY__IOS__SETUP_RUBY__STEP_MOCK, + PLATFORM_DEPLOY__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK, + PLATFORM_DEPLOY__IOS__COMPARE_PODFILE_AND_MANIFEST__STEP_MOCK, + PLATFORM_DEPLOY__IOS__COCOAPODS__STEP_MOCK, + PLATFORM_DEPLOY__IOS__DECRYPT_PROFILE__STEP_MOCK, + PLATFORM_DEPLOY__IOS__DECRYPT_CERTIFICATE__STEP_MOCK, + PLATFORM_DEPLOY__IOS__DECRYPT_APP_STORE_API_KEY__STEP_MOCK, + PLATFORM_DEPLOY__IOS__FASTLANE__STEP_MOCK, + PLATFORM_DEPLOY__IOS__ARCHIVE_SOURCEMAPS__STEP_MOCK, + PLATFORM_DEPLOY__IOS__UPLOAD_BROWSERSTACK__STEP_MOCK, + PLATFORM_DEPLOY__IOS__SET_VERSION__STEP_MOCK, + PLATFORM_DEPLOY__IOS__RELEASE_FASTLANE__STEP_MOCK, + PLATFORM_DEPLOY__IOS__WARN_FAIL__STEP_MOCK, +]; + +// web +const PLATFORM_DEPLOY__WEB__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'WEB'); +const PLATFORM_DEPLOY__WEB__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setting up Node', 'WEB'); +const PLATFORM_DEPLOY__WEB__CLOUDFLARE__STEP_MOCK = utils.createMockStep('Setup Cloudflare CLI', 'Setting up Cloudflare CLI', 'WEB'); +const PLATFORM_DEPLOY__WEB__AWS_CREDENTIALS__STEP_MOCK = utils.createMockStep('Configure AWS Credentials', 'Configuring AWS credentials', 'WEB', [ + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', +]); +const PLATFORM_DEPLOY__WEB__BUILD_PRODUCTION__STEP_MOCK = utils.createMockStep('Build web for production', 'Building web for production', 'WEB'); +const PLATFORM_DEPLOY__WEB__BUILD_STAGING__STEP_MOCK = utils.createMockStep('Build web for staging', 'Building web for staging', 'WEB'); +const PLATFORM_DEPLOY__WEB__BUILD_STORYBOOK_DOCS_FOR_PRODUCTION__STEP_MOCK = utils.createMockStep('Build storybook docs for production', 'Build storybook docs for production', 'WEB'); +const PLATFORM_DEPLOY__WEB__BUILD_STORYBOOK_DOCS_FOR_STAGING__STEP_MOCK = utils.createMockStep('Build storybook docs for staging', 'Build storybook docs for staging', 'WEB'); +const PLATFORM_DEPLOY__WEB__DEPLOY_PRODUCTION_S3__STEP_MOCK = utils.createMockStep('Deploy production to S3', 'Deploying production to S3', 'WEB'); +const PLATFORM_DEPLOY__WEB__DEPLOY_STAGING_S3__STEP_MOCK = utils.createMockStep('Deploy staging to S3', 'Deploying staging to S3', 'WEB'); +const PLATFORM_DEPLOY__WEB__PURGE_PRODUCTION_CACHE__STEP_MOCK = utils.createMockStep('Purge production Cloudflare cache', 'Purging production Cloudflare cache', 'WEB', null, ['CF_API_KEY']); +const PLATFORM_DEPLOY__WEB__PURGE_STAGING_CACHE__STEP_MOCK = utils.createMockStep('Purge staging Cloudflare cache', 'Purging staging Cloudflare cache', 'WEB', null, ['CF_API_KEY']); +const PLATFORM_DEPLOY__WEB__STEP_MOCKS = [ + PLATFORM_DEPLOY__WEB__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__WEB__SETUP_NODE__STEP_MOCK, + PLATFORM_DEPLOY__WEB__CLOUDFLARE__STEP_MOCK, + PLATFORM_DEPLOY__WEB__AWS_CREDENTIALS__STEP_MOCK, + PLATFORM_DEPLOY__WEB__BUILD_PRODUCTION__STEP_MOCK, + PLATFORM_DEPLOY__WEB__BUILD_STAGING__STEP_MOCK, + PLATFORM_DEPLOY__WEB__BUILD_STORYBOOK_DOCS_FOR_PRODUCTION__STEP_MOCK, + PLATFORM_DEPLOY__WEB__BUILD_STORYBOOK_DOCS_FOR_STAGING__STEP_MOCK, + PLATFORM_DEPLOY__WEB__DEPLOY_PRODUCTION_S3__STEP_MOCK, + PLATFORM_DEPLOY__WEB__DEPLOY_STAGING_S3__STEP_MOCK, + PLATFORM_DEPLOY__WEB__PURGE_PRODUCTION_CACHE__STEP_MOCK, + PLATFORM_DEPLOY__WEB__PURGE_STAGING_CACHE__STEP_MOCK, +]; + +// post slack message on failure +const PLATFORM_DEPLOY__POST_SLACK_FAIL__POST_SLACK__STEP_MOCK = utils.createMockStep('Post Slack message on failure', 'Posting Slack message on platform deploy failure', 'POST_SLACK_FAIL', [ + 'SLACK_WEBHOOK', +]); +const PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS = [PLATFORM_DEPLOY__POST_SLACK_FAIL__POST_SLACK__STEP_MOCK]; + +// post slack message on success +const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'POST_SLACK_SUCCESS'); +const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__SET_VERSION__STEP_MOCK = utils.createMockStep('Set version', 'Setting version', 'POST_SLACK_SUCCESS', null, null, null, {VERSION: '1.2.3'}); +const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__ANNOUNCE_CHANNEL__STEP_MOCK = utils.createMockStep( + 'Announces the deploy in the #announce Slack room', + 'Posting message to \\#announce channel', + 'POST_SLACK_SUCCESS', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__DEPLOYER_CHANNEL__STEP_MOCK = utils.createMockStep( + 'Announces the deploy in the #deployer Slack room', + 'Posting message to \\#deployer channel', + 'POST_SLACK_SUCCESS', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__EXPENSIFY_CHANNEL__STEP_MOCK = utils.createMockStep( + 'Announces a production deploy in the #expensify-open-source Slack room', + 'Posting message to \\#expensify-open-source channel', + 'POST_SLACK_SUCCESS', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const PLATFORM_DEPLOY__POST_SLACK_SUCCESS__STEP_MOCKS = [ + PLATFORM_DEPLOY__POST_SLACK_SUCCESS__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__POST_SLACK_SUCCESS__SET_VERSION__STEP_MOCK, + PLATFORM_DEPLOY__POST_SLACK_SUCCESS__ANNOUNCE_CHANNEL__STEP_MOCK, + PLATFORM_DEPLOY__POST_SLACK_SUCCESS__DEPLOYER_CHANNEL__STEP_MOCK, + PLATFORM_DEPLOY__POST_SLACK_SUCCESS__EXPENSIFY_CHANNEL__STEP_MOCK, +]; + +// post github comment +const PLATFORM_DEPLOY__POST_GIHUB_COMMENT__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checking out', 'POST_GITHUB_COMMENT'); +const PLATFORM_DEPLOY__POST_GIHUB_COMMENT__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setting up Node', 'POST_GITHUB_COMMENT'); +const PLATFORM_DEPLOY__POST_GIHUB_COMMENT__SET_VERSION__STEP_MOCK = utils.createMockStep('Set version', 'Setting version', 'POST_GITHUB_COMMENT', null, null, null, {VERSION: '1.2.3'}); +const PLATFORM_DEPLOY__POST_GIHUB_COMMENT__GET_PR_LIST__STEP_MOCK = utils.createMockStep( + 'Get Release Pull Request List', + 'Getting release pull request list', + 'POST_GITHUB_COMMENT', + ['TAG', 'GITHUB_TOKEN', 'IS_PRODUCTION_DEPLOY'], + null, + {PR_LIST: '[1.2.1, 1.2.2]'}, +); +const PLATFORM_DEPLOY__POST_GIHUB_COMMENT__COMMENT__STEP_MOCK = utils.createMockStep('Comment on issues', 'Commenting on issues', 'POST_GITHUB_COMMENT', [ + 'PR_LIST', + 'IS_PRODUCTION_DEPLOY', + 'DEPLOY_VERSION', + 'GITHUB_TOKEN', + 'ANDROID', + 'DESKTOP', + 'IOS', + 'WEB', +]); +const PLATFORM_DEPLOY__POST_GITHUB_COMMENT__STEP_MOCKS = [ + PLATFORM_DEPLOY__POST_GIHUB_COMMENT__CHECKOUT__STEP_MOCK, + PLATFORM_DEPLOY__POST_GIHUB_COMMENT__SETUP_NODE__STEP_MOCK, + PLATFORM_DEPLOY__POST_GIHUB_COMMENT__SET_VERSION__STEP_MOCK, + PLATFORM_DEPLOY__POST_GIHUB_COMMENT__GET_PR_LIST__STEP_MOCK, + PLATFORM_DEPLOY__POST_GIHUB_COMMENT__COMMENT__STEP_MOCK, +]; + +module.exports = { + PLATFORM_DEPLOY__VALIDATE_ACTOR__TEAM_MEMBER__STEP_MOCKS, + PLATFORM_DEPLOY__VALIDATE_ACTOR__OUTSIDER__STEP_MOCKS, + PLATFORM_DEPLOY__ANDROID__STEP_MOCKS, + PLATFORM_DEPLOY__DESKTOP__STEP_MOCKS, + PLATFORM_DEPLOY__IOS__STEP_MOCKS, + PLATFORM_DEPLOY__WEB__STEP_MOCKS, + PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS, + PLATFORM_DEPLOY__POST_SLACK_SUCCESS__STEP_MOCKS, + PLATFORM_DEPLOY__POST_GITHUB_COMMENT__STEP_MOCKS, + PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/preDeployMocks.js b/workflow_tests/mocks/preDeployMocks.js new file mode 100644 index 000000000000..3dc67a904cf9 --- /dev/null +++ b/workflow_tests/mocks/preDeployMocks.js @@ -0,0 +1,201 @@ +const utils = require('../utils/utils'); + +// typecheck +const TYPECHECK_WORKFLOW_MOCK_STEP = utils.createMockStep('Run typecheck workflow', 'Running typecheck workflow', 'TYPECHECK'); +const TYPECHECK_JOB_MOCK_STEPS = [TYPECHECK_WORKFLOW_MOCK_STEP]; + +// lint +const LINT_WORKFLOW_MOCK_STEP = utils.createMockStep('Run lint workflow', 'Running lint workflow', 'LINT'); +const LINT_JOB_MOCK_STEPS = [LINT_WORKFLOW_MOCK_STEP]; + +// test +const TEST_WORKFLOW_MOCK_STEP = utils.createMockStep('Run test workflow', 'Running test workflow', 'TEST'); +const TEST_JOB_MOCK_STEPS = [TEST_WORKFLOW_MOCK_STEP]; + +// confirm_passing_build +const ANNOUNCE_IN_SLACK_MOCK_STEP = utils.createMockStep('Announce failed workflow in Slack', 'Announcing failed workflow in slack', 'CONFIRM_PASSING_BUILD', ['SLACK_WEBHOOK']); +const CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS = [ + ANNOUNCE_IN_SLACK_MOCK_STEP, + + // 2nd step runs normally +]; + +// choose_deploy_actions +const GET_MERGED_PULL_REQUEST_MOCK_STEP__CHOOSE_DEPLOY = utils.createMockStep('Get merged pull request', 'Getting merged pull request', 'CHOOSE_DEPLOY_ACTIONS', ['github_token'], null, { + number: '123', + labels: '[]', +}); +const CHECK_IF_STAGINGDEPLOYCASH_IS_LOCKED_MOCK_STEP__LOCKED = utils.createMockStep( + 'Check if StagingDeployCash is locked', + 'Checking StagingDeployCash', + 'CHOOSE_DEPLOY_ACTIONS', + ['GITHUB_TOKEN'], + null, + {IS_LOCKED: true}, +); +const CHECK_IF_STAGINGDEPLOYCASH_IS_LOCKED_MOCK_STEP__UNLOCKED = utils.createMockStep( + 'Check if StagingDeployCash is locked', + 'Checking StagingDeployCash', + 'CHOOSE_DEPLOY_ACTIONS', + ['GITHUB_TOKEN'], + null, + {IS_LOCKED: false}, +); +const CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED = [ + GET_MERGED_PULL_REQUEST_MOCK_STEP__CHOOSE_DEPLOY, + CHECK_IF_STAGINGDEPLOYCASH_IS_LOCKED_MOCK_STEP__LOCKED, + + // step 3 runs normally +]; +const CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED = [ + GET_MERGED_PULL_REQUEST_MOCK_STEP__CHOOSE_DEPLOY, + CHECK_IF_STAGINGDEPLOYCASH_IS_LOCKED_MOCK_STEP__UNLOCKED, + + // step 3 runs normally +]; + +// skip_deploy +const COMMENT_ON_DEFERRED_PR_MOCK_STEP = utils.createMockStep('Comment on deferred PR', 'Skipping deploy', 'SKIP_DEPLOY', ['github_token', 'number', 'body']); +const SKIP_DEPLOY_JOB_MOCK_STEPS = [COMMENT_ON_DEFERRED_PR_MOCK_STEP]; + +// create_new_version +const CREATE_NEW_VERSION_MOCK_STEP = utils.createMockStep( + 'Create new version', + 'Creating new version', + 'CREATE_NEW_VERSION', + null, + null, + {NEW_VERSION: '1.2.3'}, + null, + true, + 'createNewVersion', +); +const CREATE_NEW_VERSION_JOB_MOCK_STEPS = [CREATE_NEW_VERSION_MOCK_STEP]; + +// update_staging +const RUN_TURNSTYLE_MOCK_STEP = utils.createMockStep('Run turnstyle', 'Running turnstyle', 'UPDATE_STAGING', ['poll-interval-seconds'], ['GITHUB_TOKEN']); +const CHECKOUT_MAIN_MOCK_STEP = utils.createMockStep('Checkout main', 'Checkout main', 'UPDATE_STAGING', ['ref', 'token']); +const SETUP_GIT_FOR_OSBOTIFY_MOCK_STEP = utils.createMockStep('Setup Git for OSBotify', 'Setup Git for OSBotify', 'UPDATE_STAGING', ['GPG_PASSPHRASE']); +const UPDATE_STAGING_BRANCH_FROM_MAIN_MOCK_STEP = utils.createMockStep('Update staging branch from main', 'Update staging branch from main', 'UPDATE_STAGING'); +const ANNOUNCE_FAILED_WORKFLOW_IN_SLACK_MOCK_STEP = utils.createMockStep('Announce failed workflow in Slack', 'Announcing failed workflow in Slack', 'UPDATE_STAGING', ['SLACK_WEBHOOK']); +const UPDATE_STAGING_JOB_MOCK_STEPS = [ + RUN_TURNSTYLE_MOCK_STEP, + CHECKOUT_MAIN_MOCK_STEP, + SETUP_GIT_FOR_OSBOTIFY_MOCK_STEP, + UPDATE_STAGING_BRANCH_FROM_MAIN_MOCK_STEP, + ANNOUNCE_FAILED_WORKFLOW_IN_SLACK_MOCK_STEP, +]; + +// is_expensify_employee +const GET_MERGED_PULL_REQUEST_MOCK_STEP__IS_EXPENSIFY_EMPLOYEE = utils.createMockStep( + 'Get merged pull request', + 'Getting merged pull request', + 'IS_EXPENSIFY_EMPLOYEE', + ['github_token'], + null, + {author: 'Dummy Author'}, +); +const CHECK_TEAM_MEMBERSHIP_MOCK_STEP__TRUE = utils.createMockStep( + 'Check whether the PR author is member of Expensify/expensify team', + 'Checking actors Expensify membership', + 'IS_EXPENSIFY_EMPLOYEE', + [], + ['GITHUB_TOKEN'], + {IS_EXPENSIFY_EMPLOYEE: true}, +); +const IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE = [GET_MERGED_PULL_REQUEST_MOCK_STEP__IS_EXPENSIFY_EMPLOYEE, CHECK_TEAM_MEMBERSHIP_MOCK_STEP__TRUE]; +const CHECK_TEAM_MEMBERSHIP_MOCK_STEP__FALSE = utils.createMockStep( + 'Check whether the PR author is member of Expensify/expensify team', + 'Checking actors Expensify membership', + 'IS_EXPENSIFY_EMPLOYEE', + [], + ['GITHUB_TOKEN'], + {IS_EXPENSIFY_EMPLOYEE: false}, +); +const IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE = [GET_MERGED_PULL_REQUEST_MOCK_STEP__IS_EXPENSIFY_EMPLOYEE, CHECK_TEAM_MEMBERSHIP_MOCK_STEP__FALSE]; + +// new_contributor_welcome_message +const CHECKOUT_MOCK_STEP = utils.createMockStep('Checkout', 'Checking out', 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', ['token'], null, {author: 'Dummy Author'}); +const CHECKOUT_MOCK_STEP__OSBOTIFY = utils.createMockStep('Checkout', 'Checking out', 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', ['token'], null, {author: 'OSBotify'}); +const GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE = utils.createMockStep( + 'Get merged pull request', + 'Getting merged pull request', + 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', + ['github_token'], + null, + {number: '12345', author: 'Dummy Author'}, +); +const GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE__OSBOTIFY = utils.createMockStep( + 'Get merged pull request', + 'Getting merged pull request', + 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', + ['github_token'], + null, + {number: '12345', author: 'OSBotify'}, +); +const GET_PR_COUNT_MOCK_STEP__1 = utils.createMockStep( + // eslint-disable-next-line no-template-curly-in-string + 'Get PR count for ${{ steps.getMergedPullRequest.outputs.author }}', + 'Getting PR count', + 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', + null, + ['GITHUB_TOKEN'], + null, + {PR_COUNT: '1'}, +); +const GET_PR_COUNT_MOCK_STEP__10 = utils.createMockStep( + // eslint-disable-next-line no-template-curly-in-string + 'Get PR count for ${{ steps.getMergedPullRequest.outputs.author }}', + 'Getting PR count', + 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', + null, + ['GITHUB_TOKEN'], + null, + {PR_COUNT: '10'}, +); +const COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP = utils.createMockStep( + // eslint-disable-next-line no-template-curly-in-string + "Comment on ${{ steps.getMergedPullRequest.outputs.author }}\\'s first pull request!", + 'Creating comment', + 'NEW_CONTRIBUTOR_WELCOME_MESSAGE', + ['github_token', 'number', 'body'], +); +const NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS = [ + CHECKOUT_MOCK_STEP, + GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE, + GET_PR_COUNT_MOCK_STEP__10, + COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP, +]; +const NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__ONE_PR = [ + CHECKOUT_MOCK_STEP, + GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE, + GET_PR_COUNT_MOCK_STEP__1, + COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP, +]; +const NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY = [ + CHECKOUT_MOCK_STEP__OSBOTIFY, + GET_MERGED_PULL_REQUEST_MOCK_STEP__WELCOME_MESSAGE__OSBOTIFY, + GET_PR_COUNT_MOCK_STEP__10, + COMMENT_ON_FIRST_PULL_REQUEST_MOCK_STEP, +]; + +const PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP = utils.createMockStep('Perform E2E tests', 'Perform E2E tests', 'E2EPERFORMANCETESTS'); +const PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS = [PREDEPLOY__E2EPERFORMANCETESTS__PERFORM_E2E_TESTS__MOCK_STEP]; + +module.exports = { + TYPECHECK_JOB_MOCK_STEPS, + LINT_JOB_MOCK_STEPS, + TEST_JOB_MOCK_STEPS, + CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED, + CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + SKIP_DEPLOY_JOB_MOCK_STEPS, + CREATE_NEW_VERSION_JOB_MOCK_STEPS, + UPDATE_STAGING_JOB_MOCK_STEPS, + IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, + NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__ONE_PR, + NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, + PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, +}; diff --git a/workflow_tests/mocks/reviewerChecklistMocks.js b/workflow_tests/mocks/reviewerChecklistMocks.js new file mode 100644 index 000000000000..5f9ef67198a8 --- /dev/null +++ b/workflow_tests/mocks/reviewerChecklistMocks.js @@ -0,0 +1,9 @@ +const utils = require('../utils/utils'); + +// checklist +const REVIEWERCHECKLIST__CHECKLIST__REVIEWERCHECKLIST_JS__STEP_MOCK = utils.createMockStep('reviewerChecklist.js', 'reviewerChecklist.js', 'CHECKLIST', ['GITHUB_TOKEN'], []); +const REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS = [REVIEWERCHECKLIST__CHECKLIST__REVIEWERCHECKLIST_JS__STEP_MOCK]; + +module.exports = { + REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/testBuildMocks.js b/workflow_tests/mocks/testBuildMocks.js new file mode 100644 index 000000000000..bdff0ac57ae1 --- /dev/null +++ b/workflow_tests/mocks/testBuildMocks.js @@ -0,0 +1,263 @@ +const utils = require('../utils/utils'); + +// validateactor +const TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__TRUE__STEP_MOCK = utils.createMockStep('Is Expensify employee', 'Is Expensify employee', 'VALIDATEACTOR', [], ['GITHUB_TOKEN'], { + IS_EXPENSIFY_EMPLOYEE: true, +}); +const TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__FALSE__STEP_MOCK = utils.createMockStep('Is Expensify employee', 'Is Expensify employee', 'VALIDATEACTOR', [], ['GITHUB_TOKEN'], { + IS_EXPENSIFY_EMPLOYEE: false, +}); +const TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__TRUE__STEP_MOCK = utils.createMockStep( + 'Set HAS_READY_TO_BUILD_LABEL flag', + 'Set HAS_READY_TO_BUILD_LABEL flag', + 'VALIDATEACTOR', + [], + ['PULL_REQUEST_NUMBER', 'GITHUB_TOKEN'], + {HAS_READY_TO_BUILD_LABEL: true}, +); +const TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__FALSE__STEP_MOCK = utils.createMockStep( + 'Set HAS_READY_TO_BUILD_LABEL flag', + 'Set HAS_READY_TO_BUILD_LABEL flag', + 'VALIDATEACTOR', + [], + ['PULL_REQUEST_NUMBER', 'GITHUB_TOKEN'], + {HAS_READY_TO_BUILD_LABEL: false}, +); +const TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS = [ + TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__TRUE__STEP_MOCK, + TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__TRUE__STEP_MOCK, +]; +const TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS = [ + TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__TRUE__STEP_MOCK, + TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__FALSE__STEP_MOCK, +]; +const TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS = [ + TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__FALSE__STEP_MOCK, + TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__TRUE__STEP_MOCK, +]; +const TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS = [ + TESTBUILD__VALIDATEACTOR__IS_TEAM_MEMBER__FALSE__STEP_MOCK, + TESTBUILD__VALIDATEACTOR__SET_HAS_READY_TO_BUILD_LABEL_FLAG__FALSE__STEP_MOCK, +]; + +// getbranchref +const TESTBUILD__GETBRANCHREF__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'GETBRANCHREF', [], []); +const TESTBUILD__GETBRANCHREF__CHECK_IF_PULL_REQUEST_NUMBER_IS_CORRECT__STEP_MOCK = utils.createMockStep( + 'Check if pull request number is correct', + 'Check if pull request number is correct', + 'GETBRANCHREF', + [], + ['GITHUB_TOKEN'], + {REF: 'test-ref'}, +); +const TESTBUILD__GETBRANCHREF__STEP_MOCKS = [TESTBUILD__GETBRANCHREF__CHECKOUT__STEP_MOCK, TESTBUILD__GETBRANCHREF__CHECK_IF_PULL_REQUEST_NUMBER_IS_CORRECT__STEP_MOCK]; + +// android +const TESTBUILD__ANDROID__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'ANDROID', ['ref'], []); +const TESTBUILD__ANDROID__CREATE_ENV_ADHOC__STEP_MOCK = utils.createMockStep( + 'Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', + 'Creating .env.adhoc file based on staging', + 'ANDROID', + [], + [], +); +const TESTBUILD__ANDROID__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'ANDROID', [], []); +const TESTBUILD__ANDROID__SETUP_RUBY__STEP_MOCK = utils.createMockStep('Setup Ruby', 'Setup Ruby', 'ANDROID', ['ruby-version', 'bundler-cache'], []); +const TESTBUILD__ANDROID__DECRYPT_KEYSTORE__STEP_MOCK = utils.createMockStep('Decrypt keystore', 'Decrypt keystore', 'ANDROID', [], ['LARGE_SECRET_PASSPHRASE']); +const TESTBUILD__ANDROID__DECRYPT_JSON_KEY__STEP_MOCK = utils.createMockStep('Decrypt json key', 'Decrypt json key', 'ANDROID', [], ['LARGE_SECRET_PASSPHRASE']); +const TESTBUILD__ANDROID__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK = utils.createMockStep( + 'Configure AWS Credentials', + 'Configure AWS Credentials', + 'ANDROID', + ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'], + [], +); +const TESTBUILD__ANDROID__CONFIGURE_MAPBOX_SDK__STEP_MOCK = utils.createMockStep('Configure MapBox SDK', 'Configure MapBox SDK', 'ANDROID'); +const TESTBUILD__ANDROID__RUN_FASTLANE_BETA_TEST__STEP_MOCK = utils.createMockStep( + 'Run Fastlane beta test', + 'Run Fastlane beta test', + 'ANDROID', + [], + ['S3_ACCESS_KEY', 'S3_SECRET_ACCESS_KEY', 'S3_BUCKET', 'S3_REGION', 'MYAPP_UPLOAD_STORE_PASSWORD', 'MYAPP_UPLOAD_KEY_PASSWORD'], +); +const TESTBUILD__ANDROID__UPLOAD_ARTIFACT__STEP_MOCK = utils.createMockStep('Upload Artifact', 'Upload Artifact', 'ANDROID', ['name', 'path'], []); +const TESTBUILD__ANDROID__STEP_MOCKS = [ + TESTBUILD__ANDROID__CHECKOUT__STEP_MOCK, + TESTBUILD__ANDROID__CREATE_ENV_ADHOC__STEP_MOCK, + TESTBUILD__ANDROID__SETUP_NODE__STEP_MOCK, + TESTBUILD__ANDROID__SETUP_RUBY__STEP_MOCK, + TESTBUILD__ANDROID__DECRYPT_KEYSTORE__STEP_MOCK, + TESTBUILD__ANDROID__DECRYPT_JSON_KEY__STEP_MOCK, + TESTBUILD__ANDROID__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK, + TESTBUILD__ANDROID__CONFIGURE_MAPBOX_SDK__STEP_MOCK, + TESTBUILD__ANDROID__RUN_FASTLANE_BETA_TEST__STEP_MOCK, + TESTBUILD__ANDROID__UPLOAD_ARTIFACT__STEP_MOCK, +]; + +// ios +const TESTBUILD__IOS__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'IOS', ['ref'], []); +const TESTBUILD__IOS__CONFIGURE_MAPBOX_SDK__STEP_MOCK = utils.createMockStep('Configure MapBox SDK', 'Configure MapBox SDK', 'IOS'); +const TESTBUILD__IOS__CREATE_ENV_ADHOC__STEP_MOCK = utils.createMockStep( + 'Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', + 'Creating .env.adhoc file based on staging', + 'IOS', + [], + [], +); +const TESTBUILD__IOS__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'IOS', [], []); +const TESTBUILD__IOS__SETUP_XCODE__STEP_MOCK = utils.createMockStep('Setup XCode', 'Setup XCode', 'IOS', [], []); +const TESTBUILD__IOS__SETUP_RUBY__STEP_MOCK = utils.createMockStep('Setup Ruby', 'Setup Ruby', 'IOS', ['ruby-version', 'bundler-cache'], []); +const TESTBUILD__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK = utils.createMockStep('Cache Pod dependencies', 'Cache Pod dependencies', 'IOS', ['path', 'key', 'restore-keys'], [], { + 'cache-hit': false, +}); +const TESTBUILD__IOS__COMPARE_PODFILE_AND_MANIFEST__STEP_MOCK = utils.createMockStep('Compare Podfile.lock and Manifest.lock', 'Compare Podfile.lock and Manifest.lock', 'IOS', [], [], { + IS_PODFILE_SAME_AS_MANIFEST: false, +}); +const TESTBUILD__IOS__INSTALL_COCOAPODS__STEP_MOCK = utils.createMockStep('Install cocoapods', 'Install cocoapods', 'IOS', ['timeout_minutes', 'max_attempts', 'command'], []); +const TESTBUILD__IOS__DECRYPT_PROFILE__STEP_MOCK = utils.createMockStep('Decrypt profile', 'Decrypt profile', 'IOS', [], ['LARGE_SECRET_PASSPHRASE']); +const TESTBUILD__IOS__DECRYPT_CERTIFICATE__STEP_MOCK = utils.createMockStep('Decrypt certificate', 'Decrypt certificate', 'IOS', [], ['LARGE_SECRET_PASSPHRASE']); +const TESTBUILD__IOS__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK = utils.createMockStep( + 'Configure AWS Credentials', + 'Configure AWS Credentials', + 'IOS', + ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'], + [], +); +const TESTBUILD__IOS__RUN_FASTLANE__STEP_MOCK = utils.createMockStep('Run Fastlane', 'Run Fastlane', 'IOS', [], ['S3_ACCESS_KEY', 'S3_SECRET_ACCESS_KEY', 'S3_BUCKET', 'S3_REGION']); +const TESTBUILD__IOS__UPLOAD_ARTIFACT__STEP_MOCK = utils.createMockStep('Upload Artifact', 'Upload Artifact', 'IOS', ['name', 'path'], []); +const TESTBUILD__IOS__STEP_MOCKS = [ + TESTBUILD__IOS__CHECKOUT__STEP_MOCK, + TESTBUILD__IOS__CONFIGURE_MAPBOX_SDK__STEP_MOCK, + TESTBUILD__IOS__CREATE_ENV_ADHOC__STEP_MOCK, + TESTBUILD__IOS__SETUP_NODE__STEP_MOCK, + TESTBUILD__IOS__SETUP_XCODE__STEP_MOCK, + TESTBUILD__IOS__SETUP_RUBY__STEP_MOCK, + TESTBUILD__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK, + TESTBUILD__IOS__COMPARE_PODFILE_AND_MANIFEST__STEP_MOCK, + TESTBUILD__IOS__INSTALL_COCOAPODS__STEP_MOCK, + TESTBUILD__IOS__DECRYPT_PROFILE__STEP_MOCK, + TESTBUILD__IOS__DECRYPT_CERTIFICATE__STEP_MOCK, + TESTBUILD__IOS__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK, + TESTBUILD__IOS__RUN_FASTLANE__STEP_MOCK, + TESTBUILD__IOS__UPLOAD_ARTIFACT__STEP_MOCK, +]; + +// desktop +const TESTBUILD__DESKTOP__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'DESKTOP', ['ref'], []); +const TESTBUILD__DESKTOP__CREATE_ENV_ADHOC__STEP_MOCK = utils.createMockStep( + 'Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', + 'Creating .env.adhoc file based on staging', + 'DESKTOP', + [], + [], +); +const TESTBUILD__DESKTOP__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'DESKTOP', [], []); +const TESTBUILD__DESKTOP__DECRYPT_DEVELOPER_ID_CERTIFICATE__STEP_MOCK = utils.createMockStep( + 'Decrypt Developer ID Certificate', + 'Decrypt Developer ID Certificate', + 'DESKTOP', + [], + ['DEVELOPER_ID_SECRET_PASSPHRASE'], +); +const TESTBUILD__DESKTOP__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK = utils.createMockStep( + 'Configure AWS Credentials', + 'Configure AWS Credentials', + 'DESKTOP', + ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'], + [], +); +const TESTBUILD__DESKTOP__BUILD_DESKTOP_APP_FOR_TESTING__STEP_MOCK = utils.createMockStep( + 'Build desktop app for testing', + 'Build desktop app for testing', + 'DESKTOP', + [], + ['CSC_LINK', 'CSC_KEY_PASSWORD', 'APPLE_ID', 'APPLE_APP_SPECIFIC_PASSWORD', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'], +); +const TESTBUILD__DESKTOP__STEP_MOCKS = [ + TESTBUILD__DESKTOP__CHECKOUT__STEP_MOCK, + TESTBUILD__DESKTOP__CREATE_ENV_ADHOC__STEP_MOCK, + TESTBUILD__DESKTOP__SETUP_NODE__STEP_MOCK, + TESTBUILD__DESKTOP__DECRYPT_DEVELOPER_ID_CERTIFICATE__STEP_MOCK, + TESTBUILD__DESKTOP__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK, + TESTBUILD__DESKTOP__BUILD_DESKTOP_APP_FOR_TESTING__STEP_MOCK, +]; + +// web +const TESTBUILD__WEB__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'WEB', ['ref'], []); +const TESTBUILD__WEB__CREATE_ENV_ADHOC__STEP_MOCK = utils.createMockStep( + 'Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it', + 'Creating .env.adhoc file based on staging', + 'WEB', + [], + [], +); +const TESTBUILD__WEB__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'WEB', [], []); +const TESTBUILD__WEB__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK = utils.createMockStep( + 'Configure AWS Credentials', + 'Configure AWS Credentials', + 'WEB', + ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'], + [], +); +const TESTBUILD__WEB__BUILD_WEB_FOR_TESTING__STEP_MOCK = utils.createMockStep('Build web for testing', 'Build web for testing', 'WEB', [], []); +const TESTBUILD__WEB__BUILD_DOCS__STEP_MOCK = utils.createMockStep('Build docs', 'Build docs', 'WEB', [], []); +const TESTBUILD__WEB__DEPLOY_TO_S3_FOR_INTERNAL_TESTING__STEP_MOCK = utils.createMockStep('Deploy to S3 for internal testing', 'Deploy to S3 for internal testing', 'WEB', [], []); +const TESTBUILD__WEB__STEP_MOCKS = [ + TESTBUILD__WEB__CHECKOUT__STEP_MOCK, + TESTBUILD__WEB__CREATE_ENV_ADHOC__STEP_MOCK, + TESTBUILD__WEB__SETUP_NODE__STEP_MOCK, + TESTBUILD__WEB__CONFIGURE_AWS_CREDENTIALS__STEP_MOCK, + TESTBUILD__WEB__BUILD_WEB_FOR_TESTING__STEP_MOCK, + TESTBUILD__WEB__BUILD_DOCS__STEP_MOCK, + TESTBUILD__WEB__DEPLOY_TO_S3_FOR_INTERNAL_TESTING__STEP_MOCK, +]; + +// postgithubcomment +const TESTBUILD__POSTGITHUBCOMMENT__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'POSTGITHUBCOMMENT', ['ref'], []); +const TESTBUILD__POSTGITHUBCOMMENT__DOWNLOAD_ARTIFACT__STEP_MOCK = utils.createMockStep('Download Artifact', 'Download Artifact', 'POSTGITHUBCOMMENT', [], []); +const TESTBUILD__POSTGITHUBCOMMENT__READ_JSONS_WITH_ANDROID_PATHS__STEP_MOCK = utils.createMockStep( + 'Read JSONs with android paths', + 'Read JSONs with android paths', + 'POSTGITHUBCOMMENT', + [], + [], + {android_path: 'http://dummy.android.link'}, +); +const TESTBUILD__POSTGITHUBCOMMENT__READ_JSONS_WITH_IOS_PATHS__STEP_MOCK = utils.createMockStep('Read JSONs with iOS paths', 'Read JSONs with iOS paths', 'POSTGITHUBCOMMENT', [], [], { + ios_path: 'http://dummy.ios.link', +}); +const TESTBUILD__POSTGITHUBCOMMENT__MAINTAIN_COMMENT__STEP_MOCK = utils.createMockStep( + 'maintain-comment', + 'maintain-comment', + 'POSTGITHUBCOMMENT', + ['token', 'body-include', 'number', 'delete'], + [], +); +const TESTBUILD__POSTGITHUBCOMMENT__PUBLISH_LINKS_TO_APPS_FOR_DOWNLOAD__STEP_MOCK = utils.createMockStep( + 'Publish links to apps for download', + 'Publish links to apps for download', + 'POSTGITHUBCOMMENT', + ['PR_NUMBER', 'GITHUB_TOKEN', 'ANDROID', 'DESKTOP', 'IOS', 'WEB', 'ANDROID_LINK', 'DESKTOP_LINK', 'IOS_LINK', 'WEB_LINK'], + [], +); +const TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS = [ + TESTBUILD__POSTGITHUBCOMMENT__CHECKOUT__STEP_MOCK, + TESTBUILD__POSTGITHUBCOMMENT__DOWNLOAD_ARTIFACT__STEP_MOCK, + TESTBUILD__POSTGITHUBCOMMENT__READ_JSONS_WITH_ANDROID_PATHS__STEP_MOCK, + TESTBUILD__POSTGITHUBCOMMENT__READ_JSONS_WITH_IOS_PATHS__STEP_MOCK, + TESTBUILD__POSTGITHUBCOMMENT__MAINTAIN_COMMENT__STEP_MOCK, + TESTBUILD__POSTGITHUBCOMMENT__PUBLISH_LINKS_TO_APPS_FOR_DOWNLOAD__STEP_MOCK, +]; + +module.exports = { + TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + TESTBUILD__GETBRANCHREF__STEP_MOCKS, + TESTBUILD__ANDROID__STEP_MOCKS, + TESTBUILD__IOS__STEP_MOCKS, + TESTBUILD__DESKTOP__STEP_MOCKS, + TESTBUILD__WEB__STEP_MOCKS, + TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/testMocks.js b/workflow_tests/mocks/testMocks.js new file mode 100644 index 000000000000..19011271bb47 --- /dev/null +++ b/workflow_tests/mocks/testMocks.js @@ -0,0 +1,26 @@ +const utils = require('../utils/utils'); + +// jest +const TEST__JEST__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'JEST', [], []); +const TEST__JEST__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'JEST', [], []); +const TEST__JEST__GET_NUMBER_OF_CPU_CORES__STEP_MOCK = utils.createMockStep('Get number of CPU cores', 'Get number of CPU cores', 'JEST', [], [], {count: 8}); +const TEST__JEST__CACHE_JEST_CACHE__STEP_MOCK = utils.createMockStep('Cache Jest cache', 'Cache Jest cache', 'JEST', ['path', 'key'], []); +const TEST__JEST__JEST_TESTS__STEP_MOCK = utils.createMockStep('Jest tests', 'Jest tests', 'JEST', [], []); +const TEST__JEST__STEP_MOCKS = [ + TEST__JEST__CHECKOUT__STEP_MOCK, + TEST__JEST__SETUP_NODE__STEP_MOCK, + TEST__JEST__GET_NUMBER_OF_CPU_CORES__STEP_MOCK, + TEST__JEST__CACHE_JEST_CACHE__STEP_MOCK, + TEST__JEST__JEST_TESTS__STEP_MOCK, +]; + +// shelltests +const TEST__SHELLTESTS__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'SHELLTESTS', [], []); +const TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'SHELLTESTS', [], []); +const TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK = utils.createMockStep('Test CI git logic', 'Test CI git logic', 'SHELLTESTS', [], []); +const TEST__SHELLTESTS__STEP_MOCKS = [TEST__SHELLTESTS__CHECKOUT__STEP_MOCK, TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK, TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK]; + +module.exports = { + TEST__JEST__STEP_MOCKS, + TEST__SHELLTESTS__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/validateGithubActionsMocks.js b/workflow_tests/mocks/validateGithubActionsMocks.js new file mode 100644 index 000000000000..e2d48932acf6 --- /dev/null +++ b/workflow_tests/mocks/validateGithubActionsMocks.js @@ -0,0 +1,23 @@ +const utils = require('../utils/utils'); + +// verify +const VALIDATEGITHUBACTIONS__VERIFY__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'VERIFY'); +const VALIDATEGITHUBACTIONS__VERIFY__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'VERIFY', [], []); +const VALIDATEGITHUBACTIONS__VERIFY__VERIFY_JAVASCRIPT_ACTION_BUILDS__STEP_MOCK = utils.createMockStep( + 'Verify Javascript Action Builds', + 'Verify Javascript Action Builds', + 'VERIFY', + [], + [], +); +const VALIDATEGITHUBACTIONS__VERIFY__VALIDATE_ACTIONS_AND_WORKFLOWS__STEP_MOCK = utils.createMockStep('Validate actions and workflows', 'Validate actions and workflows', 'VERIFY', [], []); +const VALIDATEGITHUBACTIONS__VERIFY__STEP_MOCKS = [ + VALIDATEGITHUBACTIONS__VERIFY__CHECKOUT__STEP_MOCK, + VALIDATEGITHUBACTIONS__VERIFY__SETUP_NODE__STEP_MOCK, + VALIDATEGITHUBACTIONS__VERIFY__VERIFY_JAVASCRIPT_ACTION_BUILDS__STEP_MOCK, + VALIDATEGITHUBACTIONS__VERIFY__VALIDATE_ACTIONS_AND_WORKFLOWS__STEP_MOCK, +]; + +module.exports = { + VALIDATEGITHUBACTIONS__VERIFY__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/verifyPodfileMocks.js b/workflow_tests/mocks/verifyPodfileMocks.js new file mode 100644 index 000000000000..0a82eebcc748 --- /dev/null +++ b/workflow_tests/mocks/verifyPodfileMocks.js @@ -0,0 +1,11 @@ +const utils = require('../utils/utils'); + +// verify +const VERIFYPODFILE__VERIFY__CHECKOUT__STEP_MOCK = utils.createMockStep('Checkout', 'Checkout', 'VERIFY'); +const VERIFYPODFILE__VERIFY__SETUP_NODE__STEP_MOCK = utils.createMockStep('Setup Node', 'Setup Node', 'VERIFY', [], []); +const VERIFYPODFILE__VERIFY__VERIFY_PODFILE__STEP_MOCK = utils.createMockStep('Verify podfile', 'Verify podfile', 'VERIFY', [], []); +const VERIFYPODFILE__VERIFY__STEP_MOCKS = [VERIFYPODFILE__VERIFY__CHECKOUT__STEP_MOCK, VERIFYPODFILE__VERIFY__SETUP_NODE__STEP_MOCK, VERIFYPODFILE__VERIFY__VERIFY_PODFILE__STEP_MOCK]; + +module.exports = { + VERIFYPODFILE__VERIFY__STEP_MOCKS, +}; diff --git a/workflow_tests/mocks/verifySignedCommitsMocks.js b/workflow_tests/mocks/verifySignedCommitsMocks.js new file mode 100644 index 000000000000..a19fac809e55 --- /dev/null +++ b/workflow_tests/mocks/verifySignedCommitsMocks.js @@ -0,0 +1,15 @@ +const utils = require('../utils/utils'); + +// verifysignedcommits +const VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__VERIFY_SIGNED_COMMITS__STEP_MOCK = utils.createMockStep( + 'Verify signed commits', + 'Verify signed commits', + 'VERIFYSIGNEDCOMMITS', + ['GITHUB_TOKEN'], + [], +); +const VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS = [VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__VERIFY_SIGNED_COMMITS__STEP_MOCK]; + +module.exports = { + VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS, +}; diff --git a/workflow_tests/platformDeploy.test.js b/workflow_tests/platformDeploy.test.js new file mode 100644 index 000000000000..e74b8e873001 --- /dev/null +++ b/workflow_tests/platformDeploy.test.js @@ -0,0 +1,273 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/platformDeployAssertions'); +const mocks = require('./mocks/platformDeployMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'platformDeploy.yml'), + dest: '.github/workflows/platformDeploy.yml', + }, +]; + +describe('test workflow platformDeploy', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testPlatformDeployWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + pushedBranches: [], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('push', () => { + describe('tag', () => { + it('as team member - platform deploy executes on staging', async () => { + const repoPath = mockGithub.repo.getPath('testPlatformDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'platformDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + { + ref: 'refs/tags/1.2.3', + ref_type: 'tag', + ref_name: '1.2.3', + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + MYAPP_UPLOAD_STORE_PASSWORD: 'dummy_store_password', + MYAPP_UPLOAD_KEY_PASSWORD: 'dummy_key_password', + BROWSERSTACK: 'dummy_browserstack', + SLACK_WEBHOOK: 'dummy_slack_webhook', + DEVELOPER_ID_SECRET_PASSPHRASE: 'dummy_secret_passphrase', + CSC_LINK: 'dummy_csc_link', + CSC_KEY_PASSWORD: 'dummy_csc_key_pass', + APPLE_ID: 'dummy_apple_id', + APPLE_ID_PASSWORD: 'dummy_apple_pass', + AWS_ACCESS_KEY_ID: 'dummy_aws_access_key_id', + AWS_SECRET_ACCESS_KEY: 'dummy_aws_secret_access_key', + APPLE_CONTACT_EMAIL: 'dummy@email.com', + APPLE_CONTACT_PHONE: '123456789', + APPLE_DEMO_EMAIL: 'dummy.demo@email.com', + APPLE_DEMO_PASSWORD: 'dummy_password', + CLOUDFLARE_TOKEN: 'dummy_cloudflare_token', + }, + 'dummy_github_token', + { + AS_REPO: 'App', + }, + ); + act = utils.setJobRunners( + act, + { + desktop: 'ubuntu-latest', + iOS: 'ubuntu-latest', + android: 'ubuntu-latest', + web: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + validateActor: mocks.PLATFORM_DEPLOY__VALIDATE_ACTOR__TEAM_MEMBER__STEP_MOCKS, + deployChecklist: mocks.PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS, + android: mocks.PLATFORM_DEPLOY__ANDROID__STEP_MOCKS, + desktop: mocks.PLATFORM_DEPLOY__DESKTOP__STEP_MOCKS, + iOS: mocks.PLATFORM_DEPLOY__IOS__STEP_MOCKS, + web: mocks.PLATFORM_DEPLOY__WEB__STEP_MOCKS, + postSlackMessageOnFailure: mocks.PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS, + postSlackMessageOnSuccess: mocks.PLATFORM_DEPLOY__POST_SLACK_SUCCESS__STEP_MOCKS, + postGithubComment: mocks.PLATFORM_DEPLOY__POST_GITHUB_COMMENT__STEP_MOCKS, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'platformDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('platformDeploy', expect.getState().currentTestName), + }); + + assertions.assertVerifyActorJobExecuted(result, 'Dummy Author'); + assertions.assertDeployChecklistJobExecuted(result, true); + assertions.assertAndroidJobExecuted(result, true, false, true); + assertions.assertDesktopJobExecuted(result, true, false); + assertions.assertIOSJobExecuted(result, true, false, true); + assertions.assertWebJobExecuted(result, true, false); + assertions.assertPostSlackOnFailureJobExecuted(result, false); + assertions.assertPostSlackOnSuccessJobExecuted(result, true, false); + assertions.assertPostGithubCommentJobExecuted(result, true, false); + }); + + it('as OSBotify - platform deploy executes on staging', async () => { + const repoPath = mockGithub.repo.getPath('testPlatformDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'platformDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + { + ref: 'refs/tags/1.2.3', + ref_type: 'tag', + ref_name: '1.2.3', + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + MYAPP_UPLOAD_STORE_PASSWORD: 'dummy_store_password', + MYAPP_UPLOAD_KEY_PASSWORD: 'dummy_key_password', + BROWSERSTACK: 'dummy_browserstack', + SLACK_WEBHOOK: 'dummy_slack_webhook', + DEVELOPER_ID_SECRET_PASSPHRASE: 'dummy_secret_passphrase', + CSC_LINK: 'dummy_csc_link', + CSC_KEY_PASSWORD: 'dummy_csc_key_pass', + APPLE_ID: 'dummy_apple_id', + APPLE_ID_PASSWORD: 'dummy_apple_pass', + AWS_ACCESS_KEY_ID: 'dummy_aws_access_key_id', + AWS_SECRET_ACCESS_KEY: 'dummy_aws_secret_access_key', + APPLE_CONTACT_EMAIL: 'dummy@email.com', + APPLE_CONTACT_PHONE: '123456789', + APPLE_DEMO_EMAIL: 'dummy.demo@email.com', + APPLE_DEMO_PASSWORD: 'dummy_password', + CLOUDFLARE_TOKEN: 'dummy_cloudflare_token', + }, + 'dummy_github_token', + { + AS_REPO: 'App', + }, + ); + act = utils.setJobRunners( + act, + { + desktop: 'ubuntu-latest', + iOS: 'ubuntu-latest', + android: 'ubuntu-latest', + web: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + validateActor: mocks.PLATFORM_DEPLOY__VALIDATE_ACTOR__OUTSIDER__STEP_MOCKS, + deployChecklist: mocks.PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS, + android: mocks.PLATFORM_DEPLOY__ANDROID__STEP_MOCKS, + desktop: mocks.PLATFORM_DEPLOY__DESKTOP__STEP_MOCKS, + iOS: mocks.PLATFORM_DEPLOY__IOS__STEP_MOCKS, + web: mocks.PLATFORM_DEPLOY__WEB__STEP_MOCKS, + postSlackMessageOnFailure: mocks.PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS, + postSlackMessageOnSuccess: mocks.PLATFORM_DEPLOY__POST_SLACK_SUCCESS__STEP_MOCKS, + postGithubComment: mocks.PLATFORM_DEPLOY__POST_GITHUB_COMMENT__STEP_MOCKS, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'platformDeploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('platformDeploy', expect.getState().currentTestName), + }); + + assertions.assertVerifyActorJobExecuted(result, 'OSBotify'); + assertions.assertDeployChecklistJobExecuted(result, true); + assertions.assertAndroidJobExecuted(result, true, false, true); + assertions.assertDesktopJobExecuted(result, true, false); + assertions.assertIOSJobExecuted(result, true, false, true); + assertions.assertWebJobExecuted(result, true, false); + assertions.assertPostSlackOnFailureJobExecuted(result, false); + assertions.assertPostSlackOnSuccessJobExecuted(result, true, false); + assertions.assertPostGithubCommentJobExecuted(result, true, false); + }); + + it('as outsider - platform deploy does not execute', async () => { + const repoPath = mockGithub.repo.getPath('testPlatformDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'platformDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + { + ref: 'refs/tags/1.2.3', + ref_type: 'tag', + ref_name: '1.2.3', + }, + { + OS_BOTIFY_TOKEN: 'dummy_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + MYAPP_UPLOAD_STORE_PASSWORD: 'dummy_store_password', + MYAPP_UPLOAD_KEY_PASSWORD: 'dummy_key_password', + BROWSERSTACK: 'dummy_browserstack', + SLACK_WEBHOOK: 'dummy_slack_webhook', + DEVELOPER_ID_SECRET_PASSPHRASE: 'dummy_secret_passphrase', + CSC_LINK: 'dummy_csc_link', + CSC_KEY_PASSWORD: 'dummy_csc_key_pass', + APPLE_ID: 'dummy_apple_id', + APPLE_ID_PASSWORD: 'dummy_apple_pass', + AWS_ACCESS_KEY_ID: 'dummy_aws_access_key_id', + AWS_SECRET_ACCESS_KEY: 'dummy_aws_secret_access_key', + APPLE_CONTACT_EMAIL: 'dummy@email.com', + APPLE_CONTACT_PHONE: '123456789', + APPLE_DEMO_EMAIL: 'dummy.demo@email.com', + APPLE_DEMO_PASSWORD: 'dummy_password', + CLOUDFLARE_TOKEN: 'dummy_cloudflare_token', + }, + 'dummy_github_token', + { + AS_REPO: 'App', + }, + ); + act = utils.setJobRunners( + act, + { + desktop: 'ubuntu-latest', + iOS: 'ubuntu-latest', + android: 'ubuntu-latest', + web: 'ubuntu-latest', + }, + workflowPath, + ); + const testMockSteps = { + validateActor: mocks.PLATFORM_DEPLOY__VALIDATE_ACTOR__OUTSIDER__STEP_MOCKS, + deployChecklist: mocks.PLATFORM_DEPLOY__DEPLOY_CHECKLIST__STEP_MOCKS, + android: mocks.PLATFORM_DEPLOY__ANDROID__STEP_MOCKS, + desktop: mocks.PLATFORM_DEPLOY__DESKTOP__STEP_MOCKS, + iOS: mocks.PLATFORM_DEPLOY__IOS__STEP_MOCKS, + web: mocks.PLATFORM_DEPLOY__WEB__STEP_MOCKS, + postSlackMessageOnFailure: mocks.PLATFORM_DEPLOY__POST_SLACK_FAIL__STEP_MOCKS, + postSlackMessageOnSuccess: mocks.PLATFORM_DEPLOY__POST_SLACK_SUCCESS__STEP_MOCKS, + postGithubComment: mocks.PLATFORM_DEPLOY__POST_GITHUB_COMMENT__STEP_MOCKS, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'platformDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Author', + logFile: utils.getLogFilePath('platformDeploy', expect.getState().currentTestName), + }); + + assertions.assertVerifyActorJobExecuted(result, 'Dummy Author'); + assertions.assertDeployChecklistJobExecuted(result, true); + assertions.assertAndroidJobExecuted(result, false); + assertions.assertDesktopJobExecuted(result, false); + assertions.assertIOSJobExecuted(result, false); + assertions.assertWebJobExecuted(result, false); + assertions.assertPostSlackOnFailureJobExecuted(result, false); + assertions.assertPostSlackOnSuccessJobExecuted(result, false); + assertions.assertPostGithubCommentJobExecuted(result, true, false, false); + }); + }); + }); +}); diff --git a/workflow_tests/preDeploy.test.js b/workflow_tests/preDeploy.test.js new file mode 100644 index 000000000000..4a4d9dcc82bb --- /dev/null +++ b/workflow_tests/preDeploy.test.js @@ -0,0 +1,1087 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/preDeployAssertions'); +const mocks = require('./mocks/preDeployMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'preDeploy.yml'), + dest: '.github/workflows/preDeploy.yml', + }, +]; + +describe('test workflow preDeploy', () => { + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testPreDeployWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + pushedBranches: ['different_branch'], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + it('push to main - workflow executes', async () => { + // get path to the local test repo + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + + // get path to the workflow file under test + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + + // instantiate Act in the context of the test repo and given workflow file + let act = new eAct.ExtendedAct(repoPath, workflowPath); + + // set run parameters + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + + // set up mocks + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + + // run an event and get the result + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + + // assert results (some steps can run in parallel to each other so the order is not assured + // therefore we can check which steps have been executed, but not the set job order + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertUpdateStagingJobExecuted(result); + }); + + it('different event than push - workflow does not execute', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + + // pull_request + act = utils.setUpActParams( + act, + 'pull_request', + {head: {ref: 'main'}}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + let result = await act.runEvent('pull_request', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result, false); + assertions.assertLintJobExecuted(result, false); + assertions.assertTestJobExecuted(result, false); + assertions.assertIsExpensifyEmployeeJobExecuted(result, false); + assertions.assertChooseDeployActionsJobExecuted(result, false); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + + // workflow_dispatch + act = utils.setUpActParams( + act, + 'workflow_dispatch', + {}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + result = await act.runEvent('workflow_dispatch', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result, false); + assertions.assertLintJobExecuted(result, false); + assertions.assertTestJobExecuted(result, false); + assertions.assertIsExpensifyEmployeeJobExecuted(result, false); + assertions.assertChooseDeployActionsJobExecuted(result, false); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + + describe('confirm passing build', () => { + it('typecheck job failed - workflow exits', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: [utils.createMockStep('Run typecheck workflow', 'Running typecheck workflow - Typecheck workflow failed', 'TYPECHECK', null, null, null, null, false)], + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + expect(result).toEqual( + expect.arrayContaining([utils.createStepAssertion('Run typecheck workflow', false, null, 'TYPECHECK', 'Running typecheck workflow - Typecheck workflow failed')]), + ); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + expect(result).toEqual( + expect.arrayContaining([ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CONFIRM_PASSING_BUILD', 'Announcing failed workflow in slack', [ + {key: 'SLACK_WEBHOOK', value: '***'}, + ]), + utils.createStepAssertion('Exit failed workflow', false, ''), + ]), + ); + assertions.assertChooseDeployActionsJobExecuted(result, false); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + + it('lint job failed - workflow exits', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: [utils.createMockStep('Run lint workflow', 'Running lint workflow - Lint workflow failed', 'LINT', null, null, null, null, false)], + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + expect(result).toEqual(expect.arrayContaining([utils.createStepAssertion('Run lint workflow', false, null, 'LINT', 'Running lint workflow - Lint workflow failed')])); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + expect(result).toEqual( + expect.arrayContaining([ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CONFIRM_PASSING_BUILD', 'Announcing failed workflow in slack', [ + {key: 'SLACK_WEBHOOK', value: '***'}, + ]), + utils.createStepAssertion('Exit failed workflow', false, ''), + ]), + ); + assertions.assertChooseDeployActionsJobExecuted(result, false); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + + it('test job failed - workflow exits', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: [utils.createMockStep('Run test workflow', 'Running test workflow - Test workflow failed', 'TEST', null, null, null, null, false)], + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + expect(result).toEqual(expect.arrayContaining([utils.createStepAssertion('Run test workflow', false, null, 'TEST', 'Running test workflow - Test workflow failed')])); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + expect(result).toEqual( + expect.arrayContaining([ + utils.createStepAssertion('Announce failed workflow in Slack', true, null, 'CONFIRM_PASSING_BUILD', 'Announcing failed workflow in slack', [ + {key: 'SLACK_WEBHOOK', value: '***'}, + ]), + utils.createStepAssertion('Exit failed workflow', false, ''), + ]), + ); + assertions.assertChooseDeployActionsJobExecuted(result, false); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + }); + + it('lint and test job succeed - workflow continues', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertUpdateStagingJobExecuted(result); + }); + }); + + describe('new contributor welcome message', () => { + it('actor is OSBotify - no comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertNewContributorWelcomeMessageJobExecuted(result, false); + }); + + it('actor is Expensify employee - no comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertNewContributorWelcomeMessageJobExecuted(result, false); + }); + + it('actor is not Expensify employee, its not their first PR - job triggers, but no comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertNewContributorWelcomeMessageJobExecuted(result, true, false, false); + }); + + it('actor is not Expensify employee, and its their first PR - job triggers and comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__FALSE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__ONE_PR, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertNewContributorWelcomeMessageJobExecuted(result, true, false, true); + }); + }); + + describe('choose deploy actions', () => { + describe('staging locked', () => { + it('not automated PR - deploy skipped and comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, 'push', {ref: 'refs/heads/main'}, {OS_BOTIFY_TOKEN: 'dummy_token', SLACK_WEBHOOK: 'dummy_slack_webhook'}, 'dummy_github_token'); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + assertions.assertUpdateStagingJobFailed(result, false); + }); + + it('automated PR - deploy skipped, but no comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, 'push', {ref: 'refs/heads/main'}, {OS_BOTIFY_TOKEN: 'dummy_token', SLACK_WEBHOOK: 'dummy_slack_webhook'}, 'dummy_github_token'); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_LOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + assertions.assertUpdateStagingJobFailed(result, false); + }); + }); + + describe('staging not locked', () => { + it('not automated PR - proceed with deploy', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertUpdateStagingJobExecuted(result, true); + assertions.assertUpdateStagingJobFailed(result, false); + }); + + it('automated PR - deploy skipped, but no comment left', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__OSBOTIFY, + }; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'OSBotify', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result, false); + assertions.assertUpdateStagingJobExecuted(result, false); + assertions.assertUpdateStagingJobFailed(result, false); + }); + }); + + it('one of updateStaging steps failed - failure announced in Slack', async () => { + const repoPath = mockGithub.repo.getPath('testPreDeployWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + 'push', + {ref: 'refs/heads/main'}, + { + OS_BOTIFY_TOKEN: 'dummy_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + }, + 'dummy_github_token', + ); + const testMockSteps = { + confirmPassingBuild: mocks.CONFIRM_PASSING_BUILD_JOB_MOCK_STEPS, + chooseDeployActions: mocks.CHOOSE_DEPLOY_ACTIONS_JOB_MOCK_STEPS__STAGING_UNLOCKED, + skipDeploy: mocks.SKIP_DEPLOY_JOB_MOCK_STEPS, + updateStaging: mocks.UPDATE_STAGING_JOB_MOCK_STEPS, + isExpensifyEmployee: mocks.IS_EXPENSIFY_EMPLOYEE_JOB_MOCK_STEPS__TRUE, + newContributorWelcomeMessage: mocks.NEW_CONTRIBUTOR_WELCOME_MESSAGE_JOB_MOCK_STEPS__MANY_PRS, + }; + testMockSteps.updateStaging[3].mockWith = 'exit 1'; + const testMockJobs = { + typecheck: { + steps: mocks.TYPECHECK_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + lint: { + steps: mocks.LINT_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + test: { + steps: mocks.TEST_JOB_MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + createNewVersion: { + steps: mocks.CREATE_NEW_VERSION_JOB_MOCK_STEPS, + outputs: { + // eslint-disable-next-line no-template-curly-in-string + NEW_VERSION: '${{ steps.createNewVersion.outputs.NEW_VERSION }}', + }, + runsOn: 'ubuntu-latest', + }, + e2ePerformanceTests: { + steps: mocks.PREDEPLOY__E2EPERFORMANCETESTS__MOCK_STEPS, + runsOn: 'ubuntu-latest', + }, + }; + const result = await act.runEvent('push', { + workflowFile: path.join(repoPath, '.github', 'workflows', 'preDeploy.yml'), + mockSteps: testMockSteps, + actor: 'Dummy Tester', + logFile: utils.getLogFilePath('preDeploy', expect.getState().currentTestName), + mockJobs: testMockJobs, + }); + assertions.assertTypecheckJobExecuted(result); + assertions.assertLintJobExecuted(result); + assertions.assertTestJobExecuted(result); + assertions.assertIsExpensifyEmployeeJobExecuted(result); + assertions.assertChooseDeployActionsJobExecuted(result); + assertions.assertSkipDeployJobExecuted(result, false); + assertions.assertCreateNewVersionJobExecuted(result); + assertions.assertUpdateStagingJobFailed(result, true); + }); + }); +}); diff --git a/workflow_tests/reviewerChecklist.test.js b/workflow_tests/reviewerChecklist.test.js new file mode 100644 index 000000000000..9903c3eb4b8d --- /dev/null +++ b/workflow_tests/reviewerChecklist.test.js @@ -0,0 +1,84 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/reviewerChecklistAssertions'); +const mocks = require('./mocks/reviewerChecklistMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'reviewerChecklist.yml'), + dest: '.github/workflows/reviewerChecklist.yml', + }, +]; + +describe('test workflow reviewerChecklist', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testReviewerChecklistWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('event is pull_request_review', () => { + const event = 'pull_request_review'; + const eventOptions = {}; + it('runs the workflow', async () => { + const repoPath = mockGithub.repo.getPath('testReviewerChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'reviewerChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'reviewerChecklist.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('reviewerChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result); + }); + describe('actor is OSBotify', () => { + const osbotifyActor = 'OSBotify'; + it('does not run the workflow', async () => { + const repoPath = mockGithub.repo.getPath('testReviewerChecklistWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'reviewerChecklist.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + checklist: mocks.REVIEWERCHECKLIST__CHECKLIST__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'reviewerChecklist.yml'), + mockSteps: testMockSteps, + actor: osbotifyActor, + logFile: utils.getLogFilePath('reviewerChecklist', expect.getState().currentTestName), + }); + + assertions.assertChecklistJobExecuted(result, false); + }); + }); + }); +}); diff --git a/workflow_tests/scripts/runWorkflowTests.sh b/workflow_tests/scripts/runWorkflowTests.sh new file mode 100755 index 000000000000..71ddcdceffb5 --- /dev/null +++ b/workflow_tests/scripts/runWorkflowTests.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +source ./scripts/shellUtils.sh + +title 'GitHub Actions workflow tests' +printf '\n' + +# Check setup +info 'Checking environment setup' + +# Check if docker is installed +if ! docker --version > /dev/null 2>&1; then + error 'Docker is not installed' + info 'Act requires docker to be installed. Please install docker and try again' + exit 1 +fi +info 'Docker installed' + +# Check if docker is running +if ! docker info > /dev/null 2>&1; then + error 'Docker is not running' + info 'Act requires docker engine to be running. Enable docker engine and try again' + exit 1 +fi +info 'Docker engine running' + +# Check if act is installed +if ! act --version > /dev/null 2>&1; then + error 'Act not installed' + info 'Install Act with brew install act and follow the documentation on first Act run (https://github.com/nektos/act#first-act-run)' + exit 1 +fi +info 'Act installed' + +# Check if ACT_BINARY is set +if [[ -z ${ACT_BINARY} ]]; then + info 'ACT_BINARY not set, checking .env file' + if [ -f .env ]; then + set -a + source .env + set +a + else + info '.env file does not exist' + fi + if [[ -z ${ACT_BINARY} ]]; then + error 'ACT_BINARY variable not set' + info 'To make sure Act behaves in a predictable manner please set the ACT_BINARY environment variable to the path to your Act binary' + exit 1 + fi +fi +info 'ACT_BINARY environment variable set' + +if ! eval '${ACT_BINARY} --version' > /dev/null 2>&1; then + error 'ACT_BINARY variable not set properly' + info 'ACT_BINARY environment variable should be set to the path to your Act executable. Please set the variable correctly (try running "which act" to check the path)' + exit 1 +fi +info 'ACT_BINARY environment variable set to an Act executable' + +success 'Environment setup properly - running tests' + +# Run tests +npm test -- --config=workflow_tests/jest.config.js --runInBand "$@" diff --git a/workflow_tests/test.test.js b/workflow_tests/test.test.js new file mode 100644 index 000000000000..6efe8d260928 --- /dev/null +++ b/workflow_tests/test.test.js @@ -0,0 +1,184 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/testAssertions'); +const mocks = require('./mocks/testMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'test.yml'), + dest: '.github/workflows/test.yml', + }, +]; + +describe('test workflow test', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + const osbotifyActor = 'OSBotify'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testTestWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + + describe('pull request opened', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'opened', + }; + it('runs all tests', async () => { + const repoPath = mockGithub.repo.getPath('testTestWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'test.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + jest: mocks.TEST__JEST__STEP_MOCKS, + shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('test', expect.getState().currentTestName), + }); + + assertions.assertJestJobExecuted(result); + assertions.assertShellTestsJobExecuted(result); + }); + describe('actor is OSBotify', () => { + it('does not run tests', async () => { + const repoPath = mockGithub.repo.getPath('testTestWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'test.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + jest: mocks.TEST__JEST__STEP_MOCKS, + shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), + mockSteps: testMockSteps, + actor: osbotifyActor, + logFile: utils.getLogFilePath('test', expect.getState().currentTestName), + }); + + assertions.assertJestJobExecuted(result, false); + assertions.assertShellTestsJobExecuted(result, false); + }); + }); + }); + + describe('pull request synchronized', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'synchronize', + }; + it('runs all tests', async () => { + const repoPath = mockGithub.repo.getPath('testTestWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'test.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + jest: mocks.TEST__JEST__STEP_MOCKS, + shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('test', expect.getState().currentTestName), + }); + + assertions.assertJestJobExecuted(result); + assertions.assertShellTestsJobExecuted(result); + }); + describe('actor is OSBotify', () => { + it('does not run tests', async () => { + const repoPath = mockGithub.repo.getPath('testTestWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'test.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + jest: mocks.TEST__JEST__STEP_MOCKS, + shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), + mockSteps: testMockSteps, + actor: osbotifyActor, + logFile: utils.getLogFilePath('test', expect.getState().currentTestName), + }); + + assertions.assertJestJobExecuted(result, false); + assertions.assertShellTestsJobExecuted(result, false); + }); + }); + }); + + describe('event is workflow_call', () => { + const event = 'workflow_call'; + const eventOptions = {}; + it('runs all tests', async () => { + const repoPath = mockGithub.repo.getPath('testTestWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'test.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + jest: mocks.TEST__JEST__STEP_MOCKS, + shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('test', expect.getState().currentTestName), + }); + + assertions.assertJestJobExecuted(result); + assertions.assertShellTestsJobExecuted(result); + }); + describe('actor is OSBotify', () => { + it('runs all tests normally', async () => { + const repoPath = mockGithub.repo.getPath('testTestWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'test.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + jest: mocks.TEST__JEST__STEP_MOCKS, + shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), + mockSteps: testMockSteps, + actor: osbotifyActor, + logFile: utils.getLogFilePath('test', expect.getState().currentTestName), + }); + + assertions.assertJestJobExecuted(result); + assertions.assertShellTestsJobExecuted(result); + }); + }); + }); +}); diff --git a/workflow_tests/testBuild.test.js b/workflow_tests/testBuild.test.js new file mode 100644 index 000000000000..4288ea236ab2 --- /dev/null +++ b/workflow_tests/testBuild.test.js @@ -0,0 +1,753 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/testBuildAssertions'); +const mocks = require('./mocks/testBuildMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'testBuild.yml'), + dest: '.github/workflows/testBuild.yml', + }, +]; + +describe('test workflow testBuild', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + const secrets = { + OS_BOTIFY_TOKEN: 'dummy_osbotify_token', + LARGE_SECRET_PASSPHRASE: '3xtr3m3ly_53cr3t_p455w0rd', + AWS_ACCESS_KEY_ID: 'dummy_aws_access_kry_id', + AWS_SECRET_ACCESS_KEY: 'dummy_aws_secret_access_key', + DEVELOPER_ID_SECRET_PASSPHRASE: 'dummy_developer_id_secret_passphrase', + CSC_LINK: 'dummy_csc_link', + CSC_KEY_PASSWORD: 'dummy_csc_key_password', + APPLE_ID_PASSWORD: 'dummy_apple_id_password', + APPLE_ID: 'dummy_apple_id_value', + MYAPP_UPLOAD_STORE_PASSWORD: 'dummy_myapp_upload_store_password', + MYAPP_UPLOAD_KEY_PASSWORD: 'dummy_myapp_upload_key_password', + }; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testTestBuildWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('event is workflow_dispatch', () => { + const event = 'workflow_dispatch'; + const inputs = { + PULL_REQUEST_NUMBER: '1234', + }; + it('executes workflow', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref'); + }); + describe('actor is not a team member', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('android fails', () => { + it('executes workflow, failure reflected', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: utils.deepCopy(mocks.TESTBUILD__ANDROID__STEP_MOCKS), + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + testMockSteps.android[4] = utils.createMockStep('Decrypt keystore', 'Decrypt keystore', 'ANDROID', [], ['LARGE_SECRET_PASSPHRASE'], {}, {}, false); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result); + assertions.assertAndroidJobExecuted(result, 'test-ref', true, 4); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', true, 'failure', 'success', 'success', 'success'); + }); + }); + describe('iOS fails', () => { + it('executes workflow, failure reflected', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: utils.deepCopy(mocks.TESTBUILD__IOS__STEP_MOCKS), + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + testMockSteps.iOS[8] = utils.createMockStep('Install cocoapods', 'Install cocoapods', 'IOS', ['timeout_minutes', 'max_attempts', 'command'], [], {}, {}, false); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref', true, 8); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', true, 'success', 'failure', 'success', 'success'); + }); + }); + describe('desktop fails', () => { + it('executes workflow, failure reflected', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: utils.deepCopy(mocks.TESTBUILD__DESKTOP__STEP_MOCKS), + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + testMockSteps.desktop[3] = utils.createMockStep( + 'Decrypt Developer ID Certificate', + 'Decrypt Developer ID Certificate', + 'DESKTOP', + [], + ['DEVELOPER_ID_SECRET_PASSPHRASE'], + {}, + {}, + false, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref', true, 3); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', true, 'success', 'success', 'failure', 'success'); + }); + }); + describe('web fails', () => { + it('executes workflow, failure reflected', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: utils.deepCopy(mocks.TESTBUILD__WEB__STEP_MOCKS), + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + testMockSteps.web[3] = utils.createMockStep( + 'Configure AWS Credentials', + 'Configure AWS Credentials', + 'WEB', + ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'], + [], + {}, + {}, + false, + ); + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref', true, 3); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', true, 'success', 'success', 'success', 'failure'); + }); + }); + }); + describe('pull request opened', () => { + const event = 'pull_request_target'; + const eventOptions = { + action: 'opened', + number: '1234', + pull_request: { + head: { + sha: 'test-ref', + }, + }, + }; + it('executes workflow, without getBranchRef', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref'); + }); + describe('actor is not a team member', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + }); + describe('pull request synchronized', () => { + const event = 'pull_request_target'; + const eventOptions = { + action: 'synchronize', + number: '1234', + pull_request: { + head: { + sha: 'test-ref', + }, + }, + }; + it('executes workflow, without getBranchRef', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref'); + }); + describe('actor is not a team member', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + }); + describe('pull request labeled', () => { + const event = 'pull_request_target'; + const eventOptions = { + action: 'labeled', + number: '1234', + pull_request: { + head: { + sha: 'test-ref', + }, + }, + }; + it('executes workflow, withuout getBranchRef', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref'); + assertions.assertIOSJobExecuted(result, 'test-ref'); + assertions.assertDesktopJobExecuted(result, 'test-ref'); + assertions.assertWebJobExecuted(result, 'test-ref'); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref'); + }); + describe('actor is not a team member', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => { + it('stops the workflow after validation', async () => { + const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {}); + act = utils.setJobRunners(act, {iOS: 'ubuntu-latest', desktop: 'ubuntu-latest', web: 'ubuntu-latest', android: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + validateActor: mocks.TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_NO_FLAG__STEP_MOCKS, + getBranchRef: mocks.TESTBUILD__GETBRANCHREF__STEP_MOCKS, + android: mocks.TESTBUILD__ANDROID__STEP_MOCKS, + iOS: mocks.TESTBUILD__IOS__STEP_MOCKS, + desktop: mocks.TESTBUILD__DESKTOP__STEP_MOCKS, + web: mocks.TESTBUILD__WEB__STEP_MOCKS, + postGithubComment: mocks.TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'testBuild.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('testBuild', expect.getState().currentTestName), + }); + + assertions.assertValidateActorJobExecuted(result, '1234'); + assertions.assertGetBranchRefJobExecuted(result, false); + assertions.assertAndroidJobExecuted(result, 'test-ref', false); + assertions.assertIOSJobExecuted(result, 'test-ref', false); + assertions.assertDesktopJobExecuted(result, 'test-ref', false); + assertions.assertWebJobExecuted(result, 'test-ref', false); + assertions.assertPostGithubCommentJobExecuted(result, 'test-ref', '1234', false); + }); + }); + }); +}); diff --git a/workflow_tests/utils/ExtendedAct.js b/workflow_tests/utils/ExtendedAct.js new file mode 100644 index 000000000000..9b4ab1bebda2 --- /dev/null +++ b/workflow_tests/utils/ExtendedAct.js @@ -0,0 +1,44 @@ +const kieActJs = require('@kie/act-js'); +const path = require('path'); +const _ = require('underscore'); +const JobMocker = require('./JobMocker'); + +class ExtendedAct extends kieActJs.Act { + async parseRunOpts(opts) { + const {cwd, actArguments, proxy} = await super.parseRunOpts(opts); + if (opts && opts.actor) { + actArguments.push('--actor', opts.actor); + } + return {cwd, actArguments, proxy}; + } + + async runEvent(event, opts) { + const {mockJobs, ...vanillaOpts} = opts; + if (mockJobs) { + await this.handleJobMocking((workflow) => workflow.events.includes(event), {mockJobs, workflowFile: opts.workflowFile, cwd: opts.cwd}); + } + return super.runEvent(event, vanillaOpts); + } + + async handleJobMocking(filter, opts) { + let workflowFiles; + if (opts.workflowFile) { + workflowFiles = [path.basename(opts.workflowFile)]; + } else if (this.workflowFile !== this.cwd) { + workflowFiles = [path.basename(this.workflowFile)]; + } else { + workflowFiles = _(_(await this.list(undefined, opts.cwd, opts.workflowFile)).filter(filter)).map((l) => l.workflowFile); + } + return Promise.all( + _(workflowFiles).map((workflowFile) => { + // eslint-disable-next-line es/no-nullish-coalescing-operators + const jobMocker = new JobMocker.JobMocker(workflowFile, opts.cwd ?? this.cwd); + return jobMocker.mock(opts.mockJobs); + }), + ); + } +} + +module.exports = { + ExtendedAct, +}; diff --git a/workflow_tests/utils/JobMocker.js b/workflow_tests/utils/JobMocker.js new file mode 100644 index 000000000000..a2682a657380 --- /dev/null +++ b/workflow_tests/utils/JobMocker.js @@ -0,0 +1,83 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('yaml'); +const _ = require('underscore'); + +class JobMocker { + constructor(workflowFile, cwd) { + this.workflowFile = workflowFile; + this.cwd = cwd; + } + + async mock(mockJobs) { + const filePath = this.getWorkflowPath(); + const workflow = await this.readWorkflowFile(filePath); + Object.entries(mockJobs).forEach(([jobId, mockJob]) => { + const job = this.locateJob(workflow, jobId); + if (job) { + if (job.uses) { + delete job.uses; + } + if (job.secrets) { + delete job.secrets; + } + let jobWith; + if (job.with) { + jobWith = job.with; + delete job.with; + } + job.steps = _(mockJob.steps).map((step) => { + const mockStep = { + name: step.name, + run: step.mockWith, + }; + if (step.id) { + mockStep.id = step.id; + } + if (jobWith) { + mockStep.with = jobWith; + } + return mockStep; + }); + if (mockJob.outputs) { + job.outputs = mockJob.outputs; + } + if (mockJob.runsOn) { + job['runs-on'] = mockJob.runsOn; + } + } else { + throw new Error('Could not find job'); + } + }); + return this.writeWorkflowFile(filePath, workflow); + } + + locateJob(workflow, jobId) { + return workflow.jobs[jobId]; + } + + getWorkflowPath() { + if (fs.existsSync(path.join(this.cwd, this.workflowFile))) { + return path.join(this.cwd, this.workflowFile); + } + if (this.cwd.endsWith('.github')) { + return path.join(this.cwd, 'workflows', this.workflowFile); + } + if (fs.existsSync(path.join(this.cwd, '.github', 'workflows', this.workflowFile))) { + return path.join(this.cwd, '.github', 'workflows', this.workflowFile); + } + throw new Error(`Could not locate ${this.workflowFile}`); + } + + async readWorkflowFile(location) { + return yaml.parse(fs.readFileSync(location, 'utf8')); + } + + async writeWorkflowFile(location, data) { + return fs.writeFileSync(location, yaml.stringify(data), 'utf8'); + } +} + +module.exports = { + JobMocker, +}; diff --git a/workflow_tests/utils/preGenerateTest.js b/workflow_tests/utils/preGenerateTest.js new file mode 100644 index 000000000000..4ed485abec40 --- /dev/null +++ b/workflow_tests/utils/preGenerateTest.js @@ -0,0 +1,281 @@ +/* eslint no-console: ["error", { allow: ["warn", "log"] }] */ +const path = require('path'); +const {exit} = require('process'); +const fs = require('fs'); +const yaml = require('yaml'); +const _ = require('underscore'); + +const workflowsDirectory = path.resolve(__dirname, '..', '..', '.github', 'workflows'); +const workflowTestsDirectory = path.resolve(__dirname, '..'); +const workflowTestMocksDirectory = path.join(workflowTestsDirectory, 'mocks'); +const workflowTestAssertionsDirectory = path.join(workflowTestsDirectory, 'assertions'); +const workflowFilePattern = '\\w+\\.yml'; +const workflowFileRegex = new RegExp(workflowFilePattern, 'g'); + +const capitalize = (s) => (s && s.charAt(0).toUpperCase() + s.slice(1)) || ''; +const mockFileTemplate = (mockSteps, exports) => `const utils = require('../utils/utils'); +${mockSteps} +${exports} +`; +const assertionFileTemplate = (jobAssertions, exports) => `const utils = require('../utils/utils'); +${jobAssertions} +${exports} +`; +const testFileTemplate = (workflowName) => `const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/${workflowName}Assertions'); +const mocks = require('./mocks/${workflowName}Mocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + { + src: path.resolve(__dirname, '..', '.github', 'actions'), + dest: '.github/actions', + }, + { + src: path.resolve(__dirname, '..', '.github', 'libs'), + dest: '.github/libs', + }, + { + src: path.resolve(__dirname, '..', '.github', 'scripts'), + dest: '.github/scripts', + }, + { + src: path.resolve(__dirname, '..', '.github', 'workflows', '${workflowName}.yml'), + dest: '.github/workflows/${workflowName}.yml', + }, +]; + +describe('test workflow ${workflowName}', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + test${capitalize(workflowName)}WorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + + // if any branches besides main are need add: pushedBranches: ['staging', 'production'], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + it('test stub', async () => { + const repoPath = mockGithub.repo.getPath('test${capitalize(workflowName)}WorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', '${workflowName}.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + '[EVENT]', + {}, + {}, + githubToken, + ); + const testMockSteps = { + // mock steps with imported mocks + }; + const result = await act + .runEvent('[EVENT]', { + workflowFile: path.join(repoPath, '.github', 'workflows'), + mockSteps: testMockSteps, + actor, + }); + + // assert execution with imported assertions + }); +}); +`; +const mockStepTemplate = (stepMockName, step, jobId) => ` +const ${stepMockName} = utils.getMockStep( + '${step.name || ''}', + '${step.name || ''}', + ${jobId ? `'${jobId.toUpperCase()}'` : 'null'}, + ${step.inputs ? JSON.stringify(step.inputs).replaceAll('"', "'") : 'null'}, + ${step.envs ? JSON.stringify(step.envs).replaceAll('"', "'") : 'null'}, + // add outputs if needed +);`; +const stepAssertionTemplate = (step_name, job_id, step_message, inputs, envs) => ` + utils.getStepAssertion( + '${step_name}', + true, + null, + '${job_id}', + '${step_message}', + [${_(inputs).map((input) => `{key: '${input}', value: '[FILL_IN]'}`)}], + [${_(envs).map((env) => `{key: '${env}', value: '[FILL_IN]'}`)}], + ),`; +const jobMocksTemplate = (jobMocksName, stepMocks) => ` +const ${jobMocksName} = [${_(stepMocks).map( + (stepMock) => ` + ${stepMock}`, +)} +];`; +const jobAssertionTemplate = (jobAssertionName, stepAssertions) => ` +const ${jobAssertionName} = (workflowResult, didExecute = true) => { + const steps = [${stepAssertions} + ]; + + for (const expectedStep of steps) { + if (didExecute) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + } +};`; +const mocksExportsTemplate = (jobMocks) => ` +module.exports = { + ${_(jobMocks).map((jobMock) => `${jobMock}`)} +};`; +const assertionsExportsTemplate = (jobAssertions) => ` +module.exports = { + ${_(jobAssertions).map((jobAssertion) => `${jobAssertion}`)} +};`; + +const checkArguments = (args) => { + if (args.length > 0 && args[0]) { + return; + } + console.warn('Please provide workflow file name'); + exit(1); +}; +const checkWorkflowFileName = (fileName) => { + if (workflowFileRegex.test(fileName)) { + return; + } + console.warn(`Please provide a valid workflow file name ([workflow].yml) instead of ${fileName}`); + exit(1); +}; +const checkWorkflowFilePath = (filePath) => { + if (fs.existsSync(filePath)) { + return; + } + console.warn(`Provided workflow file does not exist: ${filePath}`); + exit(1); +}; +const checkIfTestFileExists = (testsDirectory, testFileName) => { + if (!fs.existsSync(path.join(testsDirectory, testFileName))) { + return; + } + console.warn(`The test file ${testFileName} already exists, exiting`); + exit(1); +}; +const checkIfMocksFileExists = (mocksDirectory, mocksFileName) => { + if (!fs.existsSync(path.join(mocksDirectory, mocksFileName))) { + return; + } + console.warn(`The mocks file ${mocksFileName} already exists, exiting`); + exit(1); +}; +const checkIfAssertionsFileExists = (assertionsDirectory, assertionsFileName) => { + if (!fs.existsSync(path.join(assertionsDirectory, assertionsFileName))) { + return; + } + console.warn(`The assertions file ${assertionsFileName} already exists, exiting`); + exit(1); +}; +const parseWorkflowFile = (workflow) => { + const workflowJobs = {}; + Object.entries(workflow.jobs).forEach(([jobId, job]) => { + workflowJobs[jobId] = { + steps: [], + }; + job.steps.forEach((step) => { + const workflowStep = { + name: step.name || '', + inputs: _.keys(step.with || {}) || [], + envs: _.keys(step.env || {}) || [], + }; + workflowJobs[jobId].steps.push(workflowStep); + }); + }); + return workflowJobs; +}; +const getMockFileContent = (workflowName, jobs) => { + let content = ''; + const jobMocks = []; + Object.entries(jobs).forEach(([jobId, job]) => { + let mockStepsContent = `\n// ${jobId.toLowerCase()}`; + const stepMocks = []; + job.steps.forEach((step) => { + const stepMockName = `${workflowName.toUpperCase()}__${jobId.toUpperCase()}__${step.name + .replaceAll(' ', '_') + .replaceAll('-', '_') + .replaceAll(',', '') + .replaceAll('#', '') + .toUpperCase()}__STEP_MOCK`; + stepMocks.push(stepMockName); + mockStepsContent += mockStepTemplate(stepMockName, step, jobId); + }); + const jobMocksName = `${workflowName.toUpperCase()}__${jobId.toUpperCase()}__STEP_MOCKS`; + jobMocks.push(jobMocksName); + mockStepsContent += jobMocksTemplate(jobMocksName, stepMocks); + content += mockStepsContent; + }); + return mockFileTemplate(content, mocksExportsTemplate(jobMocks)); +}; +const getAssertionsFileContent = (workflowName, jobs) => { + let content = ''; + const jobAssertions = []; + Object.entries(jobs).forEach(([jobId, job]) => { + let stepAssertionsContent = ''; + job.steps.forEach((step) => { + stepAssertionsContent += stepAssertionTemplate(step.name, jobId.toUpperCase(), step.name, step.inputs, step.envs); + }); + const jobAssertionName = `assert${jobId.charAt(0).toUpperCase() + jobId.slice(1)}JobExecuted`; + jobAssertions.push(jobAssertionName); + content += jobAssertionTemplate(jobAssertionName, stepAssertionsContent); + }); + return assertionFileTemplate(content, assertionsExportsTemplate(jobAssertions)); +}; +const getTestFileContent = (workflowName) => testFileTemplate(workflowName); + +const call_args = process.argv.slice(2); +checkArguments(call_args); + +const workflowFileName = call_args[0]; +checkWorkflowFileName(workflowFileName); + +const workflowName = workflowFileName.slice(0, -4); +const workflowFilePath = path.join(workflowsDirectory, workflowFileName); +checkWorkflowFilePath(workflowFilePath); + +const workflowTestFileName = `${workflowName}.test.js`; +checkIfTestFileExists(workflowTestsDirectory, workflowTestFileName); + +const workflowTestMocksFileName = `${workflowName}Mocks.js`; +checkIfMocksFileExists(workflowTestMocksDirectory, workflowTestMocksFileName); + +const workflowTestAssertionsFileName = `${workflowName}Assertions.js`; +checkIfAssertionsFileExists(workflowTestAssertionsDirectory, workflowTestAssertionsFileName); + +const workflow = yaml.parse(fs.readFileSync(workflowFilePath, 'utf8')); +const workflowJobs = parseWorkflowFile(workflow); + +const mockFileContent = getMockFileContent(workflowName, workflowJobs); +const mockFilePath = path.join(workflowTestMocksDirectory, workflowTestMocksFileName); +console.log(`Creating mock file ${mockFilePath}`); +fs.writeFileSync(mockFilePath, mockFileContent); +console.log(`Mock file ${mockFilePath} created`); + +const assertionsFileContent = getAssertionsFileContent(workflowName, workflowJobs); +const assertionsFilePath = path.join(workflowTestAssertionsDirectory, workflowTestAssertionsFileName); +console.log(`Creating assertions file ${assertionsFilePath}`); +fs.writeFileSync(assertionsFilePath, assertionsFileContent); +console.log(`Assertions file ${assertionsFilePath} created`); + +const testFileContent = getTestFileContent(workflowName, workflowJobs); +const testFilePath = path.join(workflowTestsDirectory, workflowTestFileName); +console.log(`Creating test file ${testFilePath}`); +fs.writeFileSync(testFilePath, testFileContent); +console.log(`Test file ${testFilePath} created`); diff --git a/workflow_tests/utils/utils.js b/workflow_tests/utils/utils.js new file mode 100644 index 000000000000..32e106cfb1de --- /dev/null +++ b/workflow_tests/utils/utils.js @@ -0,0 +1,178 @@ +const yaml = require('yaml'); +const fs = require('fs'); +const path = require('path'); + +function setUpActParams(act, event = null, eventOptions = null, secrets = null, githubToken = null, envVars = null, inputs = null) { + let updated_act = act; + + if (event && eventOptions) { + // according to `Act` docs event data should be under the key with the event name (`[event]: eventOptions`), but + // for some event types this does not work (like `issues`), but providing the data on the JSON top level does, + // hence `...eventOptions` - this seems to cover all options + const eventData = { + [event]: eventOptions, + ...eventOptions, + }; + updated_act = updated_act.setEvent(eventData); + } + + if (secrets) { + Object.entries(secrets).forEach(([key, value]) => { + updated_act = updated_act.setSecret(key, value); + }); + } + + if (githubToken) { + updated_act = updated_act.setGithubToken(githubToken); + } + + if (envVars) { + Object.entries(envVars).forEach(([key, value]) => { + updated_act = updated_act.setEnv(key, value); + }); + } + + if (inputs) { + Object.entries(inputs).forEach(([key, value]) => { + updated_act = updated_act.setInput(key, value); + }); + } + + return updated_act; +} + +function createMockStep(name, message, job_id = null, inputs = null, in_envs = null, outputs = null, out_envs = null, isSuccessful = true, id = null) { + const mockStepName = name; + let mockWithCommand = 'echo [MOCK]'; + if (job_id) { + mockWithCommand += ` [${job_id}]`; + } + mockWithCommand += ` ${message}`; + if (inputs) { + inputs.forEach((input) => { + mockWithCommand += `, ${input}="\${{ inputs.${input} && inputs.${input} || github.event.inputs.${input} }}"`; + }); + } + if (in_envs) { + in_envs.forEach((env) => { + mockWithCommand += `, ${env}="\${{ env.${env} }}"`; + }); + } + if (outputs) { + Object.entries(outputs).forEach(([key, value]) => { + mockWithCommand += `\necho "${key}=${value}" >> "$GITHUB_OUTPUT"`; + }); + } + if (out_envs) { + Object.entries(out_envs).forEach(([key, value]) => { + mockWithCommand += `\necho "${key}=${value}" >> "$GITHUB_ENV"`; + }); + } + if (!isSuccessful) { + mockWithCommand += '\nexit 1'; + } + const mockStep = { + name: mockStepName, + mockWith: mockWithCommand, + }; + if (id) { + mockStep.id = id; + } + return mockStep; +} + +function createStepAssertion(name, isSuccessful = true, expectedOutput = null, jobId = null, message = null, inputs = null, envs = null) { + const stepName = `Main ${name}`; + const stepStatus = isSuccessful ? 0 : 1; + let stepOutput; + if (expectedOutput !== undefined && expectedOutput !== null) { + stepOutput = expectedOutput; + } else { + stepOutput = '[MOCK]'; + if (jobId) { + stepOutput += ` [${jobId}]`; + } + if (message) { + stepOutput += ` ${message}`; + } + if (inputs) { + inputs.forEach((input) => { + stepOutput += `, ${input.key}=${input.value}`; + }); + } + if (envs) { + envs.forEach((env) => { + stepOutput += `, ${env.key}=${env.value}`; + }); + } + } + return { + name: stepName, + status: stepStatus, + output: stepOutput, + }; +} + +function setJobRunners(act, jobs, workflowPath) { + if (!act || !jobs || !workflowPath) { + return act; + } + + const workflow = yaml.parse(fs.readFileSync(workflowPath, 'utf8')); + Object.entries(jobs).forEach(([jobId, runner]) => { + const job = workflow.jobs[jobId]; + job['runs-on'] = runner; + }); + fs.writeFileSync(workflowPath, yaml.stringify(workflow), 'utf8'); + return act; +} + +function deepCopy(originalObject) { + return JSON.parse(JSON.stringify(originalObject)); +} + +function getLogFilePath(workflowName, testName) { + const logsDir = path.resolve(__dirname, '..', 'logs'); + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir); + } + const workflowTestsLogDir = path.resolve(logsDir, workflowName); + if (!fs.existsSync(workflowTestsLogDir)) { + fs.mkdirSync(workflowTestsLogDir); + } + const cleanTestName = testName.replace(' ', '_').replace('-', '_').substr(0, 240); + return path.resolve(workflowTestsLogDir, `${cleanTestName}.log`); +} + +function removeMockRepoDir() { + const mockDirRepo = path.resolve(__dirname, '..', '..', 'repo'); + if (fs.existsSync(mockDirRepo)) { + fs.rmSync(mockDirRepo, {recursive: true, force: true}); + } +} + +const FILES_TO_COPY_INTO_TEST_REPO = [ + { + src: path.resolve(__dirname, '..', '..', '.github', 'actions'), + dest: '.github/actions', + }, + { + src: path.resolve(__dirname, '..', '..', '.github', 'libs'), + dest: '.github/libs', + }, + { + src: path.resolve(__dirname, '..', '..', '.github', 'scripts'), + dest: '.github/scripts', + }, +]; + +module.exports = { + setUpActParams, + createMockStep, + createStepAssertion, + setJobRunners, + deepCopy, + getLogFilePath, + FILES_TO_COPY_INTO_TEST_REPO, + removeMockRepoDir, +}; diff --git a/workflow_tests/validateGithubActions.test.js b/workflow_tests/validateGithubActions.test.js new file mode 100644 index 000000000000..dfa5e9362ce7 --- /dev/null +++ b/workflow_tests/validateGithubActions.test.js @@ -0,0 +1,90 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/validateGithubActionsAssertions'); +const mocks = require('./mocks/validateGithubActionsMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'validateGithubActions.yml'), + dest: '.github/workflows/validateGithubActions.yml', + }, +]; + +describe('test workflow validateGithubActions', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testValidateGithubActionsWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('pull request opened', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'opened', + }; + it('executes verification', async () => { + const repoPath = mockGithub.repo.getPath('testValidateGithubActionsWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'validateGithubActions.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + verify: mocks.VALIDATEGITHUBACTIONS__VERIFY__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'validateGithubActions.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('validateGithubActions', expect.getState().currentTestName), + }); + + assertions.assertVerifyJobExecuted(result); + }); + }); + describe('pull request synchronized', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'synchronize', + }; + it('executes verification', async () => { + const repoPath = mockGithub.repo.getPath('testValidateGithubActionsWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'validateGithubActions.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + verify: mocks.VALIDATEGITHUBACTIONS__VERIFY__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'validateGithubActions.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('validateGithubActions', expect.getState().currentTestName), + }); + + assertions.assertVerifyJobExecuted(result); + }); + }); +}); diff --git a/workflow_tests/verifyPodfile.test.js b/workflow_tests/verifyPodfile.test.js new file mode 100644 index 000000000000..de062af2a2c2 --- /dev/null +++ b/workflow_tests/verifyPodfile.test.js @@ -0,0 +1,133 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/verifyPodfileAssertions'); +const mocks = require('./mocks/verifyPodfileMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'verifyPodfile.yml'), + dest: '.github/workflows/verifyPodfile.yml', + }, +]; + +describe('test workflow verifyPodfile', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + const osbotifyActor = 'OSBotify'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testVerifyPodfileWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('pull request opened', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'opened', + }; + it('executes workflow', async () => { + const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + act = utils.setJobRunners(act, {verify: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + verify: mocks.VERIFYPODFILE__VERIFY__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('verifyPodfile', expect.getState().currentTestName), + }); + + assertions.assertVerifyJobExecuted(result); + }); + describe('actor is OSBotify', () => { + it('does not execute workflow', async () => { + const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + act = utils.setJobRunners(act, {verify: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + verify: mocks.VERIFYPODFILE__VERIFY__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'), + mockSteps: testMockSteps, + actor: osbotifyActor, + logFile: utils.getLogFilePath('verifyPodfile', expect.getState().currentTestName), + }); + + assertions.assertVerifyJobExecuted(result, false); + }); + }); + }); + describe('pull request synchronized', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'synchronize', + }; + it('executes workflow', async () => { + const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + act = utils.setJobRunners(act, {verify: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + verify: mocks.VERIFYPODFILE__VERIFY__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('verifyPodfile', expect.getState().currentTestName), + }); + + assertions.assertVerifyJobExecuted(result); + }); + describe('actor is OSBotify', () => { + it('does not execute workflow', async () => { + const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + act = utils.setJobRunners(act, {verify: 'ubuntu-latest'}, workflowPath); + const testMockSteps = { + verify: mocks.VERIFYPODFILE__VERIFY__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml'), + mockSteps: testMockSteps, + actor: osbotifyActor, + logFile: utils.getLogFilePath('verifyPodfile', expect.getState().currentTestName), + }); + + assertions.assertVerifyJobExecuted(result, false); + }); + }); + }); +}); diff --git a/workflow_tests/verifySignedCommits.test.js b/workflow_tests/verifySignedCommits.test.js new file mode 100644 index 000000000000..911208e91f4a --- /dev/null +++ b/workflow_tests/verifySignedCommits.test.js @@ -0,0 +1,90 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/verifySignedCommitsAssertions'); +const mocks = require('./mocks/verifySignedCommitsMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(90 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'verifySignedCommits.yml'), + dest: '.github/workflows/verifySignedCommits.yml', + }, +]; + +describe('test workflow verifySignedCommits', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Actor'; + + beforeAll(async () => { + // in case of the tests being interrupted without cleanup the mock repo directory may be left behind + // which breaks the next test run, this removes any possible leftovers + utils.removeMockRepoDir(); + }); + + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testVerifySignedCommitsWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('pull request opened', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'opened', + }; + it('test stub', async () => { + const repoPath = mockGithub.repo.getPath('testVerifySignedCommitsWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifySignedCommits.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + verifySignedCommits: mocks.VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'verifySignedCommits.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('verifySignedCommits', expect.getState().currentTestName), + }); + + assertions.assertVerifySignedCommitsJobExecuted(result); + }); + }); + describe('pull request synchronized', () => { + const event = 'pull_request'; + const eventOptions = { + action: 'synchronize', + }; + it('test stub', async () => { + const repoPath = mockGithub.repo.getPath('testVerifySignedCommitsWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifySignedCommits.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); + const testMockSteps = { + verifySignedCommits: mocks.VERIFYSIGNEDCOMMITS__VERIFYSIGNEDCOMMITS__STEP_MOCKS, + }; + const result = await act.runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows', 'verifySignedCommits.yml'), + mockSteps: testMockSteps, + actor, + logFile: utils.getLogFilePath('verifySignedCommits', expect.getState().currentTestName), + }); + + assertions.assertVerifySignedCommitsJobExecuted(result); + }); + }); +});