From 606e56d5a4a7a2e243ac85d4650e257aac1ec027 Mon Sep 17 00:00:00 2001 From: "David A. Ramos" Date: Mon, 19 Jun 2023 16:41:18 -0700 Subject: [PATCH] Add Cypress plugin --- .eslintignore | 11 + .github/workflows/ci.yaml | 115 +- .github/workflows/cypress-realworld-app.yaml | 127 + .nvmrc | 1 + .prettierignore | 3 + .yarnrc.yml | 1 + README.md | 15 + package.json | 28 +- packages/cypress-plugin/LICENSE | 21 + packages/cypress-plugin/README.md | 29 + packages/cypress-plugin/jest.config.js | 20 + packages/cypress-plugin/mocha.d.ts | 66 + packages/cypress-plugin/package.json | 94 + packages/cypress-plugin/rollup.config.mjs | 64 + packages/cypress-plugin/src/.eslintrc.js | 27 + .../cypress-plugin/src/config-env-vars.ts | 22 + packages/cypress-plugin/src/config-wrapper.ts | 59 + .../cypress-plugin/src/cypress-env-vars.ts | 8 + packages/cypress-plugin/src/index.ts | 146 + packages/cypress-plugin/src/main.ts | 364 ++ packages/cypress-plugin/src/plugin.ts | 646 +++ .../cypress-plugin/src/reporter-common.ts | 91 + .../src/reporter-config.test.ts | 231 + .../cypress-plugin/src/reporter-config.ts | 227 + packages/cypress-plugin/src/reporter.ts | 900 ++++ packages/cypress-plugin/src/skip-tests.ts | 224 + packages/cypress-plugin/src/tsconfig.json | 4 + packages/cypress-plugin/src/utils.ts | 26 + .../src/vendored/cli-table3.d.ts | 12 + .../cypress-plugin/src/vendored/duration.ts | 86 + .../cypress-plugin/src/vendored/human_time.ts | 102 + .../cypress-plugin/src/vendored/newlines.ts | 48 + .../cypress-plugin/src/vendored/print-run.ts | 364 ++ .../src/vendored/terminal-size.ts | 46 + .../cypress-plugin/src/vendored/terminal.ts | 260 + packages/cypress-plugin/test/.eslintrc.cjs | 9 + packages/cypress-plugin/test/.gitignore | 4 + .../test/integration-common/.eslintrc.js | 5 + .../test/integration-common/package.json | 34 + .../test/integration-common/rollup.config.mjs | 36 + .../test/integration-common/src/config.ts | 60 + .../test/integration-common/src/git.ts | 92 + .../src/mock-cosmiconfig.ts | 8 + .../test/integration-common/src/tsconfig.json | 10 + .../test/integration-common/tsconfig.json | 9 + .../test/integration-input-esm/.eslintrc.cjs | 5 + .../config-js/devtools.js | 39 + .../integration-input-esm/config-js/tasks.js | 16 + .../config-js/webpack.js | 24 + .../integration-input-esm/config/devtools.ts | 31 + .../integration-input-esm/config/tasks.ts | 12 + .../integration-input-esm/config/webpack.ts | 22 + .../integration-input-esm/cypress-config.cjs | 59 + .../integration-input-esm/cypress-config.js | 46 + .../integration-input-esm/cypress.config.ts | 43 + .../integration-input-esm/cypress/component | 1 + .../test/integration-input-esm/cypress/e2e | 1 + .../cypress/support-js/commands.js | 3 + .../cypress/support-js/component.cjs | 10 + .../cypress/support-js/component.js | 11 + .../cypress/support-js/e2e.cjs | 4 + .../cypress/support-js/e2e.js | 4 + .../cypress/support/commands.ts | 20 + .../cypress/support/component-index.html | 12 + .../cypress/support/component.ts | 19 + .../cypress/support/e2e.ts | 3 + .../test/integration-input-esm/package.json | 24 + .../test/integration-input-esm/tsconfig.json | 21 + .../config/devtools.js | 39 + .../integration-input-manual/config/tasks.js | 18 + .../config/webpack.js | 24 + .../cypress-config.mjs | 59 + .../cypress-multi-reporters.config.json | 6 + .../cypress.config.js | 62 + .../cypress/component | 1 + .../cypress/e2e/fail.cy.js | 32 + .../cypress/e2e/flake.cy.js | 31 + .../cypress/e2e/hook-fail.cy.js | 85 + .../cypress/e2e/invalid.cy.js | 5 + .../cypress/e2e/mixed/mixed.cy.js | 36 + .../cypress/e2e/pass.cy.js | 11 + .../cypress/e2e/pending.cy.js | 11 + .../cypress/e2e/quarantined.cy.js | 10 + .../cypress/support/commands.js | 3 + .../cypress/support/component-index.html | 12 + .../cypress/support/component.js | 16 + .../cypress/support/e2e.js | 13 + .../integration-input-manual/package.json | 24 + .../test/integration-input/.eslintrc.js | 5 + .../integration-input/config-js/devtools.js | 39 + .../test/integration-input/config-js/tasks.js | 18 + .../integration-input/config-js/webpack.js | 24 + .../test/integration-input/config/devtools.ts | 31 + .../test/integration-input/config/tasks.ts | 12 + .../test/integration-input/config/webpack.ts | 24 + .../test/integration-input/cypress-config.js | 50 + .../test/integration-input/cypress-config.mjs | 46 + .../test/integration-input/cypress.config.ts | 48 + .../cypress/component/fail.cy.ts | 26 + .../cypress/component/flake.cy.ts | 33 + .../cypress/component/hook-fail.cy.ts | 64 + .../cypress/component/invalid.cy.ts | 5 + .../cypress/component/mixed/mixed.cy.ts | 46 + .../cypress/component/pass.cy.ts | 11 + .../cypress/component/pending.cy.ts | 11 + .../cypress/component/quarantined.cy.ts | 10 + .../test/integration-input/cypress/e2e | 1 + .../cypress/fixtures/example.json | 5 + .../cypress/support-js/commands.js | 3 + .../cypress/support-js/component.js | 10 + .../cypress/support-js/component.mjs | 11 + .../cypress/support-js/e2e.js | 4 + .../cypress/support-js/e2e.mjs | 4 + .../cypress/support/commands.ts | 12 + .../cypress/support/component-index.html | 12 + .../cypress/support/component.ts | 20 + .../integration-input/cypress/support/e2e.ts | 3 + .../test/integration-input/package.json | 23 + .../integration-input/reporter-config.json | 6 + .../test/integration-input/tsconfig.json | 20 + .../test/integration/.eslintrc.js | 5 + .../test/integration/jest.config.js | 18 + .../test/integration/package.json | 26 + .../test/integration/src/basic-matrix.test.ts | 87 + .../test/integration/src/basic.test.ts | 82 + .../test/integration/src/config.test.ts | 56 + .../integration/src/disable-plugin.test.ts | 62 + .../integration/src/disable-upload.test.ts | 61 + .../test/integration/src/git.test.ts | 119 + .../integration/src/hook-failures.test.ts | 363 ++ .../test/integration/src/long-names.test.ts | 72 + .../test/integration/src/matchers.ts | 53 + .../integration/src/no-quarantine.test.ts | 41 + .../test/integration/src/parse-output.ts | 1207 +++++ .../integration/src/plugin-failures.test.ts | 66 + .../test/integration/src/retries.test.ts | 77 + .../test/integration/src/run-test-case.ts | 973 ++++ .../test/integration/src/test-wrappers.ts | 152 + .../test/integration/src/unicode.test.ts | 42 + .../test/integration/src/verify-output.ts | 1568 ++++++ .../test/integration/tsconfig.json | 11 + packages/cypress-plugin/tsconfig.json | 19 + .../test/integration-input/package.json | 3 +- .../jest-plugin/test/integration/package.json | 2 + .../test/integration/src/runTestCase.ts | 2 +- packages/js-api/package.json | 10 + packages/js-api/src/consts.ts | 7 + packages/js-api/src/index.ts | 9 +- packages/plugins-common/package.json | 12 +- packages/plugins-common/src/quarantine.ts | 6 +- tsconfig.json | 3 +- yarn.lock | 4230 +++++++++++++++-- 152 files changed, 15441 insertions(+), 323 deletions(-) create mode 100644 .eslintignore create mode 100644 .github/workflows/cypress-realworld-app.yaml create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 packages/cypress-plugin/LICENSE create mode 100644 packages/cypress-plugin/README.md create mode 100644 packages/cypress-plugin/jest.config.js create mode 100644 packages/cypress-plugin/mocha.d.ts create mode 100644 packages/cypress-plugin/package.json create mode 100644 packages/cypress-plugin/rollup.config.mjs create mode 100644 packages/cypress-plugin/src/.eslintrc.js create mode 100644 packages/cypress-plugin/src/config-env-vars.ts create mode 100644 packages/cypress-plugin/src/config-wrapper.ts create mode 100644 packages/cypress-plugin/src/cypress-env-vars.ts create mode 100644 packages/cypress-plugin/src/index.ts create mode 100755 packages/cypress-plugin/src/main.ts create mode 100644 packages/cypress-plugin/src/plugin.ts create mode 100644 packages/cypress-plugin/src/reporter-common.ts create mode 100644 packages/cypress-plugin/src/reporter-config.test.ts create mode 100644 packages/cypress-plugin/src/reporter-config.ts create mode 100644 packages/cypress-plugin/src/reporter.ts create mode 100644 packages/cypress-plugin/src/skip-tests.ts create mode 100644 packages/cypress-plugin/src/tsconfig.json create mode 100644 packages/cypress-plugin/src/utils.ts create mode 100644 packages/cypress-plugin/src/vendored/cli-table3.d.ts create mode 100644 packages/cypress-plugin/src/vendored/duration.ts create mode 100644 packages/cypress-plugin/src/vendored/human_time.ts create mode 100644 packages/cypress-plugin/src/vendored/newlines.ts create mode 100644 packages/cypress-plugin/src/vendored/print-run.ts create mode 100644 packages/cypress-plugin/src/vendored/terminal-size.ts create mode 100644 packages/cypress-plugin/src/vendored/terminal.ts create mode 100644 packages/cypress-plugin/test/.eslintrc.cjs create mode 100644 packages/cypress-plugin/test/.gitignore create mode 100644 packages/cypress-plugin/test/integration-common/.eslintrc.js create mode 100644 packages/cypress-plugin/test/integration-common/package.json create mode 100644 packages/cypress-plugin/test/integration-common/rollup.config.mjs create mode 100644 packages/cypress-plugin/test/integration-common/src/config.ts create mode 100644 packages/cypress-plugin/test/integration-common/src/git.ts create mode 100644 packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts create mode 100644 packages/cypress-plugin/test/integration-common/src/tsconfig.json create mode 100644 packages/cypress-plugin/test/integration-common/tsconfig.json create mode 100644 packages/cypress-plugin/test/integration-input-esm/.eslintrc.cjs create mode 100644 packages/cypress-plugin/test/integration-input-esm/config-js/devtools.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/config-js/tasks.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/config-js/webpack.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/config/devtools.ts create mode 100644 packages/cypress-plugin/test/integration-input-esm/config/tasks.ts create mode 100644 packages/cypress-plugin/test/integration-input-esm/config/webpack.ts create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress-config.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress.config.ts create mode 120000 packages/cypress-plugin/test/integration-input-esm/cypress/component create mode 120000 packages/cypress-plugin/test/integration-input-esm/cypress/e2e create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support-js/commands.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.cjs create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.cjs create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.js create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support/commands.ts create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support/component-index.html create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support/component.ts create mode 100644 packages/cypress-plugin/test/integration-input-esm/cypress/support/e2e.ts create mode 100644 packages/cypress-plugin/test/integration-input-esm/package.json create mode 100644 packages/cypress-plugin/test/integration-input-esm/tsconfig.json create mode 100644 packages/cypress-plugin/test/integration-input-manual/config/devtools.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/config/tasks.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/config/webpack.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress-multi-reporters.config.json create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress.config.js create mode 120000 packages/cypress-plugin/test/integration-input-manual/cypress/component create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/fail.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/flake.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/hook-fail.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/invalid.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/mixed/mixed.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pass.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pending.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/e2e/quarantined.cy.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/support/commands.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/support/component-index.html create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/support/component.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e.js create mode 100644 packages/cypress-plugin/test/integration-input-manual/package.json create mode 100644 packages/cypress-plugin/test/integration-input/.eslintrc.js create mode 100644 packages/cypress-plugin/test/integration-input/config-js/devtools.js create mode 100644 packages/cypress-plugin/test/integration-input/config-js/tasks.js create mode 100644 packages/cypress-plugin/test/integration-input/config-js/webpack.js create mode 100644 packages/cypress-plugin/test/integration-input/config/devtools.ts create mode 100644 packages/cypress-plugin/test/integration-input/config/tasks.ts create mode 100644 packages/cypress-plugin/test/integration-input/config/webpack.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress-config.js create mode 100644 packages/cypress-plugin/test/integration-input/cypress-config.mjs create mode 100644 packages/cypress-plugin/test/integration-input/cypress.config.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/fail.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/flake.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/hook-fail.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/invalid.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/mixed/mixed.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/pass.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/pending.cy.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/component/quarantined.cy.ts create mode 120000 packages/cypress-plugin/test/integration-input/cypress/e2e create mode 100644 packages/cypress-plugin/test/integration-input/cypress/fixtures/example.json create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support-js/commands.js create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support-js/component.js create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support-js/component.mjs create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.js create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.mjs create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support/commands.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support/component-index.html create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support/component.ts create mode 100644 packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts create mode 100644 packages/cypress-plugin/test/integration-input/package.json create mode 100644 packages/cypress-plugin/test/integration-input/reporter-config.json create mode 100644 packages/cypress-plugin/test/integration-input/tsconfig.json create mode 100644 packages/cypress-plugin/test/integration/.eslintrc.js create mode 100644 packages/cypress-plugin/test/integration/jest.config.js create mode 100644 packages/cypress-plugin/test/integration/package.json create mode 100644 packages/cypress-plugin/test/integration/src/basic-matrix.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/basic.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/config.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/disable-plugin.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/disable-upload.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/git.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/hook-failures.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/long-names.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/matchers.ts create mode 100644 packages/cypress-plugin/test/integration/src/no-quarantine.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/parse-output.ts create mode 100644 packages/cypress-plugin/test/integration/src/plugin-failures.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/retries.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/run-test-case.ts create mode 100644 packages/cypress-plugin/test/integration/src/test-wrappers.ts create mode 100644 packages/cypress-plugin/test/integration/src/unicode.test.ts create mode 100644 packages/cypress-plugin/test/integration/src/verify-output.ts create mode 100644 packages/cypress-plugin/test/integration/tsconfig.json create mode 100644 packages/cypress-plugin/tsconfig.json create mode 100644 packages/js-api/src/consts.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..7e4c9de --- /dev/null +++ b/.eslintignore @@ -0,0 +1,11 @@ +# TypeScript uniques files that are included in a project twice via symlinks. eslint complains that +# the symlinked files aren't included in tsconfig.json (due to the deduping), so we ignore the +# symlinked copies and just lint the canonical location at: +# packages/cypress-plugin/test/integration-input/cypress/component +packages/cypress-plugin/test/integration-input*/cypress/e2e + +# Cypress produces screenshot folders with names that match the *.ts glob, and eslint complains. +packages/cypress-plugin/test/**/cypress/screenshots/*.ts + +# Don't lint output files. +packages/**/dist diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f196a74..ddca3c6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ on: - cron: 0 18 * * * # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: { } + workflow_dispatch: {} jobs: check: @@ -24,19 +24,32 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: '16' + node-version: "16" cache: yarn - - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - id: install run: yarn install --immutable - run: yarn build + - run: yarn workspace @unflakable/cypress-plugin pack + - uses: actions/upload-artifact@v3 + with: + name: cypress-plugin + path: packages/cypress-plugin/package.tgz + + - run: yarn workspace @unflakable/jest-plugin pack + - uses: actions/upload-artifact@v3 + with: + name: jest-plugin + path: packages/jest-plugin/package.tgz + + - run: yarn workspace @unflakable/js-api pack + - uses: actions/upload-artifact@v3 + with: + name: js-api + path: packages/js-api/package.tgz + - if: ${{ always() && steps.install.outcome == 'success' }} run: yarn lint @@ -56,6 +69,13 @@ jobs: # it's ignored for PR events, but that's fine. base: ${{ github.ref }} filters: | + cypress: + - package.json + - tsconfig.json + - .github/workflows/ci.yaml + - packages/cypress-plugin/** + - packages/js-api/** + - packages/plugins-common/** jest: - package.json - tsconfig.json @@ -65,10 +85,86 @@ jobs: - packages/plugins-common/** - scripts/set-jest-version.ts outputs: + affects_cypress: ${{ steps.affects_plugins.outputs.cypress }} affects_jest: ${{ steps.affects_plugins.outputs.jest }} + cypress_integration_tests: + name: "Cypress Integration Tests: Cypress ${{ matrix.cypress }} + Node ${{ matrix.node }}" + # FIXME: also test on Windows + runs-on: ubuntu-latest + timeout-minutes: 45 + needs: + # Don't incur the cost of the test matrix if the basic build fails. + - check + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || needs.check.outputs.affects_cypress == 'true' + strategy: + fail-fast: false + matrix: + node: + - 16 + - 18 + - 20 + cypress: + # FIXME: support earlier versions + #- "10.0" + #- "10.11" + #- "11.0" + - "11.2" + - "12.0" + - "12.10" + - "12.14" + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: yarn + + - name: Cache Cypress binary + uses: actions/cache@v3 + with: + path: ~/.cache/Cypress + key: cypress-${{ runner.os }}-node${{ matrix.node }}-cypress${{ matrix.cypress }}-modules-${{ hashFiles('yarn.lock') }} + + - id: install + run: yarn install --immutable + + - name: Set Cypress version + run: | + yarn set resolution "cypress@npm:10 - 12" ${{ matrix.cypress }} + grep --after-context=1 "^\".*cypress.*" yarn.lock + + - run: yarn build:plugins && yarn build:cypress-tests + + - name: Test + env: + # Enable debug logs within the Jest tests that run Cypress. WARNING: these are very + # verbose but are useful for seeing the raw chalk terminal codes. + # DEBUG: unflakable:* + + # Enable debug logs within the Cypress plugin. + # TEST_DEBUG: unflakable:* + + # Enable terminal colors for debug() output. + DEBUG_COLORS: "1" + + # Make chalk emit TTY colors. + FORCE_COLOR: "1" + run: | + if [ "${{ github.repository }}" == "unflakable/unflakable-javascript" ]; then + export UNFLAKABLE_SUITE_ID=2QwtGckRudLNUGBsdkVEoSknck1 + else + export UNFLAKABLE_SUITE_ID=2Qwt9RyPIbOI95C6qjXCzcTelni + fi + UNFLAKABLE_API_KEY=${{ secrets.UNFLAKABLE_API_KEY }} \ + yarn workspace cypress-integration test \ + --reporters @unflakable/jest-plugin/dist/reporter \ + --runner @unflakable/jest-plugin/dist/runner + jest_integration_tests: name: "Jest Integration Tests: Jest ${{ matrix.jest }} + Node ${{ matrix.node }}" + # FIXME: also test on Windows runs-on: ubuntu-latest timeout-minutes: 15 needs: @@ -118,11 +214,6 @@ jobs: node-version: ${{ matrix.node }} cache: yarn - - uses: actions/cache@v3 - with: - path: '**/node_modules' - key: ${{ runner.os }}-node${{ matrix.node }}-jest${{ matrix.jest }}-modules-${{ hashFiles('yarn.lock') }} - - id: install run: yarn install --immutable diff --git a/.github/workflows/cypress-realworld-app.yaml b/.github/workflows/cypress-realworld-app.yaml new file mode 100644 index 0000000..0bb2ac2 --- /dev/null +++ b/.github/workflows/cypress-realworld-app.yaml @@ -0,0 +1,127 @@ +name: Cypress Real World App + +# Controls when the workflow will run +on: + # Run daily so that we get continuous dogfooding. + schedule: + # Run at 7pm UTC/11am Pacific, which is after the CI pipeline runs so that we can use the + # latest build each day. + - cron: 0 19 * * * + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: {} + +jobs: + ui-chrome-tests: + timeout-minutes: 15 + runs-on: ubuntu-latest + container: + image: cypress/browsers:node16.16.0-chrome105-ff104-edge + options: --user 1001 + steps: + - name: Check out latest cypress-io/cypress-realworld-app + uses: actions/checkout@v3 + with: + repository: cypress-io/cypress-realworld-app + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + ~/.cache/Cypress + key: yarn-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn-${{ runner.os }}- + + - name: Download latest cypress-realworld-app build + uses: dawidd6/action-download-artifact@v2 + with: + repo: cypress-io/cypress-realworld-app + workflow: main.yml + branch: develop + name: build + path: build + search_artifacts: true + + - name: Download latest Cypress plugin build + uses: dawidd6/action-download-artifact@v2 + with: + workflow: ci.yaml + workflow_conclusion: success + branch: main + + - run: yarn install --frozen-lockfile + + - name: Install @unflakable/cypress-plugin + # We clean the Yarn cache here to make sure we're installing the package contents from the + # tarballs. Otherwise, Yarn may install the previously cached version from an earlier build + # with the same package version number. + run: | + curl -Lo jq https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64 + chmod +x jq + + yarn cache clean @unflakable/js-api + yarn cache clean @unflakable/cypress-plugin + # See https://github.com/yarnpkg/yarn/issues/5357#issuecomment-748434801. + rm -rf "$(yarn cache dir)/.tmp" + + # Make sure Yarn resolves @unflakable/js-api to the tarball. If we just try to install + # both packages at the same time, Yarn will complain if the js-api package version number + # doesn't exist in the public NPM registry. + cat package.json \ + | ./jq '. + {"resolutions": (.resolutions + {"@unflakable/js-api": "file:./js-api/package.tgz"})}' \ + > package-new.json + mv package-new.json package.json + + yarn add --dev file:./cypress-plugin/package.tgz + + - name: Cypress info + run: yarn cypress info + + - name: Node info + run: node -v + + - name: __e Dir + run: ls /__e + + - name: "UI Tests - Chrome" + env: + DEBUG: "@cypress/github-action,unflakable:*" + UNFLAKABLE_SUITE_ID: 2RBwks266ZvV5jn9zR5Ula9XK7m + UNFLAKABLE_API_KEY: ${{ secrets.UNFLAKABLE_API_KEY }} + uses: cypress-io/github-action@v5 + with: + install: false + command: yarn exec cypress-unflakable -- + start: yarn start:ci + wait-on: "http://localhost:3000" + wait-on-timeout: 120 + browser: chrome + record: true + parallel: true + group: "UI - Chrome" + spec: cypress/tests/ui/* + config-file: cypress.config.js + + - name: Save screenshots + if: always() + uses: actions/upload-artifact@v3 + # add the line below to store screenshots only on failures + # if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + if-no-files-found: ignore # 'warn' or 'error' are also available, defaults to `warn` + + - name: Save videos + if: always() + uses: actions/upload-artifact@v3 + with: + name: cypress-videos + path: cypress/videos + if-no-files-found: ignore # 'warn' or 'error' are also available, defaults to `warn` diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..fac0b0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.20.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..50a1918 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +.yarn +packages/cypress-plugin/**/cypress/screenshots/*.ts +packages/**/dist diff --git a/.yarnrc.yml b/.yarnrc.yml index 75e740a..d69fd38 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,5 +1,6 @@ yarnPath: .yarn/releases/yarn-3.5.1.cjs nodeLinker: node-modules +nmMode: hardlinks-local # By default, publish packages privately to GitHub for internal testing instead of to NPM. npmScopes: diff --git a/README.md b/README.md index a26fbfb..0b3915a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,21 @@ and TypeScript. Refer to our [Getting Started](https://docs.unflakable.com/getting-started) documentation for instructions on getting started using Unflakable. +## Cypress Plugin + +This Unflakable plugin enables users of the [Cypress](https://cypress.io) JavaScript test framework +to quarantine flaky tests and track test results. + +Refer to the [Cypress Plugin](https://docs.unflakable.com/plugins/cypress) documentation for +complete usage instructions. + +### Compatibility + +This plugin maintains compatibility with the Cypress and Node.js versions listed below: + +[![11.2.0+ | 12.0.0+](https://img.shields.io/badge/Cypress-11.2.0%2B%20%7C%2012.0.0%2B-17202C?logo=cypress&labelColor=white&logoColor=17202C&style=flat)](https://cypress.io) +[![16 | 18 | 20](https://img.shields.io/badge/Node.js-16%20%7C%2018%20%7C%2020-339933?logo=node.js&labelColor=white&logoColor=339933&style=flat)](https://nodejs.org) + ## Jest Plugin The Jest plugin enables users of the [Jest](https://jestjs.io) JavaScript test framework diff --git a/package.json b/package.json index 4002e6f..c01d2c5 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,6 @@ "@babel/core": "^7.21.5", "@babel/preset-env": "^7.21.5", "@babel/preset-typescript": "^7.21.5", - "@rollup/plugin-node-resolve": "^15.0.2", - "@rollup/plugin-typescript": "^11.1.0", "@types/debug": "^4.1.7", "@types/js-yaml": "^4.0.5", "@types/node": "^14.18.43", @@ -16,33 +14,39 @@ "debug": "^4.3.3", "eslint": "^8.39.0", "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-cypress": "^2.13.3", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jest": "^27.2.1", "js-yaml": "^4.1.0", "prettier": "^2.5.1", - "rollup": "^3.21.1", "semver": "^7.5.1", "ts-node": "^10.7.0", "typescript": "^4.9.5" }, "scripts": { "audit": "yarn npm audit --all --recursive", - "build": "yarn build:plugins && yarn typecheck:scripts && yarn typecheck:tests", - "build:plugins": "yarn build:js-api && yarn build:plugins-common && yarn build:jest-plugin", + "build": "yarn build:plugins && yarn typecheck:scripts && yarn build:tests", + "build:plugins": "yarn build:js-api && yarn build:plugins-common && yarn build:cypress-plugin && yarn build:jest-plugin", "build:js-api": "yarn workspace @unflakable/js-api build", - "build:plugins-common": "yarn workspace @unflakable/plugins-common build", + "build:cypress-plugin": "yarn workspace @unflakable/cypress-plugin build", "build:jest-plugin": "yarn workspace @unflakable/jest-plugin build", + "build:plugins-common": "yarn workspace @unflakable/plugins-common build", "typecheck:scripts": "tsc --noEmit --types node -p scripts", - "typecheck:tests": "yarn workspace jest-integration typecheck && yarn workspace jest-integration-input typecheck", - "lint": "eslint packages/**/src/**/*.{ts,js} scripts/**/*.{ts,js}", - "prettier": "prettier --write packages/**/src/*.{ts,js}", - "prettier:check": "prettier --check packages/**/src/*.{ts,js}", + "build:tests": "yarn build:cypress-tests && yarn typecheck:jest-tests", + "build:cypress-tests": "yarn workspace cypress-integration-common build && yarn workspace cypress-integration typecheck && yarn workspace cypress-integration-input typecheck && yarn workspace cypress-integration-input-esm typecheck", + "typecheck:jest-tests": "yarn workspace jest-integration typecheck && yarn workspace jest-integration-input typecheck", + "lint": "eslint .", + "prettier": "prettier --write .", + "prettier:check": "prettier --check .", "publish": "yarn workspace @unflakable/js-api npm publish --access public && yarn workspace @unflakable/jest-plugin npm publish --access public", "set-jest-version": "ts-node scripts/set-jest-version.ts" }, "workspaces": [ "packages/*", - "packages/jest-plugin/test/integration", - "packages/jest-plugin/test/integration-input" + "packages/*/test/integration", + "packages/*/test/integration-common", + "packages/*/test/integration-input", + "packages/*/test/integration-input-esm", + "packages/*/test/integration-input-manual" ] } diff --git a/packages/cypress-plugin/LICENSE b/packages/cypress-plugin/LICENSE new file mode 100644 index 0000000..964dfd7 --- /dev/null +++ b/packages/cypress-plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Developer Innovations, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cypress-plugin/README.md b/packages/cypress-plugin/README.md new file mode 100644 index 0000000..961ceac --- /dev/null +++ b/packages/cypress-plugin/README.md @@ -0,0 +1,29 @@ +

+ + Unflakable + +

+ +[![npm version](https://img.shields.io/npm/v/@unflakable/cypress-plugin.svg)](https://www.npmjs.com/package/@unflakable/cypress-plugin) +[![Twitter](https://img.shields.io/twitter/url?label=%40unflakable&style=social&url=https%3A%2F%2Ftwitter.com%2Funflakable)](https://twitter.com/unflakable) + +# Unflakable Plugin for Cypress + +This Unflakable plugin enables users of the [Cypress](https://cypress.io) JavaScript test framework +to quarantine flaky tests and track test results. + +Refer to the [Cypress Plugin](https://docs.unflakable.com/plugins/cypress) documentation for +complete usage instructions. + +### Compatibility + +This plugin maintains compatibility with the Cypress and Node.js versions listed below: + +[![11.2.0+ | 12.0.0+](https://img.shields.io/badge/Cypress-11.2.0%2B%20%7C%2012.0.0%2B-17202C?logo=cypress&labelColor=white&logoColor=17202C&style=flat)](https://cypress.io) +[![16 | 18 | 20](https://img.shields.io/badge/Node.js-16%20%7C%2018%20%7C%2020-339933?logo=node.js&labelColor=white&logoColor=339933&style=flat)](https://nodejs.org) + +## Contributing + +To report a bug or request a new feature, please +[file a GitHub issue](https://github.com/unflakable/unflakable-javascript/issues). +We also welcome pull requests! diff --git a/packages/cypress-plugin/jest.config.js b/packages/cypress-plugin/jest.config.js new file mode 100644 index 0000000..d2d9b3c --- /dev/null +++ b/packages/cypress-plugin/jest.config.js @@ -0,0 +1,20 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + extensionsToTreatAsEsm: [".ts"], + roots: ["src"], + testEnvironment: "node", + testTimeout: 60000, + transform: { + "^.+\\.ts?$": [ + "ts-jest", + { + tsconfig: "src/tsconfig.json", + //isolatedModules: true, + useESM: true, + }, + ], + }, + verbose: true, +}; diff --git a/packages/cypress-plugin/mocha.d.ts b/packages/cypress-plugin/mocha.d.ts new file mode 100644 index 0000000..d81ed0d --- /dev/null +++ b/packages/cypress-plugin/mocha.d.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +type GlobalError = Error; + +// NB: Cypress exports its own copy of @types/mocha, so we don't depend on that package directly. + +declare namespace Mocha { + // This type is inferred from Mocha.reporters.Base.list(), Mocha.reporters.Base.showDiff(), and + // Mocha's EVENT_TEST_FAIL handler: + // https://github.com/mochajs/mocha/blob/1412dc80d87d0479f7f1d60202da2b33c90eb939/lib/reporters/base.js#L235 + // https://github.com/mochajs/mocha/blob/1412dc80d87d0479f7f1d60202da2b33c90eb939/lib/reporters/base.js#L157C1-L164 + // https://github.com/mochajs/mocha/blob/1412dc80d87d0479f7f1d60202da2b33c90eb939/lib/reporters/base.js#L317-L329 + export type Error = GlobalError & { + actual?: unknown; + expected?: unknown; + inspect?: () => string; + multiple?: Mocha.Error[]; + showDiff?: boolean; + }; + + // Additional fields that Cypress adds to Mocha.Test. + export type CypressTestProps = { + id: string; + order: number; + wallClockStartedAt: string; + }; + + export type CypressTest = Test & CypressTestProps; + + // Since Mocha.Test objects need to be serialized before they can be sent from the Cypress driver + // in the browser to the Node server, the event payload isn't an actual Mocha.Test instance. + // For every event type except retries, Cypress merges the event payload back into the proper + // Mocha.Test instance that already exists on the Node server side. + // Mocha.Test properties that get sent from the browser driver to the Cypress Node server + // are listed here: + // https://github.com/cypress-io/cypress/blob/660ff675782dba8725e16620ca35fc38e004c23f/packages/driver/src/cypress/runner.ts#L27 + // Cypress merging of event payloads for different event types is implemented here: + // https://github.com/cypress-io/cypress/blob/b0c0eaa508bb6dafdc1997bc00fb7ed6f5bcc160/packages/server/lib/reporter.js#L223-L238 + export type MochaEventTest = Pick< + Test, + | "title" + | "err" + | "state" + | "pending" + | "body" + | "speed" + | "type" + | "duration" + > & + CypressTestProps & { + currentRetry: number; + retries: number; + }; + + namespace reporters { + namespace Base { + // Exported here: + // https://github.com/mochajs/mocha/blob/9c10adab3340abd8baff147cb595256234d88de6/lib/mocha.js#L876 + let hideDiff: boolean | undefined; + + // This function is exported by Mocha but missing from Cypress's exported @types/mocha: + // https://github.com/mochajs/mocha/blob/1412dc80d87d0479f7f1d60202da2b33c90eb939/lib/reporters/base.js#L157-L164 + function showDiff(err: unknown): err is Mocha.Error; + } + } +} diff --git a/packages/cypress-plugin/package.json b/packages/cypress-plugin/package.json new file mode 100644 index 0000000..05ff780 --- /dev/null +++ b/packages/cypress-plugin/package.json @@ -0,0 +1,94 @@ +{ + "name": "@unflakable/cypress-plugin", + "description": "Cypress plugin for Unflakable", + "repository": { + "type": "git", + "url": "https://github.com/unflakable/unflakable-javascript.git" + }, + "bugs": "https://github.com/unflakable/unflakable-javascript/issues", + "homepage": "https://unflakable.com", + "license": "MIT", + "version": "0.1.0", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./config-wrapper": { + "types": "./dist/config-wrapper.d.ts", + "default": "./dist/config-wrapper.mjs" + }, + "./reporter": { + "types": "./dist/reporter.d.ts", + "default": "./dist/reporter.js" + }, + "./skip-tests": { + "types": "./dist/skip-tests.d.ts", + "default": "./dist/skip-tests.js" + } + }, + "files": [ + "README.md", + "dist/**/*.js", + "dist/**/*.mjs", + "dist/index.d.ts", + "dist/config-wrapper.d.ts", + "dist/reporter.d.ts", + "dist/skip-tests.d.ts" + ], + "bin": { + "cypress-unflakable": "./dist/main.js" + }, + "dependencies": { + "@unflakable/js-api": "workspace:^", + "ansi-styles": "^4.3.0", + "chalk": "^4.1.0", + "cli-table3": "0.5.1", + "cosmiconfig": "^7.0.1", + "cypress-multi-reporters": "^1.6.3", + "dayjs": "^1.10.4", + "debug": "^4.3.3", + "deep-equal": "^2.0.5", + "es6-promisify": "^7.0.0", + "lodash": "^4.17.21", + "log-symbols": "^4.1.0", + "mocha": "=7.0.1", + "ms": "2.1.1", + "path-browserify": "^1.0.1", + "simple-git": "^3.16.0", + "term-size": "2.1.0", + "tmp": "~0.2.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-typescript": "^11.1.1", + "@types/debug": "^4.1.7", + "@types/deep-equal": "^1.0.1", + "@types/es6-promisify": "^6.0.1", + "@types/lodash": "^4.14.194", + "@types/path-browserify": "^1.0.0", + "@types/tmp": "^0.2.3", + "@types/yargs": "^17.0.24", + "@unflakable/plugins-common": "workspace:", + "cross-env": "^7.0.3", + "cypress": "10 - 12", + "jest": "^29.5.0", + "jest-environment-node": "^29.5.0", + "rollup": "^3.21.1", + "typescript": "^4.9.5", + "widest-line": "3.1.0" + }, + "peerDependencies": { + "cypress": "10 - 12" + }, + "scripts": { + "build": "yarn clean && tsc --noEmit && tsc --noEmit -p src && yarn build:cjs && yarn build:esm", + "build:cjs": "rollup --config --dir dist", + "build:esm": "rollup --config --input src/config-wrapper.ts --file dist/config-wrapper.mjs --format es --chunkFileNames \"[name]-[hash].mjs\"", + "clean": "rm -rf dist/", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --useStderr --verbose" + } +} diff --git a/packages/cypress-plugin/rollup.config.mjs b/packages/cypress-plugin/rollup.config.mjs new file mode 100644 index 0000000..32bb23a --- /dev/null +++ b/packages/cypress-plugin/rollup.config.mjs @@ -0,0 +1,64 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import pluginCommonJs from "@rollup/plugin-commonjs"; +import pluginJson from "@rollup/plugin-json"; +import pluginNodeResolve from "@rollup/plugin-node-resolve"; +import pluginTypescript from "@rollup/plugin-typescript"; + +/** + * @type {import("rollup").NormalizedInputOptions} + */ +export default { + // NB: We exclude src/config-wrapper.ts since that needs to be compiled as an ESM target in + // order to be able to import user cypress.config.js files for projects that use ESM. The reason + // is that Cypress loads our config file and expects the default export to be the config + // object, which requires us to load the user config file when our script loads. Dynamic + // import() of ESM (require() can't import ESM modules) is async, but CommonJS doesn't support + // await at the top level of a file (ESM does). To summarize: (1) code that's imported by Cypress + // (with the exception of the config file, due to (2)) should be CommonJS, while (2) code that + // imports user code should be ESM so that it supports both CommonJS and ESM user code. + input: [ + "src/index.ts", + "src/main.ts", + "src/reporter.ts", + "src/skip-tests.ts", + ], + output: { + format: "cjs", + /** + * @param {import("rollup").PreRenderedChunk} chunk + * @returns {string} + */ + banner: (chunk) => (chunk.name === "main" ? "#!/usr/bin/env node" : ""), + }, + // Bundle the internal @unflakable/plugins-common package, along with dependencies used by + // vendored Cypress code that Cypress itself also bundles (i.e., doesn't list in its public + // package.json as deps) in dist/, but leave most other imported packages as an external. Internal + // modules begin with `.` or `/`. We don't include `term-size` here because it depends on a + // bundled vendor/ directory that rollup doesn't include. + external: (id) => + !id.startsWith(".") && + !id.startsWith("/") && + !id.startsWith("src/") && + !id.startsWith("@unflakable/plugins-common/") && + ![ + // Avoid having skip-tests depend on @unflakable/js-api, which could pull in Node dependencies + // that Webpack v5 doesn't bundle by default. It's also unclear which versions of Webpack + // support sub-path exports from package.json like this. To avoid requiring any changes to + // the user's Webpack config, we try to make skip-tests self-contained and easy to import. + "@unflakable/js-api/consts", + "@unflakable/plugins-common", + "widest-line", + ].includes(id), + plugins: [ + pluginCommonJs(), + pluginJson(), + pluginNodeResolve({ preferBuiltins: true }), + pluginTypescript({ tsconfig: "src/tsconfig.json" }), + ], + treeshake: { + // Assume internal modules do not have side effects when they're imported. This helps remove + // unnecessary require()'s from the transpiled code. + moduleSideEffects: (id, external) => external, + }, +}; diff --git a/packages/cypress-plugin/src/.eslintrc.js b/packages/cypress-plugin/src/.eslintrc.js new file mode 100644 index 0000000..62e8179 --- /dev/null +++ b/packages/cypress-plugin/src/.eslintrc.js @@ -0,0 +1,27 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + extends: ["../../../.eslintrc-ts.js"], + overrides: [ + { + // Disable some rules that fail for vendored Cypress code. If we fix these, it'll be harder to + // merge upstream changes. + files: ["vendored/**"], + rules: { + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/strict-boolean-expressions": "off", + curly: "off", + eqeqeq: "off", + "prefer-const": "off", + }, + }, + ], +}; diff --git a/packages/cypress-plugin/src/config-env-vars.ts b/packages/cypress-plugin/src/config-env-vars.ts new file mode 100644 index 0000000..d9623b1 --- /dev/null +++ b/packages/cypress-plugin/src/config-env-vars.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { EnvVar } from "@unflakable/plugins-common"; + +export const ENV_VAR_AUTO_SUPPORT = new EnvVar("__UNFLAKABLE_AUTO_SUPPORT"); + +// Serialized copy of the Unflakable config (after resolving all CLI options, environment variables, +// and config file contents. +export const ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON = new EnvVar( + "__UNFLAKABLE_RESOLVED_CONFIG_JSON" +); + +// Cypress config passed via --config Cypress CLI option. +export const ENV_VAR_USER_CONFIG_JSON = new EnvVar( + "__UNFLAKABLE_USER_CONFIG_JSON" +); + +// Cypress config file path passed via --configFile Cypress CLI option or discovered by searching +// for a Cypress config file. +export const ENV_VAR_USER_CONFIG_PATH = new EnvVar( + "__UNFLAKABLE_USER_CONFIG_PATH" +); diff --git a/packages/cypress-plugin/src/config-wrapper.ts b/packages/cypress-plugin/src/config-wrapper.ts new file mode 100644 index 0000000..ce904e0 --- /dev/null +++ b/packages/cypress-plugin/src/config-wrapper.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { wrapCypressConfig } from "./index"; +import _debug from "debug"; +import { require } from "./utils"; +import { + ENV_VAR_USER_CONFIG_JSON, + ENV_VAR_USER_CONFIG_PATH, +} from "./config-env-vars"; +import path from "path"; + +const debug = _debug("unflakable:config-wrapper"); + +type LoadedConfig = + | { + default: Cypress.ConfigOptions; + } + | (Cypress.ConfigOptions & { default: undefined }); + +const loadUserConfig = async (): Promise> => { + if (ENV_VAR_USER_CONFIG_JSON.value !== undefined) { + debug(`Parsing inline user config ${ENV_VAR_USER_CONFIG_JSON.value}`); + + return JSON.parse( + ENV_VAR_USER_CONFIG_JSON.value + ) as Cypress.ConfigOptions; + } else if (ENV_VAR_USER_CONFIG_PATH.value === undefined) { + throw new Error("No user config to load"); + } + + debug(`Loading user config from ${ENV_VAR_USER_CONFIG_PATH.value}`); + + // Relative paths from the user's config need to resolve relative to the location of their + // cypress.config.js, not ours. This affects things like webpack for component testing. + const configPathDir = path.dirname(ENV_VAR_USER_CONFIG_PATH.value); + debug(`Changing working directory to ${configPathDir}`); + process.chdir(configPathDir); + + // For CommonJS projects, we need to use require(), at least for TypeScript config files. + // Dynamic import() doesn't support TypeScript imports in CommonJS projects, at least the way + // Cypress sets up the environment before loading the config. + try { + const config = require(ENV_VAR_USER_CONFIG_PATH.value) as LoadedConfig; + return config.default ?? config; + } catch (e) { + // require() can't import ES modules, so now we try a dynamic import(). This is what gets used + // for ESM projects. + debug(`require() failed; attempting dynamic import(): ${e as string}`); + const config = (await import( + ENV_VAR_USER_CONFIG_PATH.value + )) as LoadedConfig; + return config.default ?? config; + } +}; + +const userConfig = await loadUserConfig(); +debug("Loaded user config %o", userConfig); + +export default wrapCypressConfig(userConfig); diff --git a/packages/cypress-plugin/src/cypress-env-vars.ts b/packages/cypress-plugin/src/cypress-env-vars.ts new file mode 100644 index 0000000..14c03b0 --- /dev/null +++ b/packages/cypress-plugin/src/cypress-env-vars.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// NB: This file is separate from config-env-vars.ts so that it can be included by skip-tests.ts +// without adding any Node.JS dependencies (since that file needs to run in the browser). + +export const CYPRESS_ENV_VAR_CONFIG = "__UNFLAKABLE_CONFIG"; +export const CYPRESS_ENV_VAR_MANIFEST = "__UNFLAKABLE_MANIFEST"; +export const CYPRESS_ENV_VAR_REPO_ROOT = "__UNFLAKABLE_REPO_ROOT"; diff --git a/packages/cypress-plugin/src/index.ts b/packages/cypress-plugin/src/index.ts new file mode 100644 index 0000000..90b54e5 --- /dev/null +++ b/packages/cypress-plugin/src/index.ts @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import _debug from "debug"; +import { + getRepoRoot, + getTestSuiteManifest, + loadApiKey, + loadGitRepo, + UnflakableConfig, +} from "@unflakable/plugins-common"; +import { PluginOptions, UnflakableCypressPlugin } from "./plugin"; +import { printWarning, userAgent } from "./utils"; +import { + CYPRESS_ENV_VAR_CONFIG, + CYPRESS_ENV_VAR_MANIFEST, + CYPRESS_ENV_VAR_REPO_ROOT, +} from "./cypress-env-vars"; +import { + ENV_VAR_AUTO_SUPPORT, + ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON, +} from "./config-env-vars"; + +export { PluginOptions }; + +const debug = _debug("unflakable:index"); + +// Exported so that users can directly register the plugin if `wrapCypressConfig` doesn't work for +// their setup. +export const registerUnflakable = async ( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions, + options: PluginOptions = {} +): Promise => { + debug(`Called registerUnflakable()`); + + if ( + (globalThis as { __unflakableIsRegistered?: true }).__unflakableIsRegistered + ) { + printWarning("Unflakable plugin is already registered"); + return config; + } + + (globalThis as { __unflakableIsRegistered?: true }).__unflakableIsRegistered = + true; + + if (ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON.value === undefined) { + throw new Error( + `Environment variable ${ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON.name} not found` + ); + } + const unflakableConfig = JSON.parse( + ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON.value + ) as UnflakableConfig; + + if (!unflakableConfig.enabled) { + debug("Unflakable plugin is disabled"); + // If the user has made any of their own config modifications in setupNodeEvents(), be sure to + // preserve those. + return config; + } + + const apiKey = loadApiKey(); + + const manifest = await getTestSuiteManifest({ + apiKey, + baseUrl: unflakableConfig.apiBaseUrl, + clientDescription: userAgent(config.version), + log: console.error.bind(console), + testSuiteId: unflakableConfig.testSuiteId, + }); + + const git = unflakableConfig.gitAutoDetect ? await loadGitRepo() : null; + const repoRoot = git !== null ? await getRepoRoot(git) : config.projectRoot; + + const plugin = new UnflakableCypressPlugin({ + apiKey, + manifest, + repoRoot, + unflakableConfig, + }); + + // NB: We mutate the `config` object instead of creating a new object because only changes to the + // original config object Cypress passed to setupNodeEvents() will be seen by the + // `dev-server:start` event handler, which captures the config from its caller before Cypress + // invokes the user's setupNodeEvents() function. See: + // https://github.com/cypress-io/cypress/blob/abd986aa411af3ec358056a8458b74cb52952b01/packages/server/lib/plugins/child/run_require_async_child.js#L162-L178 + // This is only an issue for component testing. For e2e testing, Cypress immediately calls the + // user's setupNodeEvents(). + config.env[CYPRESS_ENV_VAR_CONFIG] = JSON.stringify(unflakableConfig); + config.env[CYPRESS_ENV_VAR_MANIFEST] = JSON.stringify(manifest ?? null); + config.env[CYPRESS_ENV_VAR_REPO_ROOT] = repoRoot; + + return plugin.register(on, config, options); +}; + +const wrapSetupNodeEvents = + ( + userSetupNodeEvents: + | Cypress.ResolvedConfigOptions["setupNodeEvents"] + | undefined + ) => + async ( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions + ): Promise => { + const userModifiedConfig = + userSetupNodeEvents !== undefined + ? await userSetupNodeEvents(on, config) + : undefined; + + return registerUnflakable( + on, + // If the user has made any of their own config modifications in setupNodeEvents(), be + // sure to preserve those. + userModifiedConfig ?? config, + ENV_VAR_AUTO_SUPPORT.value === "false" ? { autoSupportFile: false } : {} + ); + }; + +// Wraps the user's cypress.config.{js,ts,mjs,cjs} and automatically registers the Unflakable +// plugin. +export const wrapCypressConfig = ( + userConfig: Cypress.ConfigOptions +): Cypress.ConfigOptions => { + return { + ...userConfig, + component: + userConfig.component !== undefined + ? { + ...userConfig.component, + setupNodeEvents: wrapSetupNodeEvents( + userConfig.component?.setupNodeEvents + ), + } + : undefined, + e2e: + userConfig.e2e !== undefined + ? { + ...userConfig.e2e, + setupNodeEvents: wrapSetupNodeEvents( + userConfig.e2e?.setupNodeEvents + ), + } + : undefined, + }; +}; diff --git a/packages/cypress-plugin/src/main.ts b/packages/cypress-plugin/src/main.ts new file mode 100755 index 0000000..480a9cd --- /dev/null +++ b/packages/cypress-plugin/src/main.ts @@ -0,0 +1,364 @@ +//#!/usr/bin/env node + +// Copyright (c) 2023 Developer Innovations, LLC + +import * as cypress from "cypress"; +import _debug from "debug"; +import { + UnflakableConfig, + branchOverride, + commitOverride, + loadConfig, + QuarantineMode, +} from "@unflakable/plugins-common"; +import path from "path"; +import * as process from "process"; +import chalk from "chalk"; +import { aggregateRunStats } from "./reporter-common"; +import { hideBin } from "yargs/helpers"; +import yargs from "yargs"; +import * as fs from "fs/promises"; +import { require } from "./utils"; +import { + ENV_VAR_AUTO_SUPPORT, + ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON, + ENV_VAR_USER_CONFIG_JSON, + ENV_VAR_USER_CONFIG_PATH, +} from "./config-env-vars"; + +const CONFIG_WRAPPER_MODULE = "@unflakable/cypress-plugin/config-wrapper"; + +const debug = _debug("unflakable:main"); + +const exitDefault = ( + results: CypressCommandLine.CypressRunResult, + unflakableConfig: UnflakableConfig +): never => { + // Mirror's Cypress's behavior: + // https://github.com/cypress-io/cypress/blob/0b10178850fc49bb3d590ca26ca50ae7c706f899/packages/server/lib/cypress.js#L267 + if (results.runs.length > 0) { + // NB: This only arises when using the run cancellation feature of Cypress Cloud (see + // https://docs.cypress.io/guides/cloud/runs#Run-cancellation and + // https://github.com/cypress-io/cypress/pull/14925). + const isCanceled = results.runs.filter((run) => run.skippedSpec).length > 0; + if (isCanceled) { + console.error( + chalk.magenta( + "\n Exiting with non-zero exit code because the run was canceled." + ) + ); + + process.exit(1); + } + } + + if (!unflakableConfig.enabled) { + process.exit(results.totalFailed); + } else { + const totals = aggregateRunStats(results.runs); + + debug( + `${ + totals.failedTests + totals.flakyTests + } total failure(s)/flake(s) after quarantining (originally ${ + totals.failedTests + totals.flakyTests + totals.quarantinedTests + }) with${ + totals.unquarantinedSkipped > 0 ? "" : "out" + } unquarantined skipped tests` + ); + + // Be sure to exit with non-zero if there are any non-quarantined skipped tests, + // even if + // all the failed/flaky tests are quarantined. + process.exit( + Math.max( + totals.failedTests + totals.flakyTests, + totals.unquarantinedSkipped > 0 ? 1 : 0 + ) + ); + } +}; + +// This only seems to arise if the cypress.run() API fails to parse the JSON output of the child +// Cypress process: +// https://github.com/cypress-io/cypress/blob/1d3aab9d70acbce6d3571ab5b9df771f1c455964/cli/lib/cypress.js#L45-L49 +const exitFailure = ( + results: CypressCommandLine.CypressFailedRunResult +): never => { + console.error( + chalk.red(`\nCypress exited with error: ${chalk.magenta(results.message)}`) + ); + + // We always want to exit with a non-zero code in this case. If the child process already + // exited with a non-zero code, propagate it through. For reference, Cypress sets the exit code + // based on the number of failures and propagates it through in this case: + // https://github.com/cypress-io/cypress/blob/0b10178850fc49bb3d590ca26ca50ae7c706f899/cli/lib/cypress.js#L41 + process.exit(results.failures !== 0 ? results.failures : 1); +}; + +const parseArgs = ( + argv: string[] +): yargs.Argv<{ + _: (string | number)[]; + "auto-config": boolean; + "auto-support": boolean; + branch?: string; + commit?: string; + "failure-retries"?: number; + "git-auto-detect"?: boolean; + "quarantine-mode"?: string; + "test-suite-id"?: string; + "upload-results"?: boolean; +}>["argv"] => + yargs(hideBin(argv)) + .scriptName("cypress-unflakable") + .usage( + "Unflakable plugin wrapper for `cypress run` command.\n\n" + + "Usage: $0 [unflakable-options...] [-- ]\n\n" + + "All options after `--` are passed to `cypress run`.\n\n" + + "For complete documentation, please visit https://docs.unflakable.com/plugins/cypress." + ) + .example("$0", "Run all Cypress e2e tests") + .example( + "$0 -- --spec cypress/e2e/mytest.cy.ts", + "Run a single Cypress e2e spec" + ) + .example("$0 -- --component", "Run all Cypress component tests") + .example( + "$0 --test-suite-id ", + "Specify Unflakable test suite ID as a command-line option" + ) + .example( + "$0 --test-suite-id -- --spec cypress/e2e/mytest.cy.ts", + "Combine Unflakable and Cypress options" + ) + .option("auto-config", { + description: + "Automatically wrap the Cypress config file to run the Unflakable plugin", + default: true, + type: "boolean", + }) + .option("auto-support", { + description: + "Automatically wrap the Cypress support file to skip tests when quarantineMode is `skip_tests`", + default: true, + type: "boolean", + }) + .option("branch", { + description: + "Name of the version control (e.g., Git) branch containing the code being tested " + + "(overrides branch name inferred from Git when the `gitAutoDetect` config option is " + + "enabled)", + type: "string", + }) + .option("commit", { + description: + "Git commit hash or other value that uniquely identifies the commit (version control " + + "revision) of the code being tested (overrides commit hash inferred from Git " + + "when the `gitAutoDetect` config option is enabled)", + type: "string", + }) + .option("failure-retries", { + description: + "Maximum number of times to retry each failed test (overrides `failureRetries` config value)", + type: "number", + }) + .option("git-auto-detect", { + description: + "Auto-detect the current branch and commit hash from Git (overrides `gitAutoDetect` config value)", + type: "boolean", + }) + .option("quarantine-mode", { + description: + "Controls the behavior of quarantined tests (overrides `quarantineMode` config value)", + choices: ["no_quarantine", "skip_tests", "ignore_failures"], + type: "string", + }) + .option("test-suite-id", { + description: + "Unflakable test suite ID (overrides `testSuiteId` config value)", + type: "string", + }) + .option("upload-results", { + description: + "Upload test results to Unflakable (overrides `uploadResults` config value)", + // NB: we don't specify a default here because that would always override the value from the + // config. + type: "boolean", + }) + // Reject unknown options. Unfortunately, there doesn't seem to be an easy way to capture + // unknown options and treat them as position args in `argv._`. Instead, we'll just have to + // require ` -- ` prior to any Cypress options. + .strict() + // Examples look bad when wrapped. + .wrap(Math.min(process.stdout.columns, 160)).argv; + +const POTENTIAL_CONFIG_FILES = [ + "cypress.config.ts", + "cypress.config.mjs", + "cypress.config.cjs", + "cypress.config.js", +]; + +// Follow Cypress's config file resolution process. See: +// https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/data-context/src/data/ProjectLifecycleManager.ts#L659C9-L713 +const resolveUserConfigPath = async ( + projectRoot: string, + runOptions: Partial +): Promise => { + if (runOptions.configFile !== undefined) { + return path.join(projectRoot, runOptions.configFile); + } + + for (const fileName of POTENTIAL_CONFIG_FILES) { + const filePath = path.isAbsolute(fileName) + ? fileName + : path.join(projectRoot, fileName); + if ( + await fs + .stat(filePath) + .then(() => true) + .catch(() => false) + ) { + debug(`Found Cypress config file at ${filePath}`); + return filePath; + } + } + + throw new Error( + `Could not find a Cypress configuration file in folder ${projectRoot}` + ); +}; + +const findUserTsConfig = async (searchDir: string): Promise => { + try { + const possibleTsConfigPath = path.join(searchDir, "tsconfig.json"); + await fs.stat(possibleTsConfigPath); + // If fs.stat() doesn't throw an exception, the file exists. + debug(`Found tsconfig.json at ${possibleTsConfigPath}`); + return possibleTsConfigPath; + } catch { + const parent = path.dirname(searchDir); + if (parent !== searchDir) { + return findUserTsConfig(parent); + } else { + return null; + } + } +}; + +const main = async (): Promise => { + const args = await parseArgs(process.argv); + + const cypressArgs = [ + "run", + // In case yargs parsed an option as a number, we need all of the args to be strings. + ...args._.map((arg) => arg.toString()), + ]; + debug(`Parsing Cypress args ${JSON.stringify(cypressArgs)}`); + const runOptions = await cypress.cli.parseRunArguments(cypressArgs); + + const projectRoot = + runOptions.project !== undefined + ? path.resolve(process.cwd(), runOptions.project) + : process.cwd(); + + const unflakableConfig = await loadConfig(projectRoot); + debug(`Unflakable plugin is ${unflakableConfig.enabled ? "en" : "dis"}abled`); + + if (unflakableConfig.enabled) { + if (args.branch !== undefined) { + branchOverride.value = args.branch; + } + if (args.commit !== undefined) { + commitOverride.value = args.commit; + } + + if (args["failure-retries"] !== undefined) { + unflakableConfig.failureRetries = args["failure-retries"]; + } + if (args["git-auto-detect"] !== undefined) { + unflakableConfig.gitAutoDetect = args["git-auto-detect"]; + } + if (args["quarantine-mode"] !== undefined) { + unflakableConfig.quarantineMode = args[ + "quarantine-mode" + ] as QuarantineMode; + } + if (args["test-suite-id"] !== undefined) { + unflakableConfig.testSuiteId = args["test-suite-id"]; + } + if (args["upload-results"] !== undefined) { + unflakableConfig.uploadResults = args["upload-results"]; + } + + ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON.value = + JSON.stringify(unflakableConfig); + + if (args["auto-config"]) { + if (runOptions.config !== undefined) { + ENV_VAR_USER_CONFIG_JSON.value = JSON.stringify(runOptions.config); + } else { + const userConfigPath = await resolveUserConfigPath( + projectRoot, + runOptions + ); + ENV_VAR_USER_CONFIG_PATH.value = userConfigPath; + + // By default, Cypress invokes ts-node on CommonJS TypeScript projects by setting `dir` + // (deprecated alias for `cwd`) to the directory containing the Cypress config file: + // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/server/lib/plugins/child/ts_node.js#L63 + + // For both ESM and CommonJS TypeScript projects, Cypress invokes ts-node with the CWD set + // to that directory: + // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/data-context/src/data/ProjectConfigIpc.ts#L260 + + // Since we're passing our `config-wrapper.js` as the Cypress config, the CWD becomes our + // dist/ directory. However, we need ts-node to load the user's tsconfig.json, not our own, + // or the user's cypress.config.ts file may not load properly when we require()/import() + // it. + // To accomplish this, we try to discover the user's tsconfig.json by traversing the + // ancestor directories containing the user's Cypress config file. This is the same + // approach TypeScript uses: + // https://github.com/microsoft/TypeScript/blob/2beeb8b93143f75cdf788d05bb3678ce3ff0e2b3/src/compiler/program.ts#L340-L345 + + // If we find a tsconfig.json, we set the TS_NODE_PROJECT environment variable to the + // directory containing it, which ts-node then uses instead of searching the `dir` passed by + // Cypress. + const userTsConfig = await findUserTsConfig( + path.dirname(userConfigPath) + ); + if (userTsConfig !== null) { + const tsNodeProject = path.dirname(userTsConfig); + debug(`Setting TS_NODE_PROJECT to ${tsNodeProject}`); + process.env.TS_NODE_PROJECT = tsNodeProject; + } + } + } + + if (args["auto-support"] === false) { + ENV_VAR_AUTO_SUPPORT.value = "false"; + } + } + + const results = await cypress.run( + unflakableConfig.enabled + ? { + ...runOptions, + ...(args["auto-config"] + ? { + configFile: require.resolve(CONFIG_WRAPPER_MODULE), + } + : {}), + quiet: true, + } + : runOptions + ); + if (results.status === "finished") { + exitDefault(results, unflakableConfig); + } else { + exitFailure(results); + } +}; + +void main(); diff --git a/packages/cypress-plugin/src/plugin.ts b/packages/cypress-plugin/src/plugin.ts new file mode 100644 index 0000000..d5beec6 --- /dev/null +++ b/packages/cypress-plugin/src/plugin.ts @@ -0,0 +1,646 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + createTestSuiteRun, + TestAttemptResult, + TestRunAttemptRecord, + TestRunRecord, + TestSuiteManifest, + testSuiteRunUrl, +} from "@unflakable/js-api"; +import path from "path"; +import _debug from "debug"; +import { + autoDetectGit, + branchOverride, + commitOverride, + isTestQuarantined, + normalizeTestName, + UnflakableConfig, +} from "@unflakable/plugins-common"; +import { require, userAgent } from "./utils"; +import { configureMochaReporter } from "./reporter-config"; +import { + color, + colorIf, + displayRunStarting, + displayScreenshots, + displaySpecHeader, + formatPath, + formatSymbolSummary, + getWidth, + gray, + Screenshot, +} from "./vendored/print-run"; +import * as terminal from "./vendored/terminal"; +import { HorizontalAlignment, HorizontalTableRow } from "cli-table3"; +import * as humanTime from "./vendored/human_time"; +import * as duration from "./vendored/duration"; +import { aggregateRunStats, reporterStatsOrDefault } from "./reporter-common"; +import { promisify } from "es6-promisify"; +import { tmpName, TmpNameOptions } from "tmp"; +import * as fs from "fs/promises"; + +const SKIP_TESTS_MODULE = "@unflakable/cypress-plugin/skip-tests"; + +const baseDebug = _debug("unflakable:plugin"); + +export type PluginOptions = { + autoSupportFile?: boolean; +}; + +const cypressStateToResult = ( + state: string, + isQuarantined: boolean +): TestAttemptResult | null => { + switch (state) { + case "failed": + if (isQuarantined) { + return "quarantined"; + } else { + return "fail"; + } + case "passed": + return "pass"; + case "pending": + case "skipped": + return null; + default: + baseDebug(`Unexpected Cypress test state \`${state}\``); + return null; + } +}; + +const marshalAttempt = ( + attempt: CypressCommandLine.AttemptResult, + isQuarantined: boolean +): TestRunAttemptRecord | null => { + if (attempt.state === "pending" || attempt.state === "skipped") { + // Test didn't execute. + return null; + } + + const result = cypressStateToResult(attempt.state, isQuarantined); + if (result === null) { + return null; + } + + return { + start_time: new Date(attempt.startedAt).toISOString(), + // NB: there's no explicit end time for each attempt, Cypress does set the duration. + duration_ms: attempt.duration, + result, + }; +}; + +// Adapted from: +// https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/print-run.ts#L397C15-L440 +const displayResults = ( + spec: Cypress.Spec & { + relativeToCommonRoot: string; + }, + results: CypressCommandLine.RunResult +): void => { + const reporterStats = reporterStatsOrDefault(results); + + const resultColor = + reporterStats.failures + + (reporterStats.unquarantinedFlakes ?? 0) + + (reporterStats.unquarantinedSkipped ?? 0) > + 0 + ? "red" + : "green"; + + console.log(""); + + terminal.header("Results", { + color: [resultColor], + }); + + const numQuarantined = + (reporterStats.quarantinedFailures ?? 0) + + (reporterStats.quarantinedFlakes ?? 0) + + (reporterStats.quarantinedPending ?? 0); + + const table = terminal.table({ + colWidths: [14, 86], + type: "outsideBorder", + }); + + const screenshots = (results as { screenshots?: Screenshot[] }).screenshots; + + const data = ( + [ + // reporterStats.tests misses most pending tests because onTestEnd() isn't called for them. + ["Tests:", results.stats.tests], + ["Passing:", reporterStats.passes], + ["Failing:", reporterStats.failures], + (reporterStats.unquarantinedFlakes ?? 0) > 0 + ? ["Flaky:", reporterStats.unquarantinedFlakes] + : undefined, + numQuarantined > 0 ? ["Quarantined:", numQuarantined] : undefined, + ["Pending:", reporterStats.unquarantinedPending ?? 0], + [ + "Skipped:", + (reporterStats.quarantinedSkipped ?? 0) + + (reporterStats.unquarantinedSkipped ?? 0), + ], + ["Screenshots:", screenshots?.length ?? 0], + ["Video:", Boolean(results.video)], + ["Duration:", humanTime.long(results.stats.wallClockDuration ?? 0)], + [ + "Spec Ran:", + formatPath(spec.relativeToCommonRoot, getWidth(table, 1), resultColor), + ], + ] as (HorizontalTableRow | undefined)[] + ) + .filter((row): row is HorizontalTableRow => row !== undefined) + .map(([key, val]: HorizontalTableRow): HorizontalTableRow => { + return [color(key, "gray"), color(val, resultColor)]; + }); + + table.push(...data); + + console.log(""); + console.log(table.toString()); + console.log(""); + + if ((screenshots?.length ?? 0) > 0) { + displayScreenshots(screenshots); + } +}; + +// Adapted from: +// https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/print-run.ts#L70-L106 +const formatFooterSummary = ( + results: CypressCommandLine.CypressRunResult +): string[] => { + const runs = results.runs ?? []; + + const isCanceled = runs.some((run) => run.skippedSpec === true); + + const totals = aggregateRunStats(runs); + + // pass or fail color + const c = isCanceled + ? "magenta" + : totals.failedOrFlakySpecs > 0 + ? "red" + : "green"; + + const phrase = ((): string => { + if (isCanceled) { + return "The run was canceled"; + } + + // if we have any specs failing... + if (totals.failedOrFlakySpecs === 0) { + return "All specs passed!"; + } + + // number of specs + const total = runs.length; + const percent = Math.round((totals.failedOrFlakySpecs / total) * 100); + + return `${totals.failedOrFlakySpecs} of ${total} failed (${percent}%)`; + })(); + + return [ + isCanceled ? "-" : formatSymbolSummary(totals.failedOrFlakySpecs), + color(phrase, c), + gray(duration.format(results.totalDuration)), + colorIf(results.totalTests, "reset"), + colorIf(totals.passedTests, "green"), + colorIf(totals.failedTests, "red"), + colorIf(totals.flakyTests, "yellow"), + colorIf(totals.quarantinedTests + totals.quarantinedPending, "magenta"), + colorIf(totals.unquarantinedPending, "cyan"), + colorIf(totals.quarantinedSkipped + totals.unquarantinedSkipped, "blue"), + ]; +}; + +// Adapted from: +// https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/print-run.ts#L299-L395 +const renderSummaryTable = ( + results: CypressCommandLine.CypressRunResult +): void => { + const runs = results.runs ?? []; + + console.log(""); + + terminal.divider("="); + + console.log(""); + + terminal.header("Run Finished", { + color: ["reset"], + }); + + if (runs.length > 0) { + const colAligns: HorizontalAlignment[] = [ + "left", + "left", + "right", + "right", + "right", + "right", + "right", + "right", + "right", + "right", + ]; + const colWidths = [3, 29, 11, 7, 9, 9, 7, 7, 9, 9]; + + const table1 = terminal.table({ + colAligns, + colWidths, + type: "noBorder", + head: [ + "", + gray("Spec"), + "", + gray("Tests"), + gray("Passing"), + gray("Failing"), + gray("Flaky"), + gray("Quar."), + gray("Pending"), + gray("Skipped"), + ], + }); + + const table2 = terminal.table({ + colAligns, + colWidths, + type: "border", + }); + + const table3 = terminal.table({ + colAligns, + colWidths, + type: "noBorder", + head: formatFooterSummary(results), + }); + + runs.forEach((run): number => { + const reporterStats = reporterStatsOrDefault(run); + + const ms = duration.format(run.stats.wallClockDuration ?? 0); + + const formattedSpec = formatPath( + run.spec.relativeToCommonRoot, + getWidth(table2, 1) + ); + + if (run.skippedSpec) { + return table2.push([ + "-", + formattedSpec, + color("SKIPPED", "gray"), + "-", + "-", + "-", + "-", + "-", + "-", + "-", + ]); + } + + return table2.push([ + formatSymbolSummary( + Math.max( + reporterStats.failures + (reporterStats.unquarantinedFlakes ?? 0), + (reporterStats.unquarantinedSkipped ?? 0) > 0 ? 1 : 0 + ) + ), + formattedSpec, + color(ms, "gray"), + colorIf(run.stats.tests, "reset"), + colorIf(reporterStats.passes, "green"), + colorIf(reporterStats.failures, "red"), + colorIf(reporterStats.unquarantinedFlakes ?? 0, "yellow"), + colorIf( + (reporterStats.quarantinedFailures ?? 0) + + (reporterStats.quarantinedFlakes ?? 0) + + (reporterStats.quarantinedPending ?? 0), + "magenta" + ), + colorIf(reporterStats.unquarantinedPending ?? 0, "cyan"), + colorIf( + (reporterStats.quarantinedSkipped ?? 0) + + (reporterStats.unquarantinedSkipped ?? 0), + "blue" + ), + ]); + }); + + console.log(""); + console.log(""); + console.log(terminal.renderTables(table1, table2, table3)); + console.log(""); + } +}; + +export class UnflakableCypressPlugin { + private readonly apiKey: string; + private readonly manifest: TestSuiteManifest | null; + private readonly repoRoot: string; + private readonly unflakableConfig: UnflakableConfig; + + private specs: Cypress.Spec[] | null = null; + private specIndex = 0; + private supportFilePath: string | null = null; + + constructor({ + apiKey, + manifest, + repoRoot, + unflakableConfig, + }: { + apiKey: string; + manifest: TestSuiteManifest | undefined; + repoRoot: string; + unflakableConfig: UnflakableConfig; + }) { + this.apiKey = apiKey; + this.manifest = manifest ?? null; + this.repoRoot = repoRoot; + this.unflakableConfig = unflakableConfig; + } + + public register = async ( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions, + options: PluginOptions + ): Promise => { + const debug = baseDebug.extend("register"); + + if (!this.unflakableConfig.enabled) { + debug("Unflakable plugin is disabled"); + return config; + } + + configureMochaReporter( + config, + this.unflakableConfig, + this.manifest ?? undefined, + this.repoRoot + ); + + if ( + this.unflakableConfig.quarantineMode === "skip_tests" && + options.autoSupportFile !== false + ) { + const updatedSupportFile = await this.generateSkipTestsSupportFile( + config.supportFile + ); + this.supportFilePath = updatedSupportFile; + config.supportFile = updatedSupportFile; + } + + on("before:run", this.onBeforeRun.bind(this)); + on("after:run", this.onAfterRun.bind(this)); + on("before:spec", this.onBeforeSpec.bind(this)); + on("after:spec", this.onAfterSpec.bind(this)); + + config.retries = { + runMode: this.unflakableConfig.failureRetries, + }; + return config; + }; + + private generateSkipTestsSupportFile = async ( + configuredSupportFile: string | false + ): Promise => { + const debug = baseDebug.extend("generateSkipTestsSupportFile"); + + // We have to write a temporary Cypress support file on the fly because Cypress can't load + // support files inside of node_modules. See https://github.com/cypress-io/cypress/issues/23616. + // Once Cypress loads our support file, it's fine to load other modules that live in + // node_modules. + const supportFilePath = + (await promisify(tmpName)({ + prefix: "unflakable-cypress-support-file", + })) + ".js"; + debug(`Using temp path \`${supportFilePath}\` for Cypress support file`); + + const skipTestsPath = require.resolve(SKIP_TESTS_MODULE); + debug(`Support file will load skip-tests from ${skipTestsPath}`); + + if (configuredSupportFile !== false) { + debug(`Will load existing support file from ${configuredSupportFile}`); + } + + const supportFileContents = ` +require(${JSON.stringify(skipTestsPath)}).registerMochaInstrumentation(); +${ + configuredSupportFile !== false + ? `require(${JSON.stringify(configuredSupportFile)});` + : "" +} + `; + + await fs.writeFile( + supportFilePath, + Buffer.from(supportFileContents, "utf8"), + { + // Don't overwrite existing files. + flag: "wx", + } + ); + + return supportFilePath; + }; + + private uploadResults = async ( + results: CypressCommandLine.CypressRunResult, + testRuns: TestRunRecord[] + ): Promise => { + const debug = baseDebug.extend("uploadResults"); + + debug("Reporting results to Unflakable"); + + let branch = branchOverride.value, + commit = commitOverride.value; + + if ( + this.unflakableConfig.gitAutoDetect && + (branch === undefined || + branch.length === 0 || + commit === undefined || + commit.length === 0) + ) { + const { branch: gitBranch, commit: gitCommit } = await autoDetectGit( + console.error.bind(console) + ); + + if (branch === undefined || branch.length === 0) { + branch = gitBranch; + } + if (commit === undefined || commit.length === 0) { + commit = gitCommit; + } + } + + const userAgentStr = userAgent(results.cypressVersion); + + debug("Reporting results to Unflakable"); + const testSuiteRun = await createTestSuiteRun({ + request: { + branch, + commit, + start_time: new Date(results.startedTestsAt).toISOString(), + end_time: new Date(results.endedTestsAt).toISOString(), + test_runs: testRuns, + }, + testSuiteId: this.unflakableConfig.testSuiteId, + apiKey: this.apiKey, + baseUrl: this.unflakableConfig.apiBaseUrl, + clientDescription: userAgentStr, + }).catch((e) => + Promise.reject( + new Error(`failed to report results to Unflakable: ${e as string}`) + ) + ); + + console.log( + "Unflakable report: " + + testSuiteRunUrl( + testSuiteRun.suite_id, + testSuiteRun.run_id, + this.unflakableConfig.apiBaseUrl + ) + ); + }; + + private onBeforeRun = ({ + browser, + config, + group, + parallel, + runUrl, + specPattern, + specs, + tag, + autoCancelAfterFailures, + }: Cypress.BeforeRunDetails & { + // Added in Cypress 12.6: https://github.com/cypress-io/cypress/pull/25237. + autoCancelAfterFailures?: number | false; + }): void => { + const debug = baseDebug.extend("beforeRun"); + debug("Received beforeRun event"); + + this.specs = specs ?? []; + displayRunStarting({ + // Some of these public types seem wrong since Cypress passes the same values to the + // `before:run` event as it does to displayRunStarting(): + // https://github.com/cypress-io/cypress/blob/3d0a2b406115db292130df774348c4f1fd4a3240/packages/server/lib/modes/run.ts#L733-L806 + browser: browser as Cypress.Browser, + config: config as Pick & + Pick< + Cypress.ResolvedConfigOptions, + "resolvedNodeVersion" | "resolvedNodePath" + >, + group, + parallel, + runUrl, + specPattern: specPattern as string[] | RegExp | string, + specs: specs as CypressCommandLine.RunResult["spec"][], + tag, + autoCancelAfterFailures, + }); + }; + + private onAfterRun = async ( + results: + | CypressCommandLine.CypressRunResult + | CypressCommandLine.CypressFailedRunResult + ): Promise => { + const debug = baseDebug.extend("afterRun"); + debug("Received afterRun event"); + + if (results.status === "finished") { + renderSummaryTable(results); + + const testRuns = results.runs.flatMap(({ spec, tests }) => + // Contrary to Cypress's TypeScript typing, tests can be `null` when the specs fail to + // compile (e.g., due to Webpack errors). + (tests ?? []) + .map((test): TestRunRecord => { + const filename = path.relative(this.repoRoot, spec.absolute); + const isQuarantined = + this.manifest !== null && + this.unflakableConfig.quarantineMode !== "no_quarantine" && + isTestQuarantined(this.manifest, filename, test.title); + return { + filename, + name: normalizeTestName(test.title), + attempts: test.attempts + .map((attempt) => marshalAttempt(attempt, isQuarantined)) + .filter( + (attempt): attempt is TestRunAttemptRecord => attempt !== null + ), + }; + }) + .filter((test) => test.attempts.length > 0) + ); + debug(`Test results: ${JSON.stringify(testRuns)}`); + + if (!this.unflakableConfig.uploadResults) { + debug( + "Not reporting results to Unflakable because configuration option `uploadResults` is false" + ); + } else if (testRuns.length === 0) { + debug("No results to report to Unflakable"); + } else { + await this.uploadResults(results, testRuns); + } + } + + if (this.supportFilePath !== null) { + debug(`Deleting temp support file ${this.supportFilePath}`); + await fs.unlink(this.supportFilePath); + } + }; + + private onBeforeSpec = (spec: Cypress.Spec): void => { + const debug = baseDebug.extend("onBeforeSpec"); + debug(`Received onBeforeSpec event for spec ${spec.relative}`); + + displaySpecHeader( + ( + spec as Cypress.Spec & { + relativeToCommonRoot: string; + } + ).relativeToCommonRoot, + ++this.specIndex, + this.specs?.length ?? 0, + // Spec duration estimates are a Cypress Cloud feature, and we don't have access to the API + // response: + // https://github.com/cypress-io/cypress/blob/2a17efac74111b0a723af0e5c186e73d18c688bd/packages/server/lib/modes/record.js#L674 + 0 + ); + }; + + private onAfterSpec = ( + spec: Cypress.Spec, + results: CypressCommandLine.RunResult + ): void => { + const debug = baseDebug.extend("onAfterSpec"); + debug( + `Received onAfterSpec event for spec ${spec.relative} with results %o`, + results + ); + + // This can be set by Cypress Cloud. + if (!results.skippedSpec) { + displayResults( + spec as Cypress.Spec & { + relativeToCommonRoot: string; + }, + results + ); + } + }; +} diff --git a/packages/cypress-plugin/src/reporter-common.ts b/packages/cypress-plugin/src/reporter-common.ts new file mode 100644 index 0000000..11d6a62 --- /dev/null +++ b/packages/cypress-plugin/src/reporter-common.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Reporter stats from running a single spec. +export type ReporterStats = Mocha.Stats & { + quarantinedFailures?: number; + quarantinedFlakes?: number; + unquarantinedFlakes?: number; + quarantinedPending?: number; + unquarantinedPending?: number; + quarantinedSkipped?: number; + unquarantinedSkipped?: number; +}; + +// Aggregated reporter stats across all specs. +export type TotalReporterStats = { + failedTests: number; + failedOrFlakySpecs: number; + flakyTests: number; + passedTests: number; + quarantinedPending: number; + unquarantinedPending: number; + quarantinedTests: number; + quarantinedSkipped: number; + unquarantinedSkipped: number; +}; + +// On certain spec errors (e.g., if webpack compilation fails for component tests), reporterStats +// will be `null`. In that case, we just populate the reporter stats from Cypress's stats. +export const reporterStatsOrDefault = ( + results: CypressCommandLine.RunResult +): ReporterStats => + results.reporterStats !== null + ? (results.reporterStats as ReporterStats) + : { + suites: results.stats.suites, + tests: results.stats.tests, + passes: results.stats.passes, + pending: results.stats.pending, + failures: results.stats.failures, + }; + +export const aggregateRunStats = ( + runs: CypressCommandLine.RunResult[] +): TotalReporterStats => + runs.reduce( + ( + totals: TotalReporterStats, + run: CypressCommandLine.RunResult + ): TotalReporterStats => { + const reporterStats = reporterStatsOrDefault(run); + return { + failedTests: totals.failedTests + reporterStats.failures, + failedOrFlakySpecs: + totals.failedOrFlakySpecs + + (reporterStats.failures + + (reporterStats.unquarantinedFlakes ?? 0) + + (reporterStats.unquarantinedSkipped ?? 0) > + 0 + ? 1 + : 0), + flakyTests: + totals.flakyTests + (reporterStats.unquarantinedFlakes ?? 0), + passedTests: totals.passedTests + reporterStats.passes, + quarantinedPending: + totals.quarantinedPending + (reporterStats.quarantinedPending ?? 0), + unquarantinedPending: + totals.unquarantinedPending + + (reporterStats.unquarantinedPending ?? 0), + quarantinedTests: + totals.quarantinedTests + + (reporterStats.quarantinedFailures ?? 0) + + (reporterStats.quarantinedFlakes ?? 0), + quarantinedSkipped: + totals.quarantinedSkipped + (reporterStats.quarantinedSkipped ?? 0), + unquarantinedSkipped: + totals.unquarantinedSkipped + + (reporterStats.unquarantinedSkipped ?? 0), + }; + }, + { + failedTests: 0, + failedOrFlakySpecs: 0, + flakyTests: 0, + quarantinedPending: 0, + unquarantinedPending: 0, + quarantinedTests: 0, + passedTests: 0, + quarantinedSkipped: 0, + unquarantinedSkipped: 0, + } + ); diff --git a/packages/cypress-plugin/src/reporter-config.test.ts b/packages/cypress-plugin/src/reporter-config.test.ts new file mode 100644 index 0000000..55d40b6 --- /dev/null +++ b/packages/cypress-plugin/src/reporter-config.test.ts @@ -0,0 +1,231 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { configureMochaReporter } from "./reporter-config"; +import { QuarantineMode } from "@unflakable/plugins-common"; +import { expect, it } from "@jest/globals"; +import path from "path"; +import { require } from "./utils"; +import { promisify } from "es6-promisify"; +import { tmpName, TmpNameOptions } from "tmp"; +import * as fs from "fs/promises"; + +describe("configureMochaReporter", () => { + const unflakableConfig = { + apiBaseUrl: undefined, + enabled: true, + failureRetries: 2, + gitAutoDetect: true, + quarantineMode: "ignore_failures" as QuarantineMode, + testSuiteId: "MOCK_SUITE_ID", + uploadResults: true, + }; + + const cypressMultiReportersPath = path.join( + "..", + "..", + "..", + require.resolve("cypress-multi-reporters") + ); + const projectRoot = "/foo/bar/baz"; + const repoRoot = "/foo/bar"; + const reporterRepoRelPath = "mock_node_modules/cypress-plugin/dist/reporter"; + const reporterAbsPath = path.join(repoRoot, reporterRepoRelPath); + + it.each(["", "spec"])('spec reporter w/ reporter="%s"', (reporter) => { + const config = { + projectRoot, + reporter, + reporterOptions: {}, + }; + + configureMochaReporter( + config, + unflakableConfig, + undefined, + repoRoot, + reporterAbsPath + ); + + expect(config).toStrictEqual({ + projectRoot, + reporter: path.join("..", reporterRepoRelPath), + reporterOptions: { + config: unflakableConfig, + manifest: undefined, + projectRoot, + repoRoot, + }, + }); + }); + + it.each([ + "@custom/reporter", + "@custom/reporter, spec", + ["@custom/reporter"], + ["@custom/reporter", "spec"], + ])( + 'cypress-multi-reporters w/ reporterEnabled="%s"', + async (reporterEnabled) => { + const cypressMultiReportersConfigPath = + (await promisify(tmpName)({ + prefix: "unflakable-cypress-multi-reporters-config", + })) + ".json"; + + await fs.writeFile( + cypressMultiReportersConfigPath, + Buffer.from( + JSON.stringify({ + reporterEnabled, + customReporterReporterOptions: { + foo: "bar", + }, + }), + "utf8" + ), + { + // Don't overwrite existing files. + flag: "wx", + } + ); + + const config = { + projectRoot, + reporter: path.dirname(cypressMultiReportersPath), + reporterOptions: { + configFile: cypressMultiReportersConfigPath, + }, + }; + + configureMochaReporter( + config, + unflakableConfig, + undefined, + repoRoot, + reporterAbsPath + ); + + expect(config).toStrictEqual({ + projectRoot, + reporter: path.dirname(cypressMultiReportersPath), + reporterOptions: { + configFile: null, + customReporterReporterOptions: { + foo: "bar", + }, + reporterEnabled: `@unflakable/cypress-plugin/reporter, @custom/reporter`, + unflakableCypressPluginReporterReporterOptions: { + config: unflakableConfig, + manifest: undefined, + projectRoot, + repoRoot, + }, + }, + }); + } + ); + + it("junit reporter", () => { + const config = { + projectRoot, + reporter: "junit", + reporterOptions: {}, + }; + + configureMochaReporter( + config, + unflakableConfig, + undefined, + repoRoot, + reporterAbsPath + ); + + expect(config).toStrictEqual({ + projectRoot, + reporter: cypressMultiReportersPath, + reporterOptions: { + mochaJunitReporterReporterOptions: {}, + reporterEnabled: `@unflakable/cypress-plugin/reporter, mocha-junit-reporter`, + unflakableCypressPluginReporterReporterOptions: { + config: unflakableConfig, + manifest: undefined, + projectRoot, + repoRoot, + }, + }, + }); + }); + + it("tap reporter", () => { + const config = { + projectRoot, + reporter: "tap", + reporterOptions: { + tapVersion: "12", + }, + }; + + configureMochaReporter( + config, + unflakableConfig, + undefined, + repoRoot, + reporterAbsPath + ); + + expect(config).toStrictEqual({ + projectRoot, + reporter: cypressMultiReportersPath, + reporterOptions: { + tapReporterOptions: { + tapVersion: "12", + }, + reporterEnabled: `@unflakable/cypress-plugin/reporter, tap`, + unflakableCypressPluginReporterReporterOptions: { + config: unflakableConfig, + manifest: undefined, + projectRoot, + repoRoot, + }, + }, + }); + }); + + it("custom reporter", () => { + const config = { + projectRoot, + reporter: "custom", + reporterOptions: { + foo: "bar", + }, + }; + + configureMochaReporter( + config, + unflakableConfig, + undefined, + repoRoot, + reporterAbsPath + ); + + expect(config).toStrictEqual({ + projectRoot, + reporter: cypressMultiReportersPath, + reporterOptions: { + fooBarBazCustomReporterOptions: { + foo: "bar", + }, + // Absolute path to custom. + reporterEnabled: `@unflakable/cypress-plugin/reporter, ${path.join( + projectRoot, + "custom" + )}`, + unflakableCypressPluginReporterReporterOptions: { + config: unflakableConfig, + manifest: undefined, + projectRoot, + repoRoot, + }, + }, + }); + }); +}); diff --git a/packages/cypress-plugin/src/reporter-config.ts b/packages/cypress-plugin/src/reporter-config.ts new file mode 100644 index 0000000..d8e4951 --- /dev/null +++ b/packages/cypress-plugin/src/reporter-config.ts @@ -0,0 +1,227 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { ReporterConfig } from "./reporter"; +import { printWarning, require } from "./utils"; +import path from "path"; +import { reporters } from "mocha"; +import _debug from "debug"; +import { TestSuiteManifest } from "@unflakable/js-api"; +import { UnflakableConfig } from "@unflakable/plugins-common"; +import _ from "lodash"; +import chalk from "chalk"; +import process from "process"; + +const debug = _debug("unflakable:reporterConfig"); + +const MOCHA_REPORTER_MODULE = "@unflakable/cypress-plugin/reporter"; + +// This uses lodash to mirror cypress-multi-reporters's resolution of reporter names to keys within +// config.reporterOptions. See: +// https://github.com/YOU54F/cypress-plugins/blob/8c559869f3cadc0599eadae980d73472580d2077/cypress-multi-reporters/lib/MultiReporters.js#L204 +const cypressMultiReportersOptionsKey = (name: string): string => + _.camelCase(name) + "ReporterOptions"; + +// Loads the existing cypress-multi-reporters config so that we can add our reporter to it. See: +// https://github.com/YOU54F/cypress-plugins/blob/8c559869f3cadc0599eadae980d73472580d2077/cypress-multi-reporters/lib/MultiReporters.js#L140-L197 +const loadMultiReportersConfig = ( + config: Pick +): { [key: string]: unknown } => { + const multiReporterConfigFilePath = config.reporterOptions?.configFile as + | string + | undefined; + + if (multiReporterConfigFilePath === undefined) { + return config.reporterOptions ?? {}; + } + + try { + const resolvedPath = path.resolve(multiReporterConfigFilePath); + debug( + `Loading cypress-multi-reporters config file ${multiReporterConfigFilePath} (resolved to ${resolvedPath})` + ); + + // We use require here to support both JSON and executable .js config files (as + // cypress-multi-reporters does). + // eslint-disable-next-line @typescript-eslint/no-var-requires + const config = require(resolvedPath) as { + [key: string]: unknown; + }; + debug(`Loaded cypress-multi-reporters config: %o`, config); + + if (config.configFile !== undefined && config.configFile !== null) { + // Nested config files aren't supported by cypress-multi-reporters anyway, but if we leave + // it set, the package will ignore the rest of our config. + printWarning( + `Ignoring nested cypress-multi-reporters \`configFile\` in config file ${ + config.configFile as string + }` + ); + } + + // We need to clear configFile or Cypress will merge the reporterOptions we return with the + // original one and keep the configFile. + config.configFile = null; + + // If there's a configFile, all the other reporterOptions get ignored. + return config; + } catch (e) { + console.error( + chalk.red( + `Failed to load cypress-multi-reporters config from ${multiReporterConfigFilePath}: ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + e + }` + ) + ); + process.exit(1); + } +}; + +export const configureMochaReporter = ( + config: Pick< + Cypress.PluginConfigOptions, + "projectRoot" | "reporter" | "reporterOptions" + >, + unflakableConfig: UnflakableConfig, + manifest: TestSuiteManifest | undefined, + repoRoot: string, + reporterAbsPath: string = require.resolve(MOCHA_REPORTER_MODULE) +): void => { + // Cypress assumes that reporters are either (1) relative paths within the projectRoot or + // (2) inside a node_modules directory in the projectRoot + // (see https://github.com/cypress-io/cypress/issues/18922). As a workaround, we resolve the + // path to the reporter using Node's module resolution, and then convert that path to one + // that's relative to the projectRoot so that Cypress can find and load it. + + debug(`Configured reporter: ${config.reporter}`); + debug( + `Configured reporterOptions: ${JSON.stringify(config.reporterOptions)}` + ); + + const unflakableReporterConfig: ReporterConfig = { + config: unflakableConfig, + manifest, + projectRoot: config.projectRoot, + repoRoot, + }; + + // NB: If a reporter other than `spec` is being used, it's likely it won't handle retries + // correctly and will only report the final attempt. This means flakes will probably be + // considered passes by other reporters, and other reporters will of course not be aware of + // quarantining. Still, we make our best effort to support other reporters being used, which + // should report the expected results most of the time, when tests do not flake. + if (["", "spec"].includes(config.reporter)) { + if ( + // NB: typeof null is "object" + typeof config.reporterOptions === "object" && + config.reporterOptions !== null && + Object.entries(config.reporterOptions).length > 0 + ) { + printWarning( + `Ignoring unsupported Mocha reporterOptions: ${JSON.stringify( + config.reporterOptions + )}` + ); + } + + debug(`Absolute path to reporter: ${reporterAbsPath}`); + + const reporterRelPath = path.relative(config.projectRoot, reporterAbsPath); + debug(`Project-relative path to reporter: ${reporterRelPath}`); + + config.reporter = reporterRelPath; + config.reporterOptions = unflakableReporterConfig; + } else if (/[\\/]?cypress-multi-reporters$/.test(config.reporter)) { + // If cypress-multi-reporters is already being used, add our reporter to it and remove the + // default spec reporter if it's being used. + + const cypressMultiReportersConfig = loadMultiReportersConfig(config); + const nonSpecReporters = ( + Array.isArray(cypressMultiReportersConfig.reporterEnabled) + ? (cypressMultiReportersConfig.reporterEnabled as string[]) + : typeof cypressMultiReportersConfig.reporterEnabled === "string" + ? cypressMultiReportersConfig.reporterEnabled.split(",") + : [] + ) + .map((reporter) => reporter.trim()) + .filter((reporter) => reporter !== "spec"); + + config.reporterOptions = { + ...cypressMultiReportersConfig, + reporterEnabled: [MOCHA_REPORTER_MODULE, ...nonSpecReporters].join(", "), + [cypressMultiReportersOptionsKey(MOCHA_REPORTER_MODULE)]: + unflakableReporterConfig, + }; + } else { + // Cypress resolves reporters in a particular way: + // - "teamcity" (mocha-teamcity-reporter) and "junit" (mocha-junit-reporter) are + // special-cased - Mocha's builtin reporters are passed through unmodified as string - others + // are resolved relative to projectRoot; if that fails, they're resolved relative to + // `${projectRoot}/node_modules` See: + // https://github.com/cypress-io/cypress/blob/b0c0eaa508bb6dafdc1997bc00fb7ed6f5bcc160/packages/server/lib/reporter.js#L495-L548 + + // Since we're introducing cypress-multi-reporters here, we need to convert the reporter + // name/path from the representation Cypress expects to the one cypress-multi-reporters + // expects. For built-in Mocha reporters, we don't need to do anything. For others, we need + // to + // convert them to a path supported by require(), which we do by simply passing the absolute + // path to the module found relative to projectRoot or `${projectRoot}/node_modules`. + // See: + // https://github.com/YOU54F/cypress-plugins/blob/8c559869f3cadc0599eadae980d73472580d2077/cypress-multi-reporters/lib/MultiReporters.js#L66-L99 + + // There's a special case for junit and teamcity, see: + // https://github.com/cypress-io/cypress/blob/b0c0eaa508bb6dafdc1997bc00fb7ed6f5bcc160/packages/server/lib/reporter.js#L504-L511 + if (["junit", "teamcity"].includes(config.reporter)) { + const reporterModule = `mocha-${config.reporter}-reporter`; + debug( + `Configured reporter \`${config.reporter}\` is a Cypress special case mapped to \`${reporterModule}\`` + ); + config.reporterOptions = { + reporterEnabled: [MOCHA_REPORTER_MODULE, reporterModule].join(", "), + [cypressMultiReportersOptionsKey(reporterModule)]: + config.reporterOptions ?? {}, + [cypressMultiReportersOptionsKey(MOCHA_REPORTER_MODULE)]: + unflakableReporterConfig, + }; + } else if (Object.keys(reporters).includes(config.reporter)) { + debug(`Configured reporter \`${config.reporter}\` is a Mocha reporter`); + config.reporterOptions = { + reporterEnabled: [MOCHA_REPORTER_MODULE, config.reporter].join(", "), + [cypressMultiReportersOptionsKey(config.reporter)]: + config.reporterOptions ?? {}, + [cypressMultiReportersOptionsKey(MOCHA_REPORTER_MODULE)]: + unflakableReporterConfig, + }; + } else { + const userReporterAbsPath = path.join( + config.projectRoot, + config.reporter + ); + debug( + `Resolving reporter \`${config.reporter}\` to absolute path: ${userReporterAbsPath}` + ); + + config.reporterOptions = { + reporterEnabled: [MOCHA_REPORTER_MODULE, userReporterAbsPath].join( + ", " + ), + [cypressMultiReportersOptionsKey(userReporterAbsPath)]: + config.reporterOptions ?? {}, + [cypressMultiReportersOptionsKey(MOCHA_REPORTER_MODULE)]: + unflakableReporterConfig, + }; + } + + const reporterAbsPath = require.resolve("cypress-multi-reporters"); + debug(`Absolute path to cypress-multi-reporters: ${reporterAbsPath}`); + + const reporterRelPath = path.relative(config.projectRoot, reporterAbsPath); + debug( + `Project-relative path to cypress-multi-reporters: ${reporterRelPath}` + ); + config.reporter = reporterRelPath; + } + + debug(`Updated reporter: ${config.reporter}`); + debug(`Updated reporterOptions: ${JSON.stringify(config.reporterOptions)}`); +}; diff --git a/packages/cypress-plugin/src/reporter.ts b/packages/cypress-plugin/src/reporter.ts new file mode 100644 index 0000000..5e24966 --- /dev/null +++ b/packages/cypress-plugin/src/reporter.ts @@ -0,0 +1,900 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import _debug from "debug"; +import { + CypressTest, + MochaOptions, + Runner, + reporters, + Suite, + utils as mochaUtils, + MochaEventTest, +} from "mocha"; +import { TestSuiteManifest } from "@unflakable/js-api"; +import { + isTestQuarantined, + UnflakableConfig, +} from "@unflakable/plugins-common"; +import path from "path"; +import milliseconds from "ms"; +import deepEqual from "deep-equal"; +import { printWarning } from "./utils"; +import { ReporterStats } from "./reporter-common"; +import styles, { ForegroundColor } from "ansi-styles"; + +const debug = _debug("unflakable:reporter"); + +const useColors = reporters.Base.useColors; + +export type ReporterConfig = { + config: UnflakableConfig; + manifest: TestSuiteManifest | undefined; + projectRoot: string; + repoRoot: string; +}; + +const currentTestRetry = ( + test: Omit +): number => + /* eslint-disable @typescript-eslint/ban-ts-comment */ + // @ts-ignore: currentRetry is protected, but we need to access it anyway... + typeof test.currentRetry === "number" + ? // @ts-ignore: see above. + (test.currentRetry as unknown as number) + : // @ts-ignore: see above. + typeof test.currentRetry === "function" + ? // @ts-ignore: see above. + (test.currentRetry as () => number)() + : /* eslint-enable @typescript-eslint/ban-ts-comment */ + 0; + +const testRetries = (test: Omit): number => + typeof test.retries === "number" + ? (test.retries as unknown as number) + : typeof test.retries === "function" + ? test.retries() + : 1; + +const retryAttempt = ( + test: CypressTest | MochaEventTest, + isRetry: boolean +): string => { + const currentRetry = currentTestRetry(test); + + return isRetry || currentRetry > 0 + ? reporters.Base.color( + "medium", + ` (attempt ${currentRetry + 1} of ${testRetries(test) + 1})` + ) + : ""; +}; + +const testDepth = (test: CypressTest | Suite): number => + test.parent !== undefined ? 1 + testDepth(test.parent) : 0; + +const stringifyDiffObjs = (err: Mocha.Error): void => { + if (typeof err.actual !== "string" || typeof err.expected !== "string") { + err.actual = mochaUtils.stringify(err.actual); + err.expected = mochaUtils.stringify(err.expected); + } +}; + +const mergeTestFailure = ( + src: { err?: Mocha.Error }, + dest: { err?: Mocha.Error } +): void => { + if ( + src.err !== undefined && + // When the last retry fails, both onTestFail() and onTestEnd() get called even if there's only + // one error, so we need to dedupe the errors here. + !deepEqual(src.err, dest.err) + ) { + // We already recorded this test failure, but it has multiple exceptions, so we need to track + // those. + if (dest.err !== undefined) { + dest.err.multiple = [ + src.err, + ...(src.err.multiple ?? []), + ...(dest.err.multiple ?? []), + ]; + } else { + dest.err = src.err; + } + } +}; + +// Returns true if a new test was added, or false if the test failure was merged with an existing +// test. +const pushOrMergeFailedTest = ( + failedTests: TestWithError[], + test: T & { hookName?: string } +): boolean => { + const currentRetry = currentTestRetry(test); + + if (test.err !== undefined && test.hookName !== undefined) { + const hookMsg = `"${test.hookName}" hook failed:\n`; + if (test.err.message !== undefined) { + // This is consumed by formatErrorMsgAndStack(). + if (test.err.stack !== undefined) { + const indexIntoStackOrMsg = test.err.stack.indexOf(test.err.message); + if (indexIntoStackOrMsg !== -1) { + test.err.stack = + test.err.stack.slice(0, indexIntoStackOrMsg) + + hookMsg + + test.err.stack.slice(indexIntoStackOrMsg); + } + } + test.err.message = hookMsg + test.err.message; + } + } + + // NB: There's an edge case involving tests that have multiple errors. Ordinarily, onTestFail() + // doesn't get called until the final retry fails. However, if multiple errors are thrown by + // a single test (see https://github.com/mochajs/mocha/pull/4033), onTestFail() will get called + // first, followed by onTestRetry() (if there will be subsequent attempts) or onTestEnd() (if + // it's the final retry). + const existingTestFailure = failedTests + // This test will probably be the last one in this.retriedTests, but that may not be + // guaranteed (especially if tests run in parallel) due to the async nature of Cypress. + .find( + (existingTestFailure) => + existingTestFailure.test.id === test.id && + currentTestRetry(existingTestFailure.test) === currentRetry + ); + + if (existingTestFailure === undefined) { + // At this point, it's too early to determine whether the test is a flake or a failure, which + // depends on whether any of the retries will pass. + failedTests.push({ test, err: test.err }); + return true; + } else { + mergeTestFailure(test, existingTestFailure); + return false; + } +}; + +const formatFailedTestTitle = (titlePath: TestTitle): string => + titlePath.reduce( + (testTitle: string, str: string, index: number) => + testTitle + + (index > 0 ? "\n " : "") + + Array(index + 1).join(" ") + + str, + "" + ); + +const formatErrorMsgAndStack = (err: Mocha.Error): [string, string] => { + const messageRaw = + err.message !== undefined && typeof err.message.toString === "function" + ? String(err.message) + : typeof err.inspect === "function" + ? String(err.inspect()) + : ""; + const message = messageRaw !== "" ? messageRaw : "Error"; + + const stackOrMsg = err.stack ?? message; + + const indexIntoStackOrMsg = stackOrMsg.indexOf(message); + const msg = + indexIntoStackOrMsg === -1 + ? message + : stackOrMsg.slice(0, indexIntoStackOrMsg + message.length); + const stack = + indexIntoStackOrMsg === -1 + ? stackOrMsg + : stackOrMsg.slice(indexIntoStackOrMsg + message.length + 1); + + if (reporters.Base.hideDiff !== true && reporters.Base.showDiff(err)) { + stringifyDiffObjs(err); + + const match = message.match(/^([^:]+): expected/); + return [ + (match !== null ? match[1] : msg) + + reporters.Base.generateDiff( + err.actual as string, + err.expected as string + // Un-indent to prevent duplicate indent since we do our own indenting of multi-line + // error messages. + ).replace(/^ {6}/gm, " "), + stack, + ]; + } else { + return [msg, stack]; + } +}; + +type FailedTestAttempt = { + currentRetry: number; + err: Mocha.Error | undefined; + retries: number; +}; + +const reportFailedAttempt = ({ + attemptIndex, + failureIdx, + formattedTestTitle, + numAttempts, + testAttempt, +}: { + attemptIndex: number; + failureIdx: number; + formattedTestTitle: string; + numAttempts: number; + testAttempt: FailedTestAttempt; +}): void => { + const errors: string = [ + // NB: We prepend here because the second error gets passed to us first, for some reason. + ...(testAttempt.err?.multiple ?? []), + ...(testAttempt.err !== undefined ? [testAttempt.err] : []), + ] + .map(formatErrorMsgAndStack) + .reduce( + (output, [msg, stack]) => + output + + (output !== "" ? "\n" : "") + + reporters.Base.color("error message", msg.replace(/^/gm, " ")) + + reporters.Base.color( + "error stack", + "\n" + stack.replace(/^/gm, " ") + "\n" + ), + "" + ); + + console.log( + reporters.Base.color( + "error title", + // Only print the failure number on the first attempt we show (since each failed test + // is only counted once in the stats, and this numbering should match). + (attemptIndex === 0 ? ` ${failureIdx + 1}) ` : " ") + + formattedTestTitle + ) + + (numAttempts > 1 + ? reporters.Base.color( + "medium", + ` (attempt ${attemptIndex + 1} of ${numAttempts})` + ) + : "") + + reporters.Base.color("error title", errors !== "" ? ":\n" : "") + + errors + ); +}; + +// Cypress mutates the runnable in response to Mocha events, which can overwrite our modifications +// to the test's `err` field (i.e., its `multiple` field). We store a separate reference to the +// error to prevent this. +type TestWithError = { + // Omit the `err` field from the type so that we don't accidentally use it. + test: Omit; + err: Mocha.Error | undefined; +}; + +type TestTitle = string[]; + +// This reporter is a quarantine- and retry-aware TypeScript adaptation of Mocha's spec reporter: +// https://github.com/mochajs/mocha/blob/ccee5f1b37bb405b81814daa35c63801cad20b4d/lib/reporters/spec.js +export default class UnflakableSpecReporter extends reporters.Base { + static readonly description = "Unflakable reporter: hierarchical & verbose"; + + private readonly config: UnflakableConfig; + private readonly manifest: TestSuiteManifest | null; + private readonly testFilename: string | null; + + private indents = 0; + + // Used for computing the set of skipped tests from each suite. + private nonSkippedTestJsonTitlePaths: Set = new Set(); + private specTests: TestTitle[] = []; + + // Non-final failed test attempts. + private retriedTests: TestWithError[] = []; + + // Final failed test attempts. + private quarantinedFailures: TestWithError[] = []; + private unquarantinedFailures: TestWithError[] = []; + private quarantinedFlakes: TestWithError[] = []; + private unquarantinedFlakes: TestWithError[] = []; + private quarantinedPending: CypressTest[] = []; + private unquarantinedPending: CypressTest[] = []; + + constructor(runner: Runner, options?: MochaOptions) { + super(runner, options); + + debug( + `Constructing runner for suite title=\`${runner.suite.title}\` file=\`${ + runner.suite.file ?? "" + }\`` + ); + + if ( + // NB: typeof null is "object" + typeof options?.reporterOptions !== "object" || + options?.reporterOptions === null + ) { + throw new Error("Reporter config not found"); + } + const { config, manifest, projectRoot, repoRoot } = + options.reporterOptions as ReporterConfig; + this.config = config; + this.manifest = manifest ?? null; + this.testFilename = + runner.suite.file !== undefined + ? path.relative( + repoRoot, + // Absolute path of the spec file. + path.join(projectRoot, runner.suite.file) + ) + : // This can happen if a file has no tests, which should be ok since none of the event + // handlers should get called. + null; + debug(`Repo-relative test filename: ${this.testFilename ?? "null"}`); + + runner.on(Runner.constants.EVENT_RUN_BEGIN, this.onRunBegin.bind(this)); + runner.once(Runner.constants.EVENT_RUN_END, this.onRunEnd.bind(this)); + + runner.on(Runner.constants.EVENT_SUITE_BEGIN, this.onSuiteBegin.bind(this)); + runner.on(Runner.constants.EVENT_SUITE_END, this.onSuiteEnd.bind(this)); + + runner.on(Runner.constants.EVENT_TEST_FAIL, this.onTestFail.bind(this)); + runner.on( + Runner.constants.EVENT_TEST_PENDING, + this.onTestPending.bind(this) + ); + // When using the default `spec` Mocha reporter, Cypress registers a retry listener: + // https://github.com/cypress-io/cypress/blob/b0c0eaa508bb6dafdc1997bc00fb7ed6f5bcc160/packages/server/lib/reporter.js#L268-L278 + runner.on(Runner.constants.EVENT_TEST_RETRY, this.onTestRetry.bind(this)); + // We need an onTestEnd() handler to capture multiple exceptions that occur in the same test + // (see https://github.com/mochajs/mocha/issues/2906 and + // https://github.com/mochajs/mocha/pull/4033), which results in a call to onTestFail() followed + // by a call either to onTestEnd() (for the last retry) or onTestRetry() (for earlier failures). + runner.on(Runner.constants.EVENT_TEST_END, this.onTestEnd.bind(this)); + } + + private color = (c: keyof ForegroundColor, str: string): string => + // Use the same flag as reporters.Base.color so that we don't end up with some colors but not + // others. + reporters.Base.useColors + ? styles.color[c].open + str + styles.color[c].close + : str; + + private indent = (): string => Array(this.indents).join(" "); + + private isQuarantined = (titlePath: TestTitle): boolean => { + // For ignore_failure (and skip_tests if somehow the quarantined tests still executed), ignore + // failures of quarantined tests. + if (this.config.quarantineMode === "no_quarantine") { + return false; + } + + if (this.testFilename === null) { + throw new Error("Suite has no `file` attribute"); + } + + const isQuarantined = + this.manifest !== null && + isTestQuarantined(this.manifest, this.testFilename, titlePath); + + debug( + `Test is ${isQuarantined ? "" : "NOT "}quarantined: ${JSON.stringify( + titlePath + )} in file ${this.testFilename}` + ); + + return isQuarantined; + }; + + // Retry-aware version of Base.list(): + // https://github.com/mochajs/mocha/blob/1412dc80d87d0479f7f1d60202da2b33c90eb939/lib/reporters/base.js#L209-L283 + private listFailures = ( + failures: TestWithError[], + isFlake: boolean + ): void => { + console.log(); + + failures.forEach((lastTestAttempt, failureIdx: number) => { + // If the failure occurred in a before/after hook, `test`'s name includes the name of the + // hook. However, we want quarantine to be based on the test itself, so we look up the test + // in its parent suite to find the original test title. + const suiteTest = + (lastTestAttempt.test.parent?.tests as CypressTest[] | undefined)?.find( + (suiteTest) => suiteTest.id === lastTestAttempt.test.id + ) ?? lastTestAttempt.test; + + const testTitle = formatFailedTestTitle(suiteTest.titlePath()); + + const failedAttempts: FailedTestAttempt[] = [ + ...this.retriedTests + .filter( + (retriedTest) => retriedTest.test.id === lastTestAttempt.test.id + ) + .map((retriedTest) => ({ + currentRetry: currentTestRetry(retriedTest.test), + err: retriedTest.err, + retries: testRetries(retriedTest.test), + })), + // For flaky tests, the last attempt is `passed`, which we don't want to print with the + // failures. + ...(lastTestAttempt.test.state === "failed" + ? [ + { + currentRetry: currentTestRetry(lastTestAttempt.test), + err: lastTestAttempt.err, + retries: testRetries(lastTestAttempt.test), + }, + ] + : []), + ].sort((a, b) => + a.currentRetry < b.currentRetry + ? -1 + : a.currentRetry > b.currentRetry + ? 1 + : 0 + ); + + failedAttempts.forEach((testAttempt, attemptIndex: number) => + reportFailedAttempt({ + attemptIndex, + failureIdx, + formattedTestTitle: testTitle, + numAttempts: isFlake + ? failedAttempts.length + 1 + : failedAttempts.length, + testAttempt, + }) + ); + }); + }; + + private onRunBegin = (): void => { + debug("onRunBegin"); + + // For whatever reason, the xunit Mocha reporter clears reporters.Base.useColors in its + // onRunEnd handler event. We record its value above and then restore it here so that colors + // are used (or not) consistently for each spec. This affects our integration tests, but it + // also makes sense for the production code path. See: + // https://github.com/mochajs/mocha/blob/1412dc80d87d0479f7f1d60202da2b33c90eb939/lib/reporters/xunit.js#L155 + reporters.Base.useColors = useColors; + + console.log(); + }; + + private onRunEnd = (): void => { + const stats = this.stats as ReporterStats; + + // Mocha considers flakes to be passes, so we need to subtract those from the pass count. + if ( + this.quarantinedFlakes.length + this.unquarantinedFlakes.length > + (stats.passes ?? 0) + ) { + printWarning( + `Number of flakes (${ + this.quarantinedFlakes.length + this.unquarantinedFlakes.length + }) exceeds number of passes (${stats.passes ?? 0})` + ); + } + const numPasses = Math.max( + 0, + (stats.passes ?? 0) - + this.quarantinedFlakes.length - + this.unquarantinedFlakes.length + ); + + console.log(); + + console.log( + reporters.Base.color("bright pass", " ") + + reporters.Base.color("green", ` ${numPasses} passing`) + + (typeof stats.duration === "number" + ? reporters.Base.color( + "light", + ` (${milliseconds(stats.duration ?? 0)})` + ) + : "") + ); + + if (this.unquarantinedPending.length > 0) { + console.log( + reporters.Base.color( + "pending", + ` ${this.unquarantinedPending.length} pending` + ) + ); + } + + if (this.quarantinedPending.length > 0) { + console.log( + this.color( + "magenta", + ` ${this.quarantinedPending.length} quarantined pending` + ) + ); + + console.log(); + + this.quarantinedPending.forEach((test, testIdx) => { + console.log( + reporters.Base.color( + "error title", + ` ${testIdx + 1}) ` + formatFailedTestTitle(test.titlePath()) + ) + ); + console.log(); + }); + + console.log(); + } + + // NB: We don't use stats.failures here (even if we account for quarantined failures) because + // it double-counts tests with multiple errors. Our count is more accurate, so we use it + // instead. + if (this.unquarantinedFailures.length > 0) { + console.log( + reporters.Base.color( + "fail", + ` ${this.unquarantinedFailures.length} failing` + ) + ); + + this.listFailures(this.unquarantinedFailures, false); + console.log(); + } + + if (this.quarantinedFailures.length > 0) { + console.log( + this.color( + "magenta", + ` ${this.quarantinedFailures.length} quarantined failing` + ) + ); + + this.listFailures(this.quarantinedFailures, false); + console.log(); + } + + if (this.unquarantinedFlakes.length > 0) { + console.log( + reporters.Base.color( + "medium", + ` ${this.unquarantinedFlakes.length} flaky` + ) + ); + + this.listFailures(this.unquarantinedFlakes, true); + console.log(); + } + + if (this.quarantinedFlakes.length > 0) { + console.log( + this.color( + "magenta", + ` ${this.quarantinedFlakes.length} quarantined flaky` + ) + ); + + this.listFailures(this.quarantinedFlakes, true); + console.log(); + } + + const { quarantinedSkipped, unquarantinedSkipped } = this.specTests.reduce( + ({ quarantinedSkipped, unquarantinedSkipped }, testTitle) => { + if (!this.nonSkippedTestJsonTitlePaths.has(JSON.stringify(testTitle))) { + if (this.isQuarantined(testTitle)) { + return { + quarantinedSkipped: [...quarantinedSkipped, testTitle], + unquarantinedSkipped, + }; + } else { + return { + quarantinedSkipped, + unquarantinedSkipped: [...unquarantinedSkipped, testTitle], + }; + } + } else { + return { quarantinedSkipped, unquarantinedSkipped }; + } + }, + { + quarantinedSkipped: [] as TestTitle[], + unquarantinedSkipped: [] as TestTitle[], + } + ); + + // The Mocha spec reporter doesn't print skipped tests, but it seems worth letting the user + // know which tests didn't get run due to prior failures. + if (quarantinedSkipped.length + unquarantinedSkipped.length > 0) { + console.log( + this.color( + "blue", + ` ${quarantinedSkipped.length + unquarantinedSkipped.length} skipped` + ) + ); + + console.log(); + + quarantinedSkipped.forEach((testTitle, idx) => { + const formattedTestTitle = formatFailedTestTitle(testTitle); + console.log( + ` ${idx + 1}) ${formattedTestTitle} ${this.color( + "magenta", + "[quarantined]" + )}` + ); + }); + unquarantinedSkipped.forEach((testTitle, idx) => { + const formattedTestTitle = formatFailedTestTitle(testTitle); + console.log( + ` ${quarantinedSkipped.length + idx + 1}) ${formattedTestTitle}` + ); + }); + + console.log(); + } + + console.log(); + + // We need to update the reporter stats to reflect flakiness and quarantining so that the + // plugin can print the same stats as the reporter without needing to recompute everything. + // NB: We can't update this.stats all at once since it's an object shared with the underlying + // Mocha runnable, and we need to make sure the updates apply to the runnable, which is where + // the final stats sent back to the plugin come from. + stats.passes = numPasses; + stats.failures = this.unquarantinedFailures.length; + stats.quarantinedFailures = this.quarantinedFailures.length; + stats.unquarantinedFlakes = this.unquarantinedFlakes.length; + stats.quarantinedFlakes = this.quarantinedFlakes.length; + stats.unquarantinedPending = this.unquarantinedPending.length; + stats.quarantinedPending = this.quarantinedPending.length; + stats.quarantinedSkipped = quarantinedSkipped.length; + stats.unquarantinedSkipped = unquarantinedSkipped.length; + }; + + private onSuiteBegin = (suite: Suite): void => { + debug( + `onSuiteBegin title=\`${suite.title}\` file=\`${ + suite.file ?? "" + }\` root=${String(suite.root)}` + ); + + suite.tests.forEach((test) => { + this.specTests.push(test.titlePath()); + }); + + this.indents++; + console.log(reporters.Base.color("suite", this.indent() + suite.title)); + }; + + private onSuiteEnd = (suite: Suite): void => { + debug( + `onSuiteEnd title=\`${suite.title}\` file=\`${ + suite.file ?? "" + }\` root=${String(suite.root)}` + ); + + this.indents--; + if (this.indents === 1) { + console.log(); + } + }; + + // This handler gets called under two circumstances: + // 1) The final retry of a test fails (including cases where Cypress won't retry a test due to + // a before/after hook failing). + // 2) A test fails with multiple errors (see https://github.com/mochajs/mocha/issues/2906 and + // https://github.com/mochajs/mocha/pull/4033). We can detect this case by checking whether + // test.currentRetry() < test.retries(). In this case, onTestFail() will get called with the + // second exception, followed by onTestEnd() getting called with the first exception. After + // some testing, it doesn't seem like there can ever be more than two exceptions. + private onTestFail = (test: CypressTest, _err?: Mocha.Error): void => { + debug( + `onTestFail [${test.state ?? "no state"}] ${test.title}: ${ + test.err?.message ?? "" + }` + ); + + // If the failure occurred in a before/after hook, `test`'s name includes the name of the hook. + // However, we want quarantine to be based on the test itself, so we look up the test in its + // parent suite to find the original test title. + const suiteTest = + (test.parent?.tests as CypressTest[] | undefined)?.find( + (suiteTest) => suiteTest.id === test.id + ) ?? test; + + // Case (2) above, or case (1) when Cypress won't retry due to a before/after all hook failing. + // Unfortunately, we can't determine at this point whether the test will be retried or not, so + // we assume that it will, and then handle the edge case in onTestEnd(). + if (currentTestRetry(test) < testRetries(test)) { + this.retriedTests.push({ test, err: test.err }); + } else if (this.isQuarantined(suiteTest.titlePath())) { + pushOrMergeFailedTest(this.quarantinedFailures, test); + } else { + pushOrMergeFailedTest(this.unquarantinedFailures, test); + } + + // NB: We don't print any output in this function since, for non-final-attempts, it only gets + // called if there are multiple errors. Instead, we print the result of each test attempt in + // onTestEnd() or onTestRetry(). + }; + + private reportTestPassed = (test: CypressTest): void => { + const currentRetry = currentTestRetry(test); + + const isFlaky = currentRetry > 0; + const isQuarantined = isFlaky && this.isQuarantined(test.titlePath()); + if (isFlaky && isQuarantined) { + this.quarantinedFlakes.push({ test, err: test.err }); + } else if (isFlaky) { + this.unquarantinedFlakes.push({ test, err: test.err }); + } + + // Cypress overrides the behavior of the Mocha spec reporter (see + // https://github.com/cypress-io/cypress/blob/b0c0eaa508bb6dafdc1997bc00fb7ed6f5bcc160/packages/server/lib/reporter.js#L296-L309): + // Override the default reporter to always show test timing even for fast tests and display + // slow ones in yellow rather than red. + const durationColor = test.speed === "slow" ? "medium" : "fast"; + + const symbolFmt = ` ${reporters.Base.symbols.ok}`; + + // Log: `✓ test title (300ms)` when a test passes + console.log( + Array(testDepth(test)).join(" ") + + (isQuarantined + ? this.color("magenta", symbolFmt) + : reporters.Base.color(isFlaky ? "medium" : "checkmark", symbolFmt)) + + reporters.Base.color("pass", ` ${test.title}`) + + (isQuarantined + ? this.color("magenta", " [flaky, quarantined]") + : isFlaky + ? reporters.Base.color("medium", " [flaky]") + : "") + + retryAttempt(test, false) + + (test.duration !== undefined + ? reporters.Base.color(durationColor, ` (${test.duration}ms)`) + : "") + ); + }; + + private reportTestFailed = (test: CypressTest): void => { + const isQuarantined = this.isQuarantined(test.titlePath()); + + if (isQuarantined) { + pushOrMergeFailedTest(this.quarantinedFailures, test); + } else { + pushOrMergeFailedTest(this.unquarantinedFailures, test); + } + + const quarantinedFmt = ` ${reporters.Base.symbols.err} ${test.title} [failed, quarantined]`; + + console.log( + Array(testDepth(test)).join(" ") + + (isQuarantined + ? reporters.Base.useColors + ? this.color("magenta", quarantinedFmt) + : quarantinedFmt + : reporters.Base.color( + "fail", + ` ${reporters.Base.symbols.err} ${test.title}` + )) + + retryAttempt(test, false) + ); + }; + + // This handler gets called after the final attempt of each test, so it's where we determine the + // test's overall outcome and whether it's quarantined. + // NB: An `err` argument is never passed to this event handler. + private onTestEnd = (test: CypressTest): void => { + debug( + `onTestEnd [${test.state ?? "no state"}] ${test.title}: ${ + test.err?.message ?? "" + }` + ); + + this.nonSkippedTestJsonTitlePaths.add(JSON.stringify(test.titlePath())); + + if (test.state === "passed") { + this.reportTestPassed(test); + } else if (test.state === "failed") { + const currentRetry = currentTestRetry(test); + // Edge case: a test failed and won't be retried due to a before()/after() hook failing. In + // this case, we already added the test to this.retriedTests in onTestFail() (where it was + // too soon to tell whether the test was going to be retried). To avoid printing this attempt + // twice, we need to remove it from this.retriedTests, and possibly combine it with the + // current attempt if the test had multiple errors. + if (currentRetry < testRetries(test)) { + const existingTestFailureIdx = this.retriedTests.findIndex( + (existingTestFailure) => + existingTestFailure.test.id === test.id && + currentTestRetry(existingTestFailure.test) === currentRetry + ); + if (existingTestFailureIdx !== -1) { + const existingTestFailure = this.retriedTests.splice( + existingTestFailureIdx, + 1 + )[0]; + + // Swap the order of the tests since the second error gets reported to us first (in cases + // of tests with multiple errors), and in this case we're keeping the current `test` + // object instead of `existingTestFailure` (which only has the serialized Mocha fields). + [existingTestFailure.err, test.err] = [ + test.err, + existingTestFailure.err, + ]; + mergeTestFailure(existingTestFailure, test); + } + } + + this.reportTestFailed(test); + } + // NB: pending tests are handled in onTestPending since onTestEnd isn't usually called for + // pending tests. + }; + + // Handles pending tests since onTestEnd isn't usually called for pending tests. + private onTestPending = (test: CypressTest, _err?: Mocha.Error): void => { + debug( + `onTestPending [${test.state ?? "no state"}] ${test.title}: ${ + test.err?.message ?? "" + }` + ); + + const titlePath = test.titlePath(); + const titlePathJson = JSON.stringify(titlePath); + // onTestEnd() doesn't get called for pending (it.skip()) tests. + this.nonSkippedTestJsonTitlePaths.add(titlePathJson); + + // We don't expect tests to be pending during retry. If this happens, there's likely some + // shenanigans going on in the test itself to call it.skip() during retry. Just print a + // warning in this case rather than treating the test as flaky or potentially quarantining + // it. + if (currentTestRetry(test) > 0) { + if (this.testFilename === null) { + throw new Error("Suite has no `file` attribute"); + } + + printWarning( + `test ${titlePathJson} in file ${this.testFilename} was pending (skipped) during retry` + ); + } + + const isQuarantined = + this.isQuarantined(titlePath) && + this.config.quarantineMode === "skip_tests"; + if (isQuarantined) { + this.quarantinedPending.push(test); + } else { + this.unquarantinedPending.push(test); + } + + console.log( + Array(testDepth(test)).join(" ") + + reporters.Base.color("pending", ` - ${test.title}`) + + (isQuarantined ? this.color("magenta", " [quarantined]") : "") + + retryAttempt(test, false) + ); + }; + + // NB: An `err` argument is never passed to this event handler. + private onTestRetry = ( + test: MochaEventTest & { hookName?: string } + ): void => { + debug( + `onTestRetry [${test.state ?? "no state"}] ${test.title}: ${ + test.err?.message ?? "" + }` + ); + + // Edge case: onTestRetry() gets called twice if a test fails and its afterEach() hook also + // fails. However, we don't want to print the failed attempt twice in that case. + if ( + pushOrMergeFailedTest(this.retriedTests, test) || + test.hookName === undefined + ) { + console.log( + this.indent() + + reporters.Base.color( + "fail", + ` ${reporters.Base.symbols.err} ${test.title}` + ) + + retryAttempt(test, true) + ); + } + }; +} diff --git a/packages/cypress-plugin/src/skip-tests.ts b/packages/cypress-plugin/src/skip-tests.ts new file mode 100644 index 0000000..01ac7f5 --- /dev/null +++ b/packages/cypress-plugin/src/skip-tests.ts @@ -0,0 +1,224 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// This file runs inside the browser as part of the test code. It gets loaded by a Cypress support +// file injected by the plugin when quarantine mode is set to `skip_tests`. + +// In order to skip quarantined tests, we wrap Cypress's Mocha it()/specify() function to replace +// quarantined test functions with calls to this.skip(). Cypress only supports Mocha's `bdd` syntax +// (see https://docs.cypress.io/guides/references/bundled-libraries), so there's no need to wrap +// other Mocha test functions such as test(). The approach we follow here is adapted from how +// Cypress wraps Mocha functions: +// https://github.com/cypress-io/cypress/blob/660ff675782dba8725e16620ca35fc38e004c23f/packages/driver/src/cypress/mocha.ts#L58-L122 + +// We also wrap context()/describe() so keep track of the hierarchical title path for each test, +// which is required for determining whether a test is quarantined. + +// registerMochaInstrumentation() gets called by a Cypress support file that the plugin generates +// on the fly when quarantineMode is set to skip_tests. + +import type { TestSuiteManifest } from "@unflakable/js-api"; +// Avoid depending on Node.JS deps. +import { isTestQuarantined } from "@unflakable/plugins-common/quarantine"; +import type { UnflakableConfig } from "@unflakable/plugins-common"; +import path from "path-browserify"; +import _debug from "debug"; +import { + CYPRESS_ENV_VAR_CONFIG, + CYPRESS_ENV_VAR_MANIFEST, + CYPRESS_ENV_VAR_REPO_ROOT, +} from "./cypress-env-vars"; + +const debug = _debug("unflakable:skip-tests"); + +const baseContext: Mocha.SuiteFunction = context; +const baseDescribe: Mocha.SuiteFunction = describe; +const baseIt: Mocha.TestFunction = it; +const baseSpecify: Mocha.TestFunction = specify; + +type SuiteFunctionWithCypressConfigOverrides = ( + title: string, + config: Cypress.SuiteConfigOverrides | undefined, + fn: ((this: Mocha.Suite) => void) | undefined +) => Mocha.Suite; + +type TestFunctionWithCypressConfigOverrides = ( + title: string, + config: Cypress.TestConfigOverrides | undefined, + fn: Mocha.Func | undefined +) => Mocha.Test; + +// Adapted from what @cypress/grep does to get the complete test title: +// https://github.com/cypress-io/cypress/blob/7a18b79efae64dc1fc32fb5aaa89969e83971c6f/npm/grep/src/support.js#L142-L187 +const suiteStack = [] as string[]; +const instrumentDescribeFn = (fnName: "context" | "describe"): void => { + const baseFn = fnName === "context" ? baseContext : baseDescribe; + const instrumentedFnImpl = (subFn?: "only"): Mocha.ExclusiveSuiteFunction => { + const base = ( + subFn === "only" ? baseFn.only : baseFn + ) as SuiteFunctionWithCypressConfigOverrides; + + return ((...args: unknown[]): Mocha.Suite => { + if (typeof args[0] !== "string") { + // See: + // https://github.com/mochajs/mocha/blob/81cfa9072b79fee57ba8fe1b9ddf8d774aa41f2e/lib/suite.js#L49-L57 + throw new Error( + `Suite argument "title" must be a string. Received type "${typeof args[0]}"` + ); + } + + suiteStack.push(args[0]); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = base(...args); + suiteStack.pop(); + return result; + }) as Mocha.ExclusiveSuiteFunction; + }; + + const instrumentedFn = instrumentedFnImpl() as Mocha.SuiteFunction; + instrumentedFn.only = instrumentedFnImpl("only"); + instrumentedFn.skip = baseFn.skip; + + if (fnName === "context") { + context = instrumentedFn; + } else if (fnName === "describe") { + describe = instrumentedFn; + } else { + throw new Error( + `Unexpected fnName \`${ + fnName as string + }\` should be \`context\` or \`describe\`` + ); + } +}; + +// Cypress overrides both it() and specify(), so we do likewise. See: +// https://github.com/cypress-io/cypress/blob/660ff675782dba8725e16620ca35fc38e004c23f/packages/driver/src/cypress/mocha.ts#L142-L143 +const instrumentTestFn = ( + fnName: "it" | "specify", + repoRoot: string, + manifest: TestSuiteManifest | null +): void => { + const baseFn = fnName === "it" ? baseIt : baseSpecify; + const instrumentedFnImpl = (subFn?: "only"): Mocha.ExclusiveTestFunction => { + const base = ( + subFn === "only" ? baseFn.only : baseFn + ) as TestFunctionWithCypressConfigOverrides; + + const testInner = ( + title: string, + config: Cypress.TestConfigOverrides | undefined, + fn: Mocha.Func | undefined + ): Mocha.Test => { + const testFilename = path.relative(repoRoot, Cypress.spec.absolute); + const titlePath = [...suiteStack, title]; + const isQuarantined = + manifest !== null && + isTestQuarantined(manifest, testFilename, titlePath); + + debug( + `Test is ${isQuarantined ? "" : "NOT "}quarantined: ${JSON.stringify( + titlePath + )} in file ${testFilename}` + ); + + if (isQuarantined) { + // For some reason, calling baseFn.skip() causes infinite recursion in the Cypress runner. + // Instead, we call Context.skip() in the test function, which has the added benefit of + // working with it.only() calls without potentially causing it() tests to run as a result of + // skipping the it.only() call. + return (baseFn as TestFunctionWithCypressConfigOverrides)( + title, + config, + function (): never { + this.skip(); + } + ); + } else { + return base(title, config, fn); + } + }; + + // This wraps the Cypress version of it(), which may be the 3-argument version that includes + // Cypress.TestConfigOverrides as the second argument. + return ((...args: unknown[]): Mocha.Test => { + // In theory, Mocha supports calling it() without a test title (i.e., (fn: Func) => Test), + // but in Cypress this always throws an exception, so we impose the same requirement for our + // instrumented version (which requires a title for checking whether a test is quarantined). + if (typeof args[0] !== "string") { + // See: + // https://github.com/mochajs/mocha/blob/52b9a5fb97bc3a6581dc6538aa0092276e71ea41/lib/test.js#L21-L27 + throw new Error( + `Test argument "title" should be a string. Received type "${typeof args[0]}"` + ); + } else if (typeof args[1] === "function") { + // (title: string, fn?: Func): Test + return testInner(args[0], {}, args[1] as Mocha.Func); + } else { + // (title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test + return testInner( + args[0], + args[1] as Cypress.TestConfigOverrides | undefined, + args[2] as Mocha.Func | undefined + ); + } + }) as Mocha.ExclusiveTestFunction; + }; + + // @types/mocha defines a retries() method on TestFunction, but it seems like that's supposed to + // be a method on Test, and Cypress doesn't do anything involving TestFunction.retries(). + const instrumentedFn = instrumentedFnImpl() as Mocha.TestFunction; + instrumentedFn.only = instrumentedFnImpl("only"); + instrumentedFn.skip = baseFn.skip; + + if (fnName === "it") { + it = instrumentedFn; + } else if (fnName === "specify") { + specify = instrumentedFn; + } else { + throw new Error( + `Unexpected fnName \`${ + fnName as string + }\` should be \`it\` or \`specify\`` + ); + } +}; + +export const registerMochaInstrumentation = (): void => { + debug("Called registerMochaInstrumentation()"); + + const manifestJson = Cypress.env(CYPRESS_ENV_VAR_MANIFEST) as + | string + | undefined; + if (manifestJson === undefined) { + throw new Error( + `${CYPRESS_ENV_VAR_MANIFEST} environment variable is not set -- did you call registerUnflakable()?` + ); + } + const manifest = JSON.parse(manifestJson) as TestSuiteManifest | null; + + const configJson = Cypress.env(CYPRESS_ENV_VAR_CONFIG) as string | undefined; + if (configJson === undefined) { + throw new Error( + `${CYPRESS_ENV_VAR_CONFIG} environment variable is not set` + ); + } + const unflakableConfig = JSON.parse(configJson) as UnflakableConfig; + + const repoRoot = Cypress.env(CYPRESS_ENV_VAR_REPO_ROOT) as string | undefined; + if (repoRoot === undefined) { + throw new Error( + `${CYPRESS_ENV_VAR_REPO_ROOT} environment variable is not set` + ); + } + + if ( + unflakableConfig.enabled && + unflakableConfig.quarantineMode === "skip_tests" + ) { + instrumentDescribeFn("context"); + instrumentDescribeFn("describe"); + instrumentTestFn("it", repoRoot, manifest); + instrumentTestFn("specify", repoRoot, manifest); + } +}; diff --git a/packages/cypress-plugin/src/tsconfig.json b/packages/cypress-plugin/src/tsconfig.json new file mode 100644 index 0000000..44badea --- /dev/null +++ b/packages/cypress-plugin/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["../mocha.d.ts", ".", ".eslintrc.js"] +} diff --git a/packages/cypress-plugin/src/utils.ts b/packages/cypress-plugin/src/utils.ts new file mode 100644 index 0000000..95d5fa7 --- /dev/null +++ b/packages/cypress-plugin/src/utils.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import chalk from "chalk"; +import { readFileSync } from "fs"; +import process from "process"; +import { createRequire } from "module"; + +// Needed so that config-wrapper.ts can be compiled to ESM (which doesn't define `require`). +const _require = + typeof require !== "undefined" ? require : createRequire(import.meta.url); + +export { _require as require }; + +const CYPRESS_PLUGIN_VERSION: string = ( + JSON.parse( + // Works for both CommonJS and ESM targets. + readFileSync(new URL("../package.json", import.meta.url), "utf8") + ) as { version: string } +).version; + +export const userAgent = (cypressVersion: string): string => + `unflakable-cypress-plugin/${CYPRESS_PLUGIN_VERSION} (Cypress ${cypressVersion}; Node ${process.version})`; + +export const printWarning = (msg: string): void => { + console.warn(chalk.yellow("WARNING: %s"), msg); +}; diff --git a/packages/cypress-plugin/src/vendored/cli-table3.d.ts b/packages/cypress-plugin/src/vendored/cli-table3.d.ts new file mode 100644 index 0000000..14c7749 --- /dev/null +++ b/packages/cypress-plugin/src/vendored/cli-table3.d.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Cypress imports this even though it's not part of the cli-table3 exports, so we add some types +// here. +declare module "cli-table3/src/utils.js" { + import { default as Table } from "cli-table3"; + + function mergeOptions( + options: Table.TableConstructorOptions, + defaults?: Table.TableConstructorOptions + ): Table.TableConstructorOptions; +} diff --git a/packages/cypress-plugin/src/vendored/duration.ts b/packages/cypress-plugin/src/vendored/duration.ts new file mode 100644 index 0000000..77a489e --- /dev/null +++ b/packages/cypress-plugin/src/vendored/duration.ts @@ -0,0 +1,86 @@ +/* +This file includes portions of a Cypress source code file originally downloaded from: +https://github.com/cypress-io/cypress/blob/d1d15e6cfeb025eeb272015c89d925ff375774c3/packages/server/lib/util/duration.js +Its copyright notice and license are as follows: + + MIT License + + Copyright (c) 2022 Cypress.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +All modifications to the above referenced file are copyrighted and licensed under the terms set +forth in the LICENSE file at the root of this repository. +*/ + +import _ from "lodash"; +import { default as dayjs } from "dayjs"; +// File extensions required for config-wrapper.ts ESM build. +import duration from "dayjs/plugin/duration.js"; +import relativeTime from "dayjs/plugin/relativeTime.js"; +import updateLocale from "dayjs/plugin/updateLocale.js"; + +dayjs.extend(duration); +dayjs.extend(relativeTime); +dayjs.extend(updateLocale); +dayjs.updateLocale("en", { + relativeTime: { + future: "in %s", + past: "%s ago", + s: "a few secs", + ss: "%d secs", + m: "a min", + mm: "%d mins", + h: "an hour", + hh: "%d hours", + d: "a day", + dd: "%d days", + M: "a month", + MM: "%d months", + y: "a year", + yy: "%d years", + }, +}); + +const format = (durationInMs: number, padMinutes = true): string => { + const duration = dayjs.duration(durationInMs); + + const durationSecs = duration.seconds() ? `${duration.seconds()}` : ""; + const durationMins = duration.minutes() ? `${duration.minutes()}` : ""; + const durationHrs = duration.hours() ? `${duration.hours()}` : ""; + + const total = _.compact([ + durationHrs, + durationHrs !== "" || padMinutes + ? _.padStart(durationMins, 2, "0") + : durationMins, + _.padStart(durationSecs, 2, "0"), + ]); + + const totalMinSec = total.join(":"); + + if (totalMinSec === "00:00") { + return `${duration.milliseconds()}ms`; + } + + return totalMinSec; +}; + +export { format }; diff --git a/packages/cypress-plugin/src/vendored/human_time.ts b/packages/cypress-plugin/src/vendored/human_time.ts new file mode 100644 index 0000000..aa49774 --- /dev/null +++ b/packages/cypress-plugin/src/vendored/human_time.ts @@ -0,0 +1,102 @@ +/* +This file includes portions of a Cypress source code file originally downloaded from: +https://github.com/cypress-io/cypress/blob/d1d15e6cfeb025eeb272015c89d925ff375774c3/packages/server/lib/util/human_time.js +Its copyright notice and license are as follows: + + MIT License + + Copyright (c) 2022 Cypress.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +All modifications to the above referenced file are copyrighted and licensed under the terms set +forth in the LICENSE file at the root of this repository. +*/ + +import { default as dayjs } from "dayjs"; +// File extension required for config-wrapper.ts ESM build. +import duration from "dayjs/plugin/duration.js"; + +dayjs.extend(duration); + +const parse = (ms: number) => { + const duration = dayjs.duration(ms); + const hours = duration.hours(); + let mins = hours * 60; + + return { + mins, + hours, + duration, + }; +}; + +const long = (ms: number, alwaysIncludeSeconds = true): string => { + let { mins, duration } = parse(ms); + let word; + const msg = []; + + mins += duration.minutes(); + + if (mins) { + word = mins === 1 ? "minute" : "minutes"; + msg.push(`${mins} ${word}`); + } + + const secs = duration.seconds(); + + if (alwaysIncludeSeconds || secs > 0) { + word = secs === 1 ? "second" : "seconds"; + msg.push(`${secs} ${word}`); + } + + return msg.join(", "); +}; + +const short = (ms: number): string => { + let { mins, duration } = parse(ms); + const msg = []; + + mins += duration.minutes(); + + if (mins) { + msg.push(`${mins}m`); + } + + const secs = duration.seconds(); + + if (secs) { + msg.push(`${secs}s`); + } else { + if (!mins) { + const millis = duration.milliseconds(); + + if (millis) { + msg.push(`${millis}ms`); + } else { + msg.push(`${secs}s`); + } + } + } + + return msg.join(", "); +}; + +export { long, short }; diff --git a/packages/cypress-plugin/src/vendored/newlines.ts b/packages/cypress-plugin/src/vendored/newlines.ts new file mode 100644 index 0000000..b6d6396 --- /dev/null +++ b/packages/cypress-plugin/src/vendored/newlines.ts @@ -0,0 +1,48 @@ +/* +This file includes portions of a Cypress source code file originally downloaded from: +https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/newlines.js +Its copyright notice and license are as follows: + + MIT License + + Copyright (c) 2022 Cypress.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +All modifications to the above referenced file are copyrighted and licensed under the terms set +forth in the LICENSE file at the root of this repository. +*/ + +const addNewlineAtEveryNChar = (str: string, n: number): string => { + if (!str) { + return str; + } + + let result = []; + let idx = 0; + + while (idx < str.length) { + result.push(str.slice(idx, (idx += n))); + } + + return result.join("\n"); +}; + +export { addNewlineAtEveryNChar }; diff --git a/packages/cypress-plugin/src/vendored/print-run.ts b/packages/cypress-plugin/src/vendored/print-run.ts new file mode 100644 index 0000000..6c6e0b8 --- /dev/null +++ b/packages/cypress-plugin/src/vendored/print-run.ts @@ -0,0 +1,364 @@ +/* +This file includes portions of a Cypress source code file originally downloaded from: +https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/print-run.ts +Its copyright notice and license are as follows: + + MIT License + + Copyright (c) 2022 Cypress.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +All modifications to the above referenced file are copyrighted and licensed under the terms set +forth in the LICENSE file at the root of this repository. +*/ + +import _ from "lodash"; +import chalk from "chalk"; +import * as humanTime from "./human_time"; +import * as newlines from "./newlines"; +import * as terminal from "./terminal"; +import { Cell, default as Table } from "cli-table3"; +import logSymbols from "log-symbols"; + +export type Screenshot = { + width: number; + height: number; + path: string; + specName: string; +}; + +export function color( + val: Cell, + c: typeof chalk.Color | typeof chalk.Modifiers +): string { + return chalk[c](val); +} + +export function gray(val: string): string { + return color(val, "gray"); +} + +export function colorIf( + val: Cell, + c: typeof chalk.Color | typeof chalk.Modifiers +): string { + if (val === 0 || val == null) { + val = "-"; + c = "gray"; + } + + return color(val, c); +} + +export function getWidth(table: Table.Table, index: number): number { + // get the true width of a table's column, + // based off of calculated table options for that column + const columnWidth = table.options.colWidths[index]; + + if (columnWidth) { + return ( + columnWidth - + (table.options.style["padding-left"] + + table.options.style["padding-right"]) + ); + } + + throw new Error("Unable to get width for column"); +} + +function formatBrowser(browser: Cypress.Browser) { + return _.compact([ + browser.displayName, + browser.majorVersion, + browser.isHeadless && gray("(headless)"), + ]).join(" "); +} + +export function formatSymbolSummary(failures: number): string { + return failures ? logSymbols.error : logSymbols.success; +} + +function macOSRemovePrivate(str: string) { + // consistent snapshots when running system tests on macOS + if (process.platform === "darwin" && str.startsWith("/private")) { + return str.slice(8); + } + + return str; +} + +export function formatPath( + name: string, + n: number | undefined, + pathColor: typeof chalk.Color | typeof chalk.Modifiers = "reset" +): string { + if (!name) return ""; + + const fakeCwdPath = process.env.FAKE_CWD_PATH; + + if (fakeCwdPath && process.env.CYPRESS_INTERNAL_ENV === "test") { + // if we're testing within Cypress, we want to strip out + // the current working directory before calculating the stdout tables + // this will keep our snapshots consistent everytime we run + const cwdPath = process.cwd(); + + name = name.split(cwdPath).join(fakeCwdPath); + + name = macOSRemovePrivate(name); + } + + // add newLines at each n char and colorize the path + if (n) { + let nameWithNewLines = newlines.addNewlineAtEveryNChar(name, n); + + return `${color(nameWithNewLines, pathColor)}`; + } + + return `${color(name, pathColor)}`; +} + +function formatNodeVersion( + { + resolvedNodeVersion, + resolvedNodePath, + }: Pick< + Cypress.ResolvedConfigOptions, + "resolvedNodeVersion" | "resolvedNodePath" + >, + width: number +) { + if (resolvedNodePath) + return formatPath( + `v${resolvedNodeVersion} ${gray(`(${resolvedNodePath})`)}`, + width + ); + + return; +} + +function formatRecordParams( + runUrl?: string, + parallel?: boolean, + group?: string, + tag?: string, + autoCancelAfterFailures?: number | false +) { + if (runUrl) { + return `Tag: ${tag || "false"}, Group: ${ + group || "false" + }, Parallel: ${Boolean(parallel)}${ + autoCancelAfterFailures !== undefined + ? `, Auto Cancel After Failures: ${autoCancelAfterFailures}` + : "" + }`; + } + + return; +} + +export function displayRunStarting(options: { + browser: Cypress.Browser; + config: Pick & + Pick< + Cypress.ResolvedConfigOptions, + "resolvedNodeVersion" | "resolvedNodePath" + >; + group: string | undefined; + parallel?: boolean; + runUrl?: string; + specPattern: string | RegExp | string[]; + specs: CypressCommandLine.RunResult["spec"][]; + tag: string | undefined; + autoCancelAfterFailures?: number | false; +}): string { + const { + browser, + config, + group, + parallel, + runUrl, + specPattern, + specs, + tag, + autoCancelAfterFailures, + } = options; + + console.log(""); + + terminal.divider("="); + + console.log(""); + + terminal.header("Run Starting", { + color: ["reset"], + }); + + console.log(""); + + // NB: We don't vendor the experiments module because it changes too frequently for us to stay on + // top of upstream changes, and it may not work consistently with different Cypress versions. + /* + const experimental = experiments.getExperimentsFromResolved(config.resolved); + const enabledExperiments = _.pickBy(experimental, _.property("enabled")); + const hasExperiments = + !process.env.CYPRESS_INTERNAL_SKIP_EXPERIMENT_LOGS && + !_.isEmpty(enabledExperiments); + */ + + // if we show Node Version, then increase 1st column width + // to include wider 'Node Version:'. + // Without Node version, need to account for possible "Experiments" label + const colWidths = config.resolvedNodePath + ? [16, 84] + : /*: hasExperiments + ? [14, 86]*/ + [12, 88]; + + const table = terminal.table({ + colWidths, + type: "outsideBorder", + }); + + if (!specPattern) throw new Error("No specPattern in displayRunStarting"); + + const formatSpecs = (specs: CypressCommandLine.RunResult["spec"][]) => { + // 25 found: (foo.spec.js, bar.spec.js, baz.spec.js) + const names = _.map(specs, "relativeToCommonRoot"); + const specsTruncated = _.truncate(names.join(", "), { length: 250 }); + + const stringifiedSpecs = [ + `${names.length} found `, + "(", + specsTruncated, + ")", + ].join(""); + + return formatPath(stringifiedSpecs, getWidth(table, 1)); + }; + + const data = _.chain([ + [gray("Cypress:"), config.version], + [gray("Browser:"), formatBrowser(browser)], + [gray("Node Version:"), formatNodeVersion(config, getWidth(table, 1))], + [gray("Specs:"), formatSpecs(specs)], + [ + gray("Searched:"), + formatPath( + Array.isArray(specPattern) + ? specPattern.join(", ") + : String(specPattern), + getWidth(table, 1) + ), + ], + [ + gray("Params:"), + formatRecordParams(runUrl, parallel, group, tag, autoCancelAfterFailures), + ], + [gray("Run URL:"), runUrl ? formatPath(runUrl, getWidth(table, 1)) : ""], + /*[ + gray("Experiments:"), + hasExperiments ? experiments.formatExperiments(enabledExperiments) : "", + ],*/ + ]) + .filter(_.property(1)) + .value(); + + table.push(...data); + + const heading = table.toString(); + + console.log(heading); + + console.log(""); + + return heading; +} + +export function displaySpecHeader( + name: string, + curr: number, + total: number, + estimated: number +): void { + console.log(""); + + const PADDING = 2; + + const table = terminal.table({ + colWidths: [10, 70, 20], + colAligns: ["left", "left", "right"], + type: "pageDivider", + style: { + "padding-left": PADDING, + "padding-right": 0, + }, + }); + + table.push(["", ""]); + table.push([ + "Running:", + `${formatPath(name, getWidth(table, 1), "gray")}`, + gray(`(${curr} of ${total})`), + ]); + + console.log(table.toString()); + + if (estimated) { + const estimatedLabel = `${" ".repeat(PADDING)}Estimated:`; + + return console.log(estimatedLabel, gray(humanTime.long(estimated))); + } +} + +export function displayScreenshots(screenshots: Screenshot[] = []): void { + console.log(""); + + terminal.header("Screenshots", { color: ["yellow"] }); + + console.log(""); + + const table = terminal.table({ + colWidths: [3, 82, 15], + colAligns: ["left", "left", "right"], + type: "noBorder", + style: { + "padding-right": 0, + }, + chars: { + left: " ", + right: "", + }, + }); + + screenshots.forEach((screenshot) => { + const dimensions = gray(`(${screenshot.width}x${screenshot.height})`); + + table.push([ + "-", + formatPath(`${screenshot.path}`, getWidth(table, 1)), + gray(dimensions), + ]); + }); + + console.log(table.toString()); + + console.log(""); +} diff --git a/packages/cypress-plugin/src/vendored/terminal-size.ts b/packages/cypress-plugin/src/vendored/terminal-size.ts new file mode 100644 index 0000000..e5f5715 --- /dev/null +++ b/packages/cypress-plugin/src/vendored/terminal-size.ts @@ -0,0 +1,46 @@ +/* +This file includes portions of a Cypress source code file originally downloaded from: +https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/terminal-size.js +Its copyright notice and license are as follows: + + MIT License + + Copyright (c) 2022 Cypress.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +All modifications to the above referenced file are copyrighted and licensed under the terms set +forth in the LICENSE file at the root of this repository. +*/ + +import { default as termSize } from "term-size"; + +const get = (): termSize.Size => { + const obj = termSize(); + + if (process.env.CI) { + // reset to 100 + obj.columns = 100; + } + + return obj; +}; + +export { get }; diff --git a/packages/cypress-plugin/src/vendored/terminal.ts b/packages/cypress-plugin/src/vendored/terminal.ts new file mode 100644 index 0000000..5847a15 --- /dev/null +++ b/packages/cypress-plugin/src/vendored/terminal.ts @@ -0,0 +1,260 @@ +/* +This file includes portions of a Cypress source code file originally downloaded from: +https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/terminal.js +Its copyright notice and license are as follows: + + MIT License + + Copyright (c) 2022 Cypress.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +All modifications to the above referenced file are copyrighted and licensed under the terms set +forth in the LICENSE file at the root of this repository. +*/ + +import _ from "lodash"; +import chalk from "chalk"; +import { default as Table } from "cli-table3"; +// File extension required for config-wrapper.ts ESM build. +import utils from "cli-table3/src/utils.js"; +import widestLine from "widest-line"; +import * as terminalSize from "./terminal-size"; + +const MAXIMUM_SIZE = 100; +const EXPECTED_SUM = 100; + +const getMaximumColumns = (): number => { + // get the maximum amount of columns + // that can fit in the terminal + return Math.min(MAXIMUM_SIZE, terminalSize.get().columns); +}; + +const getBordersLength = (left: string, right: string): number => { + return _.chain([left, right]).compact().map(widestLine).sum().value(); +}; + +const renderTables = (...tables: Table.Table[]): string => { + return _.chain([] as Table.Table[]) + .concat(tables) + .invokeMap("toString") + .join("\n") + .value(); +}; + +export type TableType = + | "border" + | "noBorder" + | "outsideBorder" + | "pageDivider" + | "allBorders"; + +const getChars = (type: TableType): Table.TableConstructorOptions["chars"] => { + switch (type) { + case "border": + return { + "top-mid": "", + "top-left": " ┌", + left: " │", + "left-mid": " ├", + middle: "", + "mid-mid": "", + right: "│", + "bottom-mid": "", + "bottom-left": " └", + }; + case "noBorder": + return { + top: "", + "top-mid": "", + "top-left": "", + "top-right": "", + left: " ", + "left-mid": "", + middle: "", + mid: "", + "mid-mid": "", + right: " ", + "right-mid": "", + bottom: "", + "bottom-left": "", + "bottom-mid": "", + "bottom-right": "", + }; + case "outsideBorder": + return { + // "top": "" + "top-left": " ┌", + "top-mid": "", + left: " │", + "left-mid": "", + middle: "", + mid: "", + "mid-mid": "", + "right-mid": "", + "bottom-mid": "", + "bottom-left": " └", + }; + case "pageDivider": + return { + top: "─", + "top-mid": "", + "top-left": "", + "top-right": "", + bottom: "", + "bottom-mid": "", + "bottom-left": "", + "bottom-right": "", + left: "", + "left-mid": "", + mid: "", + "mid-mid": "", + right: "", + "right-mid": "", + middle: "", + }; + case "allBorders": + return { + // this is default from cli-table mostly just for debugging, + // if you want to see where borders would be drawn + top: "─", + "top-mid": "┬", + "top-left": "┌", + "top-right": "┐", + bottom: "─", + "bottom-mid": "┴", + "bottom-left": "└", + "bottom-right": "┘", + left: "│", + "left-mid": "├", + mid: "─", + "mid-mid": "┼", + right: "│", + "right-mid": "┤", + middle: "│", + }; + default: + throw new Error(`Table chars type: "${type}" is not supported`); + } +}; + +const wrapBordersInGray = ( + chars: Table.TableConstructorOptions["chars"] +): Table.TableConstructorOptions["chars"] => { + return _.mapValues(chars, (char) => { + if (char) { + return chalk.gray(char); + } + + return char; + }); +}; + +const table = ( + options: Table.TableConstructorOptions & { type: TableType } +): Table.HorizontalTable => { + const { type } = options; + const defaults = utils.mergeOptions({}); + + let { colWidths } = options; + let chars = _.defaults(getChars(type), defaults.chars); + + _.defaultsDeep(options, { + chars, + style: { + head: [], + border: [], + "padding-left": 1, + "padding-right": 1, + }, + }); + + chars = options.chars as NonNullable; + + if (colWidths) { + const sum = _.sum(colWidths); + + if (sum !== EXPECTED_SUM) { + throw new Error( + `Expected colWidths array to sum to: ${EXPECTED_SUM}, instead got: ${sum}` + ); + } + + const bordersLength = getBordersLength( + chars.left as string, + chars.right as string + ); + + if (bordersLength > 0) { + // redistribute the columns to account for borders on each side... + // and subtract borders size from the largest width cell + const largestCellWidth = _.max(colWidths) as number; + + const index = _.indexOf(colWidths, largestCellWidth); + + colWidths = _.clone(colWidths); + + colWidths[index] = largestCellWidth - bordersLength; + options.colWidths = colWidths; + } + } + + options.chars = wrapBordersInGray(chars); + + return new Table(options) as Table.HorizontalTable; +}; + +const header = ( + message: string, + options: { + color?: (typeof chalk.Color | typeof chalk.Modifiers)[]; + } = {} +): void => { + _.defaults(options, { + color: null, + }); + + message = ` (${chalk.underline.bold(message)})`; + + if (options.color) { + const colors = ( + [] as (typeof chalk.Color | typeof chalk.Modifiers)[] + ).concat(options.color); + + message = _.reduce( + colors, + (memo, color) => { + return chalk[color](memo); + }, + message + ); + } + + console.log(message); // eslint-disable-line no-console +}; + +const divider = (symbol: string, color: typeof chalk.Color = "gray"): void => { + const cols = getMaximumColumns(); + const str = symbol.repeat(cols); + + console.log(chalk[color](str)); // eslint-disable-line no-console +}; + +export { table, header, divider, renderTables, getMaximumColumns }; diff --git a/packages/cypress-plugin/test/.eslintrc.cjs b/packages/cypress-plugin/test/.eslintrc.cjs new file mode 100644 index 0000000..7b41633 --- /dev/null +++ b/packages/cypress-plugin/test/.eslintrc.cjs @@ -0,0 +1,9 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + env: { + "cypress/globals": true, + }, + extends: ["plugin:cypress/recommended"], + plugins: ["cypress"], +}; diff --git a/packages/cypress-plugin/test/.gitignore b/packages/cypress-plugin/test/.gitignore new file mode 100644 index 0000000..699991a --- /dev/null +++ b/packages/cypress-plugin/test/.gitignore @@ -0,0 +1,4 @@ +*/cypress/downloads/ +*/cypress/results/ +*/cypress/screenshots/ +*/cypress/videos/ diff --git a/packages/cypress-plugin/test/integration-common/.eslintrc.js b/packages/cypress-plugin/test/integration-common/.eslintrc.js new file mode 100644 index 0000000..b6febf5 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/.eslintrc.js @@ -0,0 +1,5 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + extends: ["../../../../.eslintrc-ts.js"], +}; diff --git a/packages/cypress-plugin/test/integration-common/package.json b/packages/cypress-plugin/test/integration-common/package.json new file mode 100644 index 0000000..0749244 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/package.json @@ -0,0 +1,34 @@ +{ + "name": "cypress-integration-common", + "private": true, + "exports": { + "./config": { + "types": "./dist/config.d.ts", + "default": "./dist/config.js" + }, + "./git": { + "types": "./dist/git.d.ts", + "default": "./dist/git.js" + }, + "./mock-cosmiconfig": { + "default": "./dist/mock-cosmiconfig.js" + } + }, + "dependencies": { + "debug": "^4.3.3", + "expect": "^29.5.0", + "simple-git": "^3.16.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-typescript": "^11.1.1", + "@unflakable/plugins-common": "workspace:^", + "rollup": "^3.21.1", + "typescript": "^4.9.5" + }, + "scripts": { + "build": "yarn clean && tsc --noEmit && tsc --noEmit -p src && rollup --config", + "clean": "rm -rf dist/" + } +} diff --git a/packages/cypress-plugin/test/integration-common/rollup.config.mjs b/packages/cypress-plugin/test/integration-common/rollup.config.mjs new file mode 100644 index 0000000..fcf3630 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/rollup.config.mjs @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import pluginCommonJs from "@rollup/plugin-commonjs"; +import pluginNodeResolve from "@rollup/plugin-node-resolve"; +import pluginTypescript from "@rollup/plugin-typescript"; + +// We emit a CommonJS bundle so that both CommonJS and ESM targets can use this package. The package +// depends on plugins-common, which is ESM, so we have to use Rollup and can't rely solely on tsc. +// Otherwise, the transitive dependency remains ESM, which fails at runtime during require(). + +/** + * @type {import("rollup").NormalizedInputOptions} + */ +export default { + input: ["src/config.ts", "src/git.ts", "src/mock-cosmiconfig.ts"], + output: { + dir: "dist", + format: "cjs", + }, + external: (id) => + !id.startsWith(".") && + !id.startsWith("/") && + !id.startsWith("src/") && + !["@unflakable/plugins-common"].includes(id), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + plugins: [ + pluginCommonJs(), + pluginNodeResolve({ preferBuiltins: true }), + pluginTypescript({ tsconfig: "src/tsconfig.json" }), + ], + treeshake: { + // Assume internal modules do not have side effects when they're imported. This helps remove + // unnecessary require()'s from the transpiled code. + moduleSideEffects: (id, external) => external, + }, +}; diff --git a/packages/cypress-plugin/test/integration-common/src/config.ts b/packages/cypress-plugin/test/integration-common/src/config.ts new file mode 100644 index 0000000..81ce31b --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/src/config.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import _debug from "debug"; +import { UnflakableConfig, setCosmiconfig } from "@unflakable/plugins-common"; +import type { cosmiconfig, Options } from "cosmiconfig"; +import { expect } from "expect"; + +const debug = _debug("unflakable:integration-common:config"); + +const throwUnimplemented = (): never => { + throw new Error("unimplemented"); +}; + +export type CosmiconfigMockParams = { + searchFrom: string; + searchResult: { + config: Partial; + filepath: string; + } | null; +}; + +export const CONFIG_MOCK_ENV_VAR = "__UNFLAKABLE_TEST_CONFIG_MOCK_PARAMS"; + +export const registerCosmiconfigMock = (): void => { + if (process.env[CONFIG_MOCK_ENV_VAR] === undefined) { + debug( + `Not mocking cosmiconfig since ${CONFIG_MOCK_ENV_VAR} environment variable is not set` + ); + return; + } + + const params = JSON.parse( + process.env[CONFIG_MOCK_ENV_VAR] + ) as CosmiconfigMockParams; + + debug("Mocking cosmiconfig with params %o", params); + + setCosmiconfig( + (moduleName: string, options?: Options): ReturnType => { + expect(moduleName).toBe("unflakable"); + expect(options?.searchPlaces).toContain("package.json"); + expect(options?.searchPlaces).toContain("unflakable.json"); + expect(options?.searchPlaces).toContain("unflakable.js"); + expect(options?.searchPlaces).toContain("unflakable.yaml"); + expect(options?.searchPlaces).toContain("unflakable.yml"); + return { + clearCaches: throwUnimplemented, + clearLoadCache: throwUnimplemented, + clearSearchCache: throwUnimplemented, + load: throwUnimplemented, + search: ( + searchFrom?: string + ): ReturnType["search"]> => { + expect(searchFrom).toBe(params.searchFrom); + return Promise.resolve(params.searchResult); + }, + }; + } + ); +}; diff --git a/packages/cypress-plugin/test/integration-common/src/git.ts b/packages/cypress-plugin/test/integration-common/src/git.ts new file mode 100644 index 0000000..a35a569 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/src/git.ts @@ -0,0 +1,92 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import _debug from "debug"; +import { setSimpleGitFactory } from "@unflakable/plugins-common"; +import { SimpleGit, TaskOptions, Response as GitResponse } from "simple-git"; +import deepEqual from "deep-equal"; + +const debug = _debug("unflakable:integration-common:git"); + +export type SimpleGitMockRef = { + sha: string; + refName: string; +}; +export type SimpleGitMockParams = + | { + abbreviatedRefs?: undefined; + commit?: undefined; + isRepo: false; + refs?: undefined; + } + | { + // Maps ref name (e.g., HEAD or refs/remotes/pull/1/merge) to the `git --abbrev-ref ` + // response (e.g., branch-name, pull/1/merge, or in the case of a detached HEAD, HEAD). + abbreviatedRefs: { [key in string]: string }; + commit: string; + isRepo: true; + refs: SimpleGitMockRef[]; + repoRoot: string; + }; + +export const GIT_MOCK_ENV_VAR = "__UNFLAKABLE_TEST_GIT_MOCK_PARAMS"; + +export const registerSimpleGitMock = (): void => { + if (process.env[GIT_MOCK_ENV_VAR] === undefined) { + debug( + `Not mocking simple-git since ${GIT_MOCK_ENV_VAR} environment variable is not set` + ); + return; + } + + const params = JSON.parse( + process.env[GIT_MOCK_ENV_VAR] + ) as SimpleGitMockParams; + + debug("Mocking simple-git with params %o", params); + + setSimpleGitFactory( + () => + ({ + checkIsRepo: () => + Promise.resolve(params.isRepo) as GitResponse, + revparse: (options: string | TaskOptions) => { + if (!params.isRepo) { + throw new Error("not a git repository"); + } else if ( + Array.isArray(options) && + options.length === 2 && + options[0] === "--abbrev-ref" + ) { + return Promise.resolve( + params.abbreviatedRefs[options[1]] ?? "HEAD" + ) as GitResponse; + } else if ( + Array.isArray(options) && + options.length === 1 && + options[0] === "--show-toplevel" + ) { + // Treat the current working directory as the repo root. + debug(`Returning mock git repo root ${params.repoRoot}`); + return Promise.resolve(params.repoRoot); + } else if (options === "HEAD") { + return Promise.resolve(params.commit) as GitResponse; + } else { + throw new Error(`unexpected options ${options.toString()}`); + } + }, + raw: (options: string | TaskOptions) => { + if (!params.isRepo) { + throw new Error("not a git repository"); + } else if (deepEqual(options, ["show-ref"])) { + return Promise.resolve( + (params.refs ?? []) + .map((mockRef) => `${mockRef.sha} ${mockRef.refName}`) + .join("\n") + "\n" + ) as GitResponse; + } else { + throw new Error(`unexpected options ${options.toString()}`); + } + }, + } as unknown as SimpleGit) + ); +}; diff --git a/packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts b/packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts new file mode 100644 index 0000000..09da367 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Script loaded by Node.JS via --require that mocks cosmiconfig within the cypress-unflakable bin +// script for testing. + +import { registerCosmiconfigMock } from "./config"; + +registerCosmiconfigMock(); diff --git a/packages/cypress-plugin/test/integration-common/src/tsconfig.json b/packages/cypress-plugin/test/integration-common/src/tsconfig.json new file mode 100644 index 0000000..3aa8530 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "../dist", + // Required by Rollup (consumed by Rollup in the *-plugin packages). + "module": "esnext" + }, + "include": ["."] +} diff --git a/packages/cypress-plugin/test/integration-common/tsconfig.json b/packages/cypress-plugin/test/integration-common/tsconfig.json new file mode 100644 index 0000000..6008fb3 --- /dev/null +++ b/packages/cypress-plugin/test/integration-common/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + // Removes DOM types. + "lib": ["ES2019"], + "types": ["node"] + }, + "include": [".eslintrc.js", "rollup.config.mjs"] +} diff --git a/packages/cypress-plugin/test/integration-input-esm/.eslintrc.cjs b/packages/cypress-plugin/test/integration-input-esm/.eslintrc.cjs new file mode 100644 index 0000000..b6febf5 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/.eslintrc.cjs @@ -0,0 +1,5 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + extends: ["../../../../.eslintrc-ts.js"], +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/config-js/devtools.js b/packages/cypress-plugin/test/integration-input-esm/config-js/devtools.js new file mode 100644 index 0000000..afbffdc --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/config-js/devtools.js @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +export const openDevToolsOnLaunch = + /** + * + * @param {Cypress.PluginEvents} on + * @returns void + */ + (on) => { + // Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or + // recordings). + on( + "before:browser:launch", + /** + * @param {Cypress.Browser} browser, + * @param {Cypress.BrowserLaunchOptions} launchOptions + * @returns {void | Cypress.BrowserLaunchOptions} + */ + (browser, launchOptions) => { + if (browser.family === "chromium" && browser.name !== "electron") { + // auto open devtools + launchOptions.args.push("--auto-open-devtools-for-tabs"); + } + + if (browser.family === "firefox") { + // auto open devtools + launchOptions.args.push("-devtools"); + } + + if (browser.name === "electron") { + // auto open devtools + launchOptions.preferences.devTools = true; + } + + // whatever you return here becomes the launchOptions + return launchOptions; + } + ); + }; diff --git a/packages/cypress-plugin/test/integration-input-esm/config-js/tasks.js b/packages/cypress-plugin/test/integration-input-esm/config-js/tasks.js new file mode 100644 index 0000000..4f0f5a3 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/config-js/tasks.js @@ -0,0 +1,16 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +/** + * @param {Cypress.PluginEvents} on + * @returns void + */ +export const registerTasks = (on) => { + // Used for both testing that the support file gets loaded and testing that the project's + // setupNodeEvents() function gets called. + on("task", { + log: (s) => { + console.log(s); + return null; + }, + }); +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/config-js/webpack.js b/packages/cypress-plugin/test/integration-input-esm/config-js/webpack.js new file mode 100644 index 0000000..d159eac --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/config-js/webpack.js @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { default as webpack } from "webpack"; + +export default { + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + plugins: [ + // Fix "process is not defined" error: + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts b/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts new file mode 100644 index 0000000..58d80d5 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +export const openDevToolsOnLaunch = (on: Cypress.PluginEvents): void => { + // Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or + // recordings). + on( + "before:browser:launch", + ( + browser: Cypress.Browser, + launchOptions: Cypress.BrowserLaunchOptions + ): void | Cypress.BrowserLaunchOptions => { + if (browser.family === "chromium" && browser.name !== "electron") { + // auto open devtools + launchOptions.args.push("--auto-open-devtools-for-tabs"); + } + + if (browser.family === "firefox") { + // auto open devtools + launchOptions.args.push("-devtools"); + } + + if (browser.name === "electron") { + // auto open devtools + launchOptions.preferences.devTools = true; + } + + // whatever you return here becomes the launchOptions + return launchOptions; + } + ); +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/config/tasks.ts b/packages/cypress-plugin/test/integration-input-esm/config/tasks.ts new file mode 100644 index 0000000..5d492df --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/config/tasks.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +export const registerTasks = (on: Cypress.PluginEvents): void => { + // Used for both testing that the support file gets loaded and testing that the project's + // setupNodeEvents() function gets called. + on("task", { + log: (s) => { + console.log(s); + return null; + }, + }); +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts b/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts new file mode 100644 index 0000000..ff784d0 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { default as webpack } from "webpack"; + +export default { + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + plugins: [ + // Fix "process is not defined" error: + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], + // stats: "detailed", +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs b/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs new file mode 100644 index 0000000..77e3bcf --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const { registerSimpleGitMock } = require("cypress-integration-common/git"); +const { + registerCosmiconfigMock, +} = require("cypress-integration-common/config"); + +module.exports = { + /** + * @type {Cypress.ConfigOptions} + */ + default: { + component: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + async setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + + const { registerTasks } = await import("./config-js/tasks.js"); + registerTasks(on); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig: async () => + (await import("./config-js/webpack.js")).default, + }, + supportFile: "cypress/support-js/component.cjs", + }, + e2e: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + async setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + + const { registerTasks } = await import("./config-js/tasks.js"); + registerTasks(on); + + // Importing an ESM module from CJS requires dynamic import(). + + const { openDevToolsOnLaunch } = await import( + "./config-js/devtools.js" + ); + + openDevToolsOnLaunch(on); + }, + supportFile: "cypress/support-js/e2e.cjs", + }, + video: false, + }, +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress-config.js b/packages/cypress-plugin/test/integration-input-esm/cypress-config.js new file mode 100644 index 0000000..2b14fc0 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress-config.js @@ -0,0 +1,46 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { openDevToolsOnLaunch } from "./config-js/devtools.js"; +import { registerTasks } from "./config-js/tasks.js"; +import webpackConfig from "./config-js/webpack.js"; +import { registerSimpleGitMock } from "cypress-integration-common/git"; +import { registerCosmiconfigMock } from "cypress-integration-common/config"; + +/** + * @type {Cypress.ConfigOptions} + */ +export default { + component: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + supportFile: "cypress/support-js/component.js", + }, + e2e: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + openDevToolsOnLaunch(on); + }, + supportFile: "cypress/support-js/e2e.js", + }, + video: false, +}; diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts b/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts new file mode 100644 index 0000000..9bd64c9 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { defineConfig } from "cypress"; +// Relative import paths require file extensions in ESM. +// See https://www.typescriptlang.org/docs/handbook/esm-node.html. +import * as devtools from "./config/devtools.js"; +import { registerTasks } from "./config/tasks.js"; +import webpackConfig from "./config/webpack.js"; +import { registerSimpleGitMock } from "cypress-integration-common/git"; +import { registerCosmiconfigMock } from "cypress-integration-common/config"; + +export default defineConfig({ + component: { + setupNodeEvents(on: Cypress.PluginEvents, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + // supportFile: false, + }, + e2e: { + setupNodeEvents( + on: Cypress.PluginEvents, + _config: Cypress.PluginConfigOptions + ): + | Promise + | Cypress.PluginConfigOptions + | void { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + devtools.openDevToolsOnLaunch(on); + }, + // retries: 2, + // supportFile: false, + }, + video: false, +}); diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/component b/packages/cypress-plugin/test/integration-input-esm/cypress/component new file mode 120000 index 0000000..c858326 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/component @@ -0,0 +1 @@ +../../integration-input/cypress/component \ No newline at end of file diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/e2e b/packages/cypress-plugin/test/integration-input-esm/cypress/e2e new file mode 120000 index 0000000..c858326 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/e2e @@ -0,0 +1 @@ +../../integration-input/cypress/component \ No newline at end of file diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/commands.js b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/commands.js new file mode 100644 index 0000000..1a7516c --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/commands.js @@ -0,0 +1,3 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +Cypress.Commands.add("consoleLog", (msg) => cy.task("log", msg)); diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.cjs b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.cjs new file mode 100644 index 0000000..0b677aa --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.cjs @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +require("./commands.js"); + +const { mount } = require("cypress/react18"); + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.js b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.js new file mode 100644 index 0000000..3c38662 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/component.js @@ -0,0 +1,11 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using ES2015 syntax: +import "./commands.js"; + +import { mount } from "cypress/react18"; + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.cjs b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.cjs new file mode 100644 index 0000000..c254fe4 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.cjs @@ -0,0 +1,4 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using CJS syntax: +require("./commands.js"); diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.js b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.js new file mode 100644 index 0000000..0447f29 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support-js/e2e.js @@ -0,0 +1,4 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using ES2015 syntax: +import "./commands.js"; diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support/commands.ts b/packages/cypress-plugin/test/integration-input-esm/cypress/support/commands.ts new file mode 100644 index 0000000..a6cb68a --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/commands.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Webpack non-deterministically reports a TypeScript error here when run through Cypress. It seems +// to happen when `cypress-support-file` includes +// vendors-node_modules_cypress_react18_dist_cypress-react_esm-bundler_js.js instead of +// vendors-node_modules_cypress_react18_dist_cypress-react_cjs_js.js (printed when +// `stats: detailed` is set in the Webpack config). It's unclear if this is a caching issue or +// potentially caused by @unflakable/cypress-plugin being a CommonJS package (since Cypress assumes +// the project is based where the config file is). The non-determinism makes it hard to diagnose, +// but the tests seem to run despite the error. +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + consoleLog(msg: string): Chainable; + } + } +} + +Cypress.Commands.add("consoleLog", (msg: string) => cy.task("log", msg)); diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support/component-index.html b/packages/cypress-plugin/test/integration-input-esm/cypress/support/component-index.html new file mode 100644 index 0000000..e39ba42 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support/component.ts b/packages/cypress-plugin/test/integration-input-esm/cypress/support/component.ts new file mode 100644 index 0000000..87db971 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/component.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import "./commands.ts"; + +import { mount } from "cypress/react18"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + mount: typeof mount; + } + } +} + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress/support/e2e.ts b/packages/cypress-plugin/test/integration-input-esm/cypress/support/e2e.ts new file mode 100644 index 0000000..8fd06b4 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/e2e.ts @@ -0,0 +1,3 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import "./commands.ts"; diff --git a/packages/cypress-plugin/test/integration-input-esm/package.json b/packages/cypress-plugin/test/integration-input-esm/package.json new file mode 100644 index 0000000..d591569 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/package.json @@ -0,0 +1,24 @@ +{ + "name": "cypress-integration-input-esm", + "description": "Integration test input that uses ESM to test @unflakable/cypress-plugin's support for wrapping Cypress config and support files that use ESM", + "private": true, + "type": "module", + "devDependencies": { + "@types/react": "^18.2.7", + "@types/react-dom": "^18.2.4", + "@unflakable/cypress-plugin": "workspace:^", + "cypress": "10 - 12", + "cypress-integration-common": "workspace:^", + "mocha": "=7.0.1", + "mocha-junit-reporter": "^2.2.0", + "process": "^0.11.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-loader": "^9.4.3", + "typescript": "^4.9.5", + "webpack": "^5.84.1" + }, + "scripts": { + "typecheck": "tsc --noEmit" + } +} diff --git a/packages/cypress-plugin/test/integration-input-esm/tsconfig.json b/packages/cypress-plugin/test/integration-input-esm/tsconfig.json new file mode 100644 index 0000000..aff7c99 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-esm/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "module": "ES2022", + "preserveSymlinks": true, + // The ts-loader that Cypress uses doesn't seem to support the `??` operator. + "target": "ES2019", + // Avoids conflicting global definitions from, e.g., jest. + "types": ["cypress", "node"] + }, + "include": [ + ".eslintrc.cjs", + "config", + "config-js", + "cypress", + "cypress.config.ts", + "cypress-config.cjs", + "cypress-config.js" + ] +} diff --git a/packages/cypress-plugin/test/integration-input-manual/config/devtools.js b/packages/cypress-plugin/test/integration-input-manual/config/devtools.js new file mode 100644 index 0000000..c117765 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/config/devtools.js @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + /** + * @param {Cypress.PluginEvents} on + * @returns void + */ + openDevToolsOnLaunch: (on) => { + // Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or + // recordings). + on( + "before:browser:launch", + /** + * @param {Cypress.Browser} browser, + * @param {Cypress.BrowserLaunchOptions} launchOptions + * @returns {void | Cypress.BrowserLaunchOptions} + */ + (browser, launchOptions) => { + if (browser.family === "chromium" && browser.name !== "electron") { + // auto open devtools + launchOptions.args.push("--auto-open-devtools-for-tabs"); + } + + if (browser.family === "firefox") { + // auto open devtools + launchOptions.args.push("-devtools"); + } + + if (browser.name === "electron") { + // auto open devtools + launchOptions.preferences.devTools = true; + } + + // whatever you return here becomes the launchOptions + return launchOptions; + } + ); + }, +}; diff --git a/packages/cypress-plugin/test/integration-input-manual/config/tasks.js b/packages/cypress-plugin/test/integration-input-manual/config/tasks.js new file mode 100644 index 0000000..95cba3e --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/config/tasks.js @@ -0,0 +1,18 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + /** + * @param {Cypress.PluginEvents} on + * @returns void + */ + registerTasks: (on) => { + // Used for both testing that the support file gets loaded and testing that the project's + // setupNodeEvents() function gets called. + on("task", { + log: (s) => { + console.log(s); + return null; + }, + }); + }, +}; diff --git a/packages/cypress-plugin/test/integration-input-manual/config/webpack.js b/packages/cypress-plugin/test/integration-input-manual/config/webpack.js new file mode 100644 index 0000000..c6501b8 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/config/webpack.js @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const webpack = require("webpack"); + +module.exports = { + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + plugins: [ + // Fix "process is not defined" error: + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, +}; diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs b/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs new file mode 100644 index 0000000..37d987c --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import devtools from "./config/devtools.js"; +import tasks from "./config/tasks.js"; +import webpackConfig from "./config/webpack.js"; +import { registerSimpleGitMock } from "cypress-integration-common/git"; +import { registerCosmiconfigMock } from "cypress-integration-common/config"; +import { registerUnflakable } from "@unflakable/cypress-plugin"; + +/** + * @type {Cypress.ConfigOptions} + */ +export default { + component: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + tasks.registerTasks(on); + + return registerUnflakable(on, config); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + // supportFile: "cypress/support-js/component.js", + }, + e2e: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + tasks.registerTasks(on); + devtools.openDevToolsOnLaunch(on); + + return registerUnflakable(on, config); + }, + // supportFile: "cypress/support/e2e.js", + }, + quiet: true, + // Test what happens if they're already using cypress-multi-reporters. + // NB: We set installConfig.hoistingLimits in package.json so that Cypress can find + // cypress-multi-reporters in its expected location in node_modules. + reporter: "cypress-multi-reporters", + reporterOptions: { + configFile: "cypress-multi-reporters.config.json", + }, + video: false, +}; diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress-multi-reporters.config.json b/packages/cypress-plugin/test/integration-input-manual/cypress-multi-reporters.config.json new file mode 100644 index 0000000..0997035 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress-multi-reporters.config.json @@ -0,0 +1,6 @@ +{ + "reporterEnabled": ["spec", "xunit"], + "xunitReporterOptions": { + "output": "cypress/results/xunit.xml" + } +} diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress.config.js b/packages/cypress-plugin/test/integration-input-manual/cypress.config.js new file mode 100644 index 0000000..f11bae3 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress.config.js @@ -0,0 +1,62 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const { openDevToolsOnLaunch } = require("./config/devtools"); +const { registerTasks } = require("./config/tasks"); +const webpackConfig = require("./config/webpack"); +const { registerSimpleGitMock } = require("cypress-integration-common/git"); +const { + registerCosmiconfigMock, +} = require("cypress-integration-common/config"); + +const { registerUnflakable } = require("@unflakable/cypress-plugin"); + +module.exports = { + /** + * @type {Cypress.ConfigOptions} + */ + component: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + + return registerUnflakable(on, config); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + // supportFile: "cypress/support/component.js", + }, + e2e: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + openDevToolsOnLaunch(on); + + return registerUnflakable(on, config); + }, + // supportFile: "cypress/support/e2e.js", + }, + quiet: true, + // Test what happens if they're already using cypress-multi-reporters. + // NB: We set installConfig.hoistingLimits in package.json so that Cypress can find + // cypress-multi-reporters in its expected location in node_modules. + reporter: "cypress-multi-reporters", + reporterOptions: { + configFile: "cypress-multi-reporters.config.json", + }, + video: false, +}; diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/component b/packages/cypress-plugin/test/integration-input-manual/cypress/component new file mode 120000 index 0000000..425210f --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/component @@ -0,0 +1 @@ +./e2e \ No newline at end of file diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/fail.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/fail.cy.js new file mode 100644 index 0000000..c1bec18 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/fail.cy.js @@ -0,0 +1,32 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const testFn = Cypress.env("SKIP_FAILURES") !== undefined ? it.skip : it; + +describe("describe block", () => { + testFn("should fail", () => { + throw new Error(); + }); + + describe("inner block", () => { + testFn("should showDiff", () => { + expect("foobar").to.equal("foo"); + }); + }); + + // Tests can fail with multiple exceptions, and we should print those as the Mocha spec reporter + // does: https://github.com/mochajs/mocha/pull/4033. + testFn( + "should fail with multiple exceptions", + /** + * @param {Mocha.Done} done + */ + (done) => { + process.nextTick(() => { + throw new Error("first"); + }); + process.nextTick(() => { + done(new Error("second")); + }); + } + ); +}); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/flake.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/flake.cy.js new file mode 100644 index 0000000..1bab7bc --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/flake.cy.js @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +/* +let calls = 0; + +afterEach(() => + cy.task("log", `RUNNING HOOK WITH CALLS=${calls}`).then(() => { + if (calls++ === 0) { + return cy + .task("log", `failing hook`) + .then(() => Promise.reject(new Error("afterEach failure"))); + } else { + return; + } + }) +); +*/ + +(Cypress.env("SKIP_FLAKE") !== undefined ? it.skip : it)( + `should be flaky${ + /** + * @type {string | undefined} + */ + Cypress.env("FLAKE_TEST_NAME_SUFFIX") ?? "" + }`, + () => { + if (cy.state("test").currentRetry() === 0) { + throw new Error("first try should fail"); + } + } +); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/hook-fail.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/hook-fail.cy.js new file mode 100644 index 0000000..3eca195 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/hook-fail.cy.js @@ -0,0 +1,85 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +describe("describe block", () => { + if (Cypress.env("SKIP_BEFORE_HOOK") === undefined) { + before( + /** + * @param {Mocha.Done} done + */ + (done) => { + process.nextTick(() => { + throw new Error("before Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("before Error #2")); + }); + } + } + ); + } + + if (Cypress.env("SKIP_BEFORE_EACH_HOOK") === undefined) { + beforeEach( + /** + * @param {Mocha.Done} done + */ + (done) => { + process.nextTick(() => { + throw new Error("before Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("before Error #2")); + }); + } + } + ); + } + + // The before[Each] hook failure gets attached to this test. + it("should fail due to hook", () => { + if (Cypress.env("HOOK_AND_TEST_ERRORS") !== undefined) { + throw new Error("test error"); + } + }); + + // This test should never run when a before()/beforeEach()/afterEach() hook fails. + it("should be skipped", () => undefined); + + if (Cypress.env("SKIP_AFTER_EACH_HOOK") === undefined) { + afterEach( + /** + * @param {Mocha.Done} done + */ + (done) => { + process.nextTick(() => { + throw new Error("after Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("after Error #2")); + }); + } + } + ); + } + + if (Cypress.env("SKIP_AFTER_HOOK") === undefined) { + after( + /** + * @param {Mocha.Done} done + */ + (done) => { + process.nextTick(() => { + throw new Error("after Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("after Error #2")); + }); + } + } + ); + } +}); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/invalid.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/invalid.cy.js new file mode 100644 index 0000000..aa580f4 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/invalid.cy.js @@ -0,0 +1,5 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +if (Cypress.env("SKIP_FAILURES") === undefined) { + throw new Error("invalid test file"); +} diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/mixed/mixed.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/mixed/mixed.cy.js new file mode 100644 index 0000000..e731026 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/mixed/mixed.cy.js @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +describe("spec with mixed test results", () => { + const quarantinedTestFn = + Cypress.env("SKIP_QUARANTINED") !== undefined ? it.skip : it; + + quarantinedTestFn("mixed: failure should be quarantined", () => { + throw new Error(); + }); + + quarantinedTestFn("mixed: flake should be quarantined", () => { + if (cy.state("test").currentRetry() === 0) { + throw new Error("first try should fail"); + } + }); + + (Cypress.env("SKIP_FAILURES") !== undefined ? it.skip : it)( + "mixed: should fail", + () => { + throw new Error(); + } + ); + + (Cypress.env("SKIP_FLAKE") !== undefined ? it.skip : it)( + "mixed: should be flaky", + () => { + if (cy.state("test").currentRetry() === 0) { + throw new Error("first try should fail"); + } + } + ); + + it("mixed: should pass", () => undefined); + + it.skip("mixed: should be skipped", () => undefined); +}); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pass.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pass.cy.js new file mode 100644 index 0000000..05d345a --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pass.cy.js @@ -0,0 +1,11 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +it("should pass", () => { + // Make sure the project's support file works even when skip_tests generates a temporary one on + // the fly. + cy.consoleLog("called consoleLog command"); +}); + +describe("suite name", () => { + it("suite test should pass", () => undefined); +}); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pending.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pending.cy.js new file mode 100644 index 0000000..2704e82 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/pending.cy.js @@ -0,0 +1,11 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +it("stub should be pending"); + +it.skip("should be pending", () => undefined); + +describe("suite name", () => { + it.skip("suite test should be pending", () => undefined); + + it.skip("suite test should be quarantined and pending", () => undefined); +}); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/quarantined.cy.js b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/quarantined.cy.js new file mode 100644 index 0000000..f43ebc0 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/e2e/quarantined.cy.js @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +describe("describe block", () => { + (Cypress.env("SKIP_QUARANTINED") !== undefined ? it.skip : it)( + "should be quarantined", + () => { + throw new Error(); + } + ); +}); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/support/commands.js b/packages/cypress-plugin/test/integration-input-manual/cypress/support/commands.js new file mode 100644 index 0000000..1a7516c --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/commands.js @@ -0,0 +1,3 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +Cypress.Commands.add("consoleLog", (msg) => cy.task("log", msg)); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/support/component-index.html b/packages/cypress-plugin/test/integration-input-manual/cypress/support/component-index.html new file mode 100644 index 0000000..e39ba42 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/support/component.js b/packages/cypress-plugin/test/integration-input-manual/cypress/support/component.js new file mode 100644 index 0000000..5d0823f --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/component.js @@ -0,0 +1,16 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +require("./commands"); + +const { mount } = require("cypress/react18"); + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() + +const { + registerMochaInstrumentation, +} = require("@unflakable/cypress-plugin/skip-tests"); + +registerMochaInstrumentation(); diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e.js b/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e.js new file mode 100644 index 0000000..9033340 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e.js @@ -0,0 +1,13 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using CJS syntax: +require("./commands.js"); + +// We have to use @unflakable/cypress-plugin/dist/skip-tests here due to Cypress depending on +// Webpack 4, which doesn't support sub-path exports. See +// https://github.com/cypress-io/cypress/issues/23826. +const { + registerMochaInstrumentation, +} = require("@unflakable/cypress-plugin/dist/skip-tests"); + +registerMochaInstrumentation(); diff --git a/packages/cypress-plugin/test/integration-input-manual/package.json b/packages/cypress-plugin/test/integration-input-manual/package.json new file mode 100644 index 0000000..f880b09 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input-manual/package.json @@ -0,0 +1,24 @@ +{ + "name": "cypress-integration-input-manual", + "description": "Integration test input that manually registers Unflakable plugin and skip-tests Mocha instrumentation", + "private": true, + "installConfig": { + "hoistingLimits": "workspaces" + }, + "devDependencies": { + "@types/react": "^18.2.7", + "@types/react-dom": "^18.2.4", + "@unflakable/cypress-plugin": "workspace:^", + "cypress": "10 - 12", + "cypress-integration-common": "workspace:^", + "cypress-multi-reporters": "^1.6.3", + "mocha": "=7.0.1", + "mocha-junit-reporter": "^2.2.0", + "process": "^0.11.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-loader": "^9.4.3", + "typescript": "^4.9.5", + "webpack": "^5.84.1" + } +} diff --git a/packages/cypress-plugin/test/integration-input/.eslintrc.js b/packages/cypress-plugin/test/integration-input/.eslintrc.js new file mode 100644 index 0000000..b6febf5 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/.eslintrc.js @@ -0,0 +1,5 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + extends: ["../../../../.eslintrc-ts.js"], +}; diff --git a/packages/cypress-plugin/test/integration-input/config-js/devtools.js b/packages/cypress-plugin/test/integration-input/config-js/devtools.js new file mode 100644 index 0000000..c117765 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/config-js/devtools.js @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + /** + * @param {Cypress.PluginEvents} on + * @returns void + */ + openDevToolsOnLaunch: (on) => { + // Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or + // recordings). + on( + "before:browser:launch", + /** + * @param {Cypress.Browser} browser, + * @param {Cypress.BrowserLaunchOptions} launchOptions + * @returns {void | Cypress.BrowserLaunchOptions} + */ + (browser, launchOptions) => { + if (browser.family === "chromium" && browser.name !== "electron") { + // auto open devtools + launchOptions.args.push("--auto-open-devtools-for-tabs"); + } + + if (browser.family === "firefox") { + // auto open devtools + launchOptions.args.push("-devtools"); + } + + if (browser.name === "electron") { + // auto open devtools + launchOptions.preferences.devTools = true; + } + + // whatever you return here becomes the launchOptions + return launchOptions; + } + ); + }, +}; diff --git a/packages/cypress-plugin/test/integration-input/config-js/tasks.js b/packages/cypress-plugin/test/integration-input/config-js/tasks.js new file mode 100644 index 0000000..95cba3e --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/config-js/tasks.js @@ -0,0 +1,18 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + /** + * @param {Cypress.PluginEvents} on + * @returns void + */ + registerTasks: (on) => { + // Used for both testing that the support file gets loaded and testing that the project's + // setupNodeEvents() function gets called. + on("task", { + log: (s) => { + console.log(s); + return null; + }, + }); + }, +}; diff --git a/packages/cypress-plugin/test/integration-input/config-js/webpack.js b/packages/cypress-plugin/test/integration-input/config-js/webpack.js new file mode 100644 index 0000000..c6501b8 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/config-js/webpack.js @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const webpack = require("webpack"); + +module.exports = { + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + plugins: [ + // Fix "process is not defined" error: + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, +}; diff --git a/packages/cypress-plugin/test/integration-input/config/devtools.ts b/packages/cypress-plugin/test/integration-input/config/devtools.ts new file mode 100644 index 0000000..58d80d5 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/config/devtools.ts @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +export const openDevToolsOnLaunch = (on: Cypress.PluginEvents): void => { + // Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or + // recordings). + on( + "before:browser:launch", + ( + browser: Cypress.Browser, + launchOptions: Cypress.BrowserLaunchOptions + ): void | Cypress.BrowserLaunchOptions => { + if (browser.family === "chromium" && browser.name !== "electron") { + // auto open devtools + launchOptions.args.push("--auto-open-devtools-for-tabs"); + } + + if (browser.family === "firefox") { + // auto open devtools + launchOptions.args.push("-devtools"); + } + + if (browser.name === "electron") { + // auto open devtools + launchOptions.preferences.devTools = true; + } + + // whatever you return here becomes the launchOptions + return launchOptions; + } + ); +}; diff --git a/packages/cypress-plugin/test/integration-input/config/tasks.ts b/packages/cypress-plugin/test/integration-input/config/tasks.ts new file mode 100644 index 0000000..5d492df --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/config/tasks.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +export const registerTasks = (on: Cypress.PluginEvents): void => { + // Used for both testing that the support file gets loaded and testing that the project's + // setupNodeEvents() function gets called. + on("task", { + log: (s) => { + console.log(s); + return null; + }, + }); +}; diff --git a/packages/cypress-plugin/test/integration-input/config/webpack.ts b/packages/cypress-plugin/test/integration-input/config/webpack.ts new file mode 100644 index 0000000..d159eac --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/config/webpack.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { default as webpack } from "webpack"; + +export default { + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + plugins: [ + // Fix "process is not defined" error: + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, +}; diff --git a/packages/cypress-plugin/test/integration-input/cypress-config.js b/packages/cypress-plugin/test/integration-input/cypress-config.js new file mode 100644 index 0000000..e83d1a0 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress-config.js @@ -0,0 +1,50 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const { openDevToolsOnLaunch } = require("config-js/devtools"); +const webpackConfig = require("config-js/webpack"); +const { registerTasks } = require("config-js/tasks"); +const { registerSimpleGitMock } = require("cypress-integration-common/git"); +const { + registerCosmiconfigMock, +} = require("cypress-integration-common/config"); + +module.exports = { + /** + * @type {Cypress.ConfigOptions} + */ + default: { + component: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + supportFile: "cypress/support-js/component.js", + }, + e2e: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + openDevToolsOnLaunch(on); + }, + supportFile: "cypress/support-js/e2e.js", + }, + video: false, + }, +}; diff --git a/packages/cypress-plugin/test/integration-input/cypress-config.mjs b/packages/cypress-plugin/test/integration-input/cypress-config.mjs new file mode 100644 index 0000000..74e8a64 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress-config.mjs @@ -0,0 +1,46 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import devtools from "./config-js/devtools.js"; +import tasks from "./config-js/tasks.js"; +import webpackConfig from "./config-js/webpack.js"; +import { registerSimpleGitMock } from "cypress-integration-common/git"; +import { registerCosmiconfigMock } from "cypress-integration-common/config"; + +/** + * @type {Cypress.ConfigOptions} + */ +export default { + component: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + tasks.registerTasks(on); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + supportFile: "cypress/support-js/component.mjs", + }, + e2e: { + /** + * @param {Cypress.PluginEvents} on + * @param {Cypress.PluginConfigOptions} _config + * @returns {Promise | Cypress.PluginConfigOptions | void} + */ + setupNodeEvents(on, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + tasks.registerTasks(on); + devtools.openDevToolsOnLaunch(on); + }, + supportFile: "cypress/support-js/e2e.mjs", + }, + video: false, +}; diff --git a/packages/cypress-plugin/test/integration-input/cypress.config.ts b/packages/cypress-plugin/test/integration-input/cypress.config.ts new file mode 100644 index 0000000..4e1efe8 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress.config.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { registerSimpleGitMock } from "cypress-integration-common/git"; +import { registerCosmiconfigMock } from "cypress-integration-common/config"; +import { defineConfig } from "cypress"; +// This intentionally uses the CommonJS relative import syntax that doesn't start with `./` in +// order to test that our inclusion of the user config file resolves relative path imports (via +// Cypress's use of tsconfig-paths) correctly. +// eslint-disable-next-line import/no-unresolved +import { openDevToolsOnLaunch } from "config/devtools"; +// eslint-disable-next-line import/no-unresolved +import webpackConfig from "config/webpack"; +// eslint-disable-next-line import/no-unresolved +import { registerTasks } from "config/tasks"; + +export default defineConfig({ + component: { + setupNodeEvents(on: Cypress.PluginEvents, _config) { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + openDevToolsOnLaunch(on); + }, + devServer: { + bundler: "webpack", + framework: "react", + webpackConfig, + }, + // supportFile: false, + }, + e2e: { + setupNodeEvents( + on: Cypress.PluginEvents, + _config + ): + | Promise + | Cypress.PluginConfigOptions + | void { + registerCosmiconfigMock(); + registerSimpleGitMock(); + registerTasks(on); + openDevToolsOnLaunch(on); + }, + // retries: 2, + // supportFile: false, + }, + video: false, +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/fail.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/fail.cy.ts new file mode 100644 index 0000000..94b40a9 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/fail.cy.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +const testFn = Cypress.env("SKIP_FAILURES") !== undefined ? it.skip : it; + +describe("describe block", () => { + testFn("should fail", () => { + throw new Error(); + }); + + describe("inner block", () => { + testFn("should showDiff", () => { + expect("foobar").to.equal("foo"); + }); + }); + + // Tests can fail with multiple exceptions, and we should print those as the Mocha spec reporter + // does: https://github.com/mochajs/mocha/pull/4033. + testFn("should fail with multiple exceptions", (done: Mocha.Done) => { + process.nextTick(() => { + throw new Error("first"); + }); + process.nextTick(() => { + done(new Error("second")); + }); + }); +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/flake.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/flake.cy.ts new file mode 100644 index 0000000..6cda5a0 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/flake.cy.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +/* +let calls = 0; + +afterEach(() => + cy.task("log", `RUNNING HOOK WITH CALLS=${calls}`).then(() => { + if (calls++ === 0) { + return cy + .task("log", `failing hook`) + .then(() => Promise.reject(new Error("afterEach failure"))); + } else { + return; + } + }) +); +*/ + +(Cypress.env("SKIP_FLAKE") !== undefined ? it.skip : it)( + `should be flaky${ + (Cypress.env("FLAKE_TEST_NAME_SUFFIX") as string | undefined) ?? "" + }`, + () => { + if ( + /* eslint-disable */ + // @ts-ignore + cy.state("test").currentRetry() === 0 + /* eslint-enable */ + ) { + throw new Error("first try should fail"); + } + } +); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/hook-fail.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/hook-fail.cy.ts new file mode 100644 index 0000000..f37bae2 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/hook-fail.cy.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +describe("describe block", () => { + if (Cypress.env("SKIP_BEFORE_HOOK") === undefined) { + before((done: Mocha.Done) => { + process.nextTick(() => { + throw new Error("before Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("before Error #2")); + }); + } + }); + } + + if (Cypress.env("SKIP_BEFORE_EACH_HOOK") === undefined) { + beforeEach((done: Mocha.Done) => { + process.nextTick(() => { + throw new Error("beforeEach Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("beforeEach Error #2")); + }); + } + }); + } + + it("should fail due to hook", () => { + if (Cypress.env("HOOK_AND_TEST_ERRORS") !== undefined) { + throw new Error("test error"); + } + }); + + // This test should never run when a before()/beforeEach()/afterEach() hook fails. + it("should be skipped", () => undefined); + + if (Cypress.env("SKIP_AFTER_EACH_HOOK") === undefined) { + afterEach((done: Mocha.Done) => { + process.nextTick(() => { + throw new Error("afterEach Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("afterEach Error #2")); + }); + } + }); + } + + if (Cypress.env("SKIP_AFTER_HOOK") === undefined) { + after((done: Mocha.Done) => { + process.nextTick(() => { + throw new Error("after Error #1"); + }); + if (Cypress.env("MULTIPLE_HOOK_ERRORS") !== undefined) { + process.nextTick(() => { + done(new Error("after Error #2")); + }); + } + }); + } +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/invalid.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/invalid.cy.ts new file mode 100644 index 0000000..aa580f4 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/invalid.cy.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +if (Cypress.env("SKIP_FAILURES") === undefined) { + throw new Error("invalid test file"); +} diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/mixed/mixed.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/mixed/mixed.cy.ts new file mode 100644 index 0000000..b0ee438 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/mixed/mixed.cy.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +describe("spec with mixed test results", () => { + const quarantinedTestFn = + Cypress.env("SKIP_QUARANTINED") !== undefined ? it.skip : it; + + quarantinedTestFn("mixed: failure should be quarantined", () => { + throw new Error(); + }); + + quarantinedTestFn("mixed: flake should be quarantined", () => { + if ( + /* eslint-disable */ + // @ts-ignore + cy.state("test").currentRetry() === 0 + /* eslint-enable */ + ) { + throw new Error("first try should fail"); + } + }); + + (Cypress.env("SKIP_FAILURES") !== undefined ? it.skip : it)( + "mixed: should fail", + () => { + throw new Error(); + } + ); + + (Cypress.env("SKIP_FLAKE") !== undefined ? it.skip : it)( + "mixed: should be flaky", + () => { + if ( + /* eslint-disable */ + // @ts-ignore + cy.state("test").currentRetry() === 0 + /* eslint-enable */ + ) { + throw new Error("first try should fail"); + } + } + ); + + it("mixed: should pass", () => undefined); + + it.skip("mixed: should be skipped", () => undefined); +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/pass.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/pass.cy.ts new file mode 100644 index 0000000..05d345a --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/pass.cy.ts @@ -0,0 +1,11 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +it("should pass", () => { + // Make sure the project's support file works even when skip_tests generates a temporary one on + // the fly. + cy.consoleLog("called consoleLog command"); +}); + +describe("suite name", () => { + it("suite test should pass", () => undefined); +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/pending.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/pending.cy.ts new file mode 100644 index 0000000..2704e82 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/pending.cy.ts @@ -0,0 +1,11 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +it("stub should be pending"); + +it.skip("should be pending", () => undefined); + +describe("suite name", () => { + it.skip("suite test should be pending", () => undefined); + + it.skip("suite test should be quarantined and pending", () => undefined); +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/component/quarantined.cy.ts b/packages/cypress-plugin/test/integration-input/cypress/component/quarantined.cy.ts new file mode 100644 index 0000000..f43ebc0 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/component/quarantined.cy.ts @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +describe("describe block", () => { + (Cypress.env("SKIP_QUARANTINED") !== undefined ? it.skip : it)( + "should be quarantined", + () => { + throw new Error(); + } + ); +}); diff --git a/packages/cypress-plugin/test/integration-input/cypress/e2e b/packages/cypress-plugin/test/integration-input/cypress/e2e new file mode 120000 index 0000000..63473bb --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/e2e @@ -0,0 +1 @@ +./component \ No newline at end of file diff --git a/packages/cypress-plugin/test/integration-input/cypress/fixtures/example.json b/packages/cypress-plugin/test/integration-input/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/packages/cypress-plugin/test/integration-input/cypress/support-js/commands.js b/packages/cypress-plugin/test/integration-input/cypress/support-js/commands.js new file mode 100644 index 0000000..1a7516c --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/commands.js @@ -0,0 +1,3 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +Cypress.Commands.add("consoleLog", (msg) => cy.task("log", msg)); diff --git a/packages/cypress-plugin/test/integration-input/cypress/support-js/component.js b/packages/cypress-plugin/test/integration-input/cypress/support-js/component.js new file mode 100644 index 0000000..fea2a9b --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/component.js @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +require("./commands"); + +const { mount } = require("cypress/react18"); + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() diff --git a/packages/cypress-plugin/test/integration-input/cypress/support-js/component.mjs b/packages/cypress-plugin/test/integration-input/cypress/support-js/component.mjs new file mode 100644 index 0000000..3c38662 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/component.mjs @@ -0,0 +1,11 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using ES2015 syntax: +import "./commands.js"; + +import { mount } from "cypress/react18"; + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() diff --git a/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.js b/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.js new file mode 100644 index 0000000..c254fe4 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.js @@ -0,0 +1,4 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using CJS syntax: +require("./commands.js"); diff --git a/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.mjs b/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.mjs new file mode 100644 index 0000000..0447f29 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.mjs @@ -0,0 +1,4 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using ES2015 syntax: +import "./commands.js"; diff --git a/packages/cypress-plugin/test/integration-input/cypress/support/commands.ts b/packages/cypress-plugin/test/integration-input/cypress/support/commands.ts new file mode 100644 index 0000000..966021a --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support/commands.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +/// + +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + interface Chainable { + consoleLog(msg: string): Chainable; + } +} + +Cypress.Commands.add("consoleLog", (msg: string) => cy.task("log", msg)); diff --git a/packages/cypress-plugin/test/integration-input/cypress/support/component-index.html b/packages/cypress-plugin/test/integration-input/cypress/support/component-index.html new file mode 100644 index 0000000..e39ba42 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + diff --git a/packages/cypress-plugin/test/integration-input/cypress/support/component.ts b/packages/cypress-plugin/test/integration-input/cypress/support/component.ts new file mode 100644 index 0000000..5fbee34 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support/component.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Import commands.js using ES2015 syntax: +import "./commands"; + +import { mount } from "cypress/react18"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + mount: typeof mount; + } + } +} + +Cypress.Commands.add("mount", mount); + +// Example use: +// cy.mount() diff --git a/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts b/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts new file mode 100644 index 0000000..5aa0b7c --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts @@ -0,0 +1,3 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import "./commands"; diff --git a/packages/cypress-plugin/test/integration-input/package.json b/packages/cypress-plugin/test/integration-input/package.json new file mode 100644 index 0000000..1f5bc39 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/package.json @@ -0,0 +1,23 @@ +{ + "name": "cypress-integration-input", + "description": "Integration test input that uses CommonJS to test @unflakable/cypress-plugin's support for wrapping Cypress config and support files that use CommonJS", + "private": true, + "devDependencies": { + "@types/react": "^18.2.7", + "@types/react-dom": "^18.2.4", + "@unflakable/cypress-plugin": "workspace:^", + "cypress": "10 - 12", + "cypress-integration-common": "workspace:^", + "mocha": "=7.0.1", + "mocha-junit-reporter": "^2.2.0", + "process": "^0.11.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-loader": "^9.4.3", + "typescript": "^4.9.5", + "webpack": "^5.84.1" + }, + "scripts": { + "typecheck": "tsc --noEmit" + } +} diff --git a/packages/cypress-plugin/test/integration-input/reporter-config.json b/packages/cypress-plugin/test/integration-input/reporter-config.json new file mode 100644 index 0000000..417c897 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/reporter-config.json @@ -0,0 +1,6 @@ +{ + "reporterEnabled": "spec, mocha-junit-reporter", + "mochaJunitReporterReporterOptions": { + "mochaFile": "cypress/results/results-[hash].xml" + } +} diff --git a/packages/cypress-plugin/test/integration-input/tsconfig.json b/packages/cypress-plugin/test/integration-input/tsconfig.json new file mode 100644 index 0000000..1243db8 --- /dev/null +++ b/packages/cypress-plugin/test/integration-input/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "preserveSymlinks": true, + // The ts-loader that Cypress uses doesn't seem to support the `??` operator. + "target": "ES2019", + // Avoids conflicting global definitions from, e.g., jest. + "types": ["cypress", "node"] + }, + "include": [ + ".eslintrc.js", + "config", + "config-js", + "cypress", + "cypress.config.ts", + "cypress-config.js", + "cypress-config.mjs" + ] +} diff --git a/packages/cypress-plugin/test/integration/.eslintrc.js b/packages/cypress-plugin/test/integration/.eslintrc.js new file mode 100644 index 0000000..b6febf5 --- /dev/null +++ b/packages/cypress-plugin/test/integration/.eslintrc.js @@ -0,0 +1,5 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +module.exports = { + extends: ["../../../../.eslintrc-ts.js"], +}; diff --git a/packages/cypress-plugin/test/integration/jest.config.js b/packages/cypress-plugin/test/integration/jest.config.js new file mode 100644 index 0000000..fba616d --- /dev/null +++ b/packages/cypress-plugin/test/integration/jest.config.js @@ -0,0 +1,18 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// NB: We *MUST* run this test suite with --runInBand because running multiple instances of Cypress +// concurrently on the same machine is not supported and runs into a bunch of race conditions that +// cause tests to fail. See https://github.com/cypress-io/cypress/issues/9135 and +// https://github.com/cypress-io/cypress/issues/1426. Using --runInBand ensures that Jest doesn't +// run tests concurrently, which in turn ensures that we don't kick off multiple Cypress instances +// concurrently. + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + setupFilesAfterEnv: ["jest-expect-message", "./src/matchers.ts"], + testEnvironment: "node", + // NB: This should be greater than TEST_TIMEOUT_MS used by the watchdog in runTestCase(). + testTimeout: 60000, + verbose: true, +}; diff --git a/packages/cypress-plugin/test/integration/package.json b/packages/cypress-plugin/test/integration/package.json new file mode 100644 index 0000000..43c3ea0 --- /dev/null +++ b/packages/cypress-plugin/test/integration/package.json @@ -0,0 +1,26 @@ +{ + "name": "cypress-integration", + "private": true, + "devDependencies": { + "@types/jest": "^29.5.2", + "@unflakable/cypress-plugin": "workspace:^", + "@unflakable/jest-plugin": "workspace:^", + "@unflakable/js-api": "workspace:^", + "@unflakable/plugins-common": "workspace:^", + "cypress": "10 - 12", + "cypress-integration-common": "workspace:^", + "debug": "^4.3.3", + "escape-string-regexp": "^4.0.0", + "jest": "^29.5.0", + "jest-environment-node": "^29.5.0", + "jest-expect-message": "^1.1.3", + "mockttp": "^3.7.5", + "tree-kill": "^1.2.2", + "ts-jest": "^29.1.0", + "typescript": "^4.9.5" + }, + "scripts": { + "test": "jest --useStderr --verbose --runInBand", + "typecheck": "tsc" + } +} diff --git a/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts b/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts new file mode 100644 index 0000000..3d5aec9 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts @@ -0,0 +1,87 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; +import { TEST_PROJECTS, TestMode, TestProjectName } from "./run-test-case"; +import { QuarantineMode } from "@unflakable/plugins-common"; +import { afterEach, beforeEach } from "@jest/globals"; +import * as fs from "fs/promises"; + +integrationTestSuite(() => { + Object.entries(TEST_PROJECTS).forEach(([projectName, project]) => { + describe( + projectName === "integration-input" + ? "CommonJS" + : projectName === "integration-input-esm" + ? "ESM" + : "CommonJS (manual)", + () => { + if (projectName === "integration-input-manual") { + const xunitPath = + "../integration-input-manual/cypress/results/xunit.xml"; + beforeEach(async () => { + // Try to delete any existing xunit reporter output (which we use to test our handling + // of cypress-multi-reporters), but ignore failures since the file likely doesn't + // exist. + await fs.unlink(xunitPath).catch(() => undefined); + }); + + afterEach(async () => { + // If the test ran successfully, it should have produced the XML output file. If it + // didn't, we didn't handle cypress-multi-reporters correctly, and we should fail the + // test. Note that this may make the afterEach() hook fail in the even that the test + // also fails, but that shouldn't cause any issues. + await fs.unlink(xunitPath).catch((e) => { + throw new Error( + `Failed to delete ${xunitPath}; did cypress-multi-reporters work as expected?`, + { cause: e } + ); + }); + }); + } + + project.configFiles.forEach((configFile) => { + describe(configFile, () => { + (["component", "e2e"] as TestMode[]).forEach((testMode) => { + describe(testMode, () => { + it.each(["ignore_failures", "skip_tests"] as QuarantineMode[])( + "defaults w/ quarantineMode = %s", + (quarantineMode, done) => + integrationTest( + { + params: { + config: + quarantineMode === "skip_tests" + ? { + quarantineMode, + } + : null, + configFile, + project: projectName as TestProjectName, + testMode, + expectQuarantinedTestsToBeSkipped: + quarantineMode === "skip_tests", + }, + summaryTotals: + quarantineMode === "skip_tests" + ? { + ...defaultSummaryTotals, + numPending: 4, + numQuarantined: 4, + } + : defaultSummaryTotals, + }, + done + ) + ); + }); + }); + }); + }); + } + ); + }); +}); diff --git a/packages/cypress-plugin/test/integration/src/basic.test.ts b/packages/cypress-plugin/test/integration/src/basic.test.ts new file mode 100644 index 0000000..7809d01 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/basic.test.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + integrationTestSuite, + integrationTest, + defaultSummaryTotals, +} from "./test-wrappers"; +import { QuarantineMode } from "@unflakable/plugins-common"; + +integrationTestSuite(() => { + it("quarantine flaky test", (done) => + integrationTest( + { + params: { + quarantineFlake: true, + }, + expectedExitCode: 6, + summaryTotals: { + ...defaultSummaryTotals, + numFlaky: 0, + numQuarantined: 5, + }, + }, + done + )); + + it("skip failures", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipFailures: true, + }, + expectedExitCode: 2, + summaryTotals: { + ...defaultSummaryTotals, + numFailing: 0, + numPassing: 5, + numPending: 9, + numSkipped: 0, + // No error thrown in invalid.cy.ts means Cypress doesn't create a fake test to represent + // the failure. + numTests: 19, + }, + }, + done + )); + + it.each(["ignore_failures", "skip_tests"] as QuarantineMode[])( + "run should succeed when skipping failures and quarantining flaky test w/ quarantineMode = %s", + (quarantineMode, done) => + integrationTest( + { + params: { + ...(quarantineMode === "skip_tests" + ? { config: { quarantineMode } } + : {}), + expectQuarantinedTestsToBeSkipped: quarantineMode === "skip_tests", + quarantineFlake: true, + skipBeforeHook: true, + skipFailures: true, + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 5, + numPending: quarantineMode === "skip_tests" ? 8 : 9, + // Quarantined tests skipped via explicit it.skip() are indistinguishable from + // quarantined tests skipped due to skip_tests (which uses it.skip()). + numQuarantined: quarantineMode === "skip_tests" ? 6 : 5, + numSkipped: 0, + // No error thrown in invalid.cy.ts means Cypress doesn't create a fake test to + // represent the failure. + numTests: 19, + }, + }, + done + ) + ); +}); diff --git a/packages/cypress-plugin/test/integration/src/config.test.ts b/packages/cypress-plugin/test/integration/src/config.test.ts new file mode 100644 index 0000000..c813e90 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/config.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { integrationTestSuite, integrationTest } from "./test-wrappers"; + +integrationTestSuite(() => { + it("set test suite ID via config", (done) => + integrationTest( + { + params: { + config: { + testSuiteId: "MOCK_SUITE_ID_CONFIG", + }, + envVars: { + UNFLAKABLE_SUITE_ID: undefined, + }, + expectedSuiteId: "MOCK_SUITE_ID_CONFIG", + }, + }, + done + )); + + it("set test suite ID via environment", (done) => + integrationTest( + { + params: { + config: { + testSuiteId: "MOCK_SUITE_ID_CONFIG", + }, + envVars: { + UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID_ENV", + }, + // Environment should take precedent over config. + expectedSuiteId: "MOCK_SUITE_ID_ENV", + }, + }, + done + )); + + it("set test suite ID via CLI", (done) => + integrationTest( + { + params: { + cliArgs: ["--test-suite-id", "MOCK_SUITE_ID_CLI"], + config: { + testSuiteId: "MOCK_SUITE_ID_CONFIG", + }, + envVars: { + UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID_ENV", + }, + // CLI should take precedent over environment and config. + expectedSuiteId: "MOCK_SUITE_ID_CLI", + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/disable-plugin.test.ts b/packages/cypress-plugin/test/integration/src/disable-plugin.test.ts new file mode 100644 index 0000000..463c9dc --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/disable-plugin.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; + +integrationTestSuite(() => { + const expectedExitCodeWithPluginDisabled = 11; + const summaryTotalsWithPluginDisabled = { + ...defaultSummaryTotals, + numQuarantined: 0, + numFlaky: 0, + numFailing: 11, + }; + + it("disable plugin via config", (done) => + integrationTest( + { + params: { + config: { + enabled: false, + }, + expectPluginToBeEnabled: false, + }, + expectedExitCode: expectedExitCodeWithPluginDisabled, + summaryTotals: summaryTotalsWithPluginDisabled, + }, + done + )); + + it("disable plugin via environment", (done) => + integrationTest( + { + params: { + envVars: { + UNFLAKABLE_ENABLED: "false", + }, + expectPluginToBeEnabled: false, + }, + expectedExitCode: expectedExitCodeWithPluginDisabled, + summaryTotals: summaryTotalsWithPluginDisabled, + }, + done + )); + + it("enable plugin via environment (override config)", (done) => + integrationTest( + { + params: { + config: { + enabled: false, + }, + envVars: { + UNFLAKABLE_ENABLED: "true", + }, + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/disable-upload.test.ts b/packages/cypress-plugin/test/integration/src/disable-upload.test.ts new file mode 100644 index 0000000..f9171e3 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/disable-upload.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { integrationTest, integrationTestSuite } from "./test-wrappers"; + +integrationTestSuite(() => { + it("disable upload via config", (done) => + integrationTest( + { + params: { + config: { + uploadResults: false, + }, + expectResultsToBeUploaded: false, + }, + }, + done + )); + + it("disable upload via environment", (done) => + integrationTest( + { + params: { + envVars: { + UNFLAKABLE_UPLOAD_RESULTS: "false", + }, + expectResultsToBeUploaded: false, + }, + }, + done + )); + + it("disable upload via CLI", (done) => + integrationTest( + { + params: { + cliArgs: ["--no-upload-results"], + envVars: { + // Make sure CLI arg overrides environment variable and config. + UNFLAKABLE_UPLOAD_RESULTS: "true", + }, + expectResultsToBeUploaded: false, + }, + }, + done + )); + + it("enable upload via environment (override config)", (done) => + integrationTest( + { + params: { + config: { + uploadResults: false, + }, + envVars: { + UNFLAKABLE_UPLOAD_RESULTS: "true", + }, + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/git.test.ts b/packages/cypress-plugin/test/integration/src/git.test.ts new file mode 100644 index 0000000..26290db --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/git.test.ts @@ -0,0 +1,119 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { integrationTest, integrationTestSuite } from "./test-wrappers"; +import path from "path"; + +integrationTestSuite(() => { + it("no git repo", (done) => + integrationTest( + { + params: { + expectedBranch: undefined, + expectedCommit: undefined, + // Without a repo, paths are relative to the Cypress project root. + expectedRepoRelativePathPrefix: "", + git: { + isRepo: false, + }, + }, + }, + done + )); + + // This tests the environment present in GitHub Actions for a `pull_request` event. + it("git repo with detached HEAD", (done) => + integrationTest( + { + params: { + git: { + abbreviatedRefs: { + // Mock a detached HEAD. + HEAD: "HEAD", + "refs/remote/pull/MOCK_PR_NUMBER/merge": + "pull/MOCK_PR_NUMBER/merge", + }, + commit: "MOCK_PR_COMMIT", + isRepo: true, + // Mock the `git show-ref` response. + refs: [ + { + sha: "MOCK_PR_COMMIT", + refName: "refs/remote/pull/MOCK_PR_NUMBER/merge", + }, + ], + repoRoot: path.resolve("../.."), + }, + expectedCommit: "MOCK_PR_COMMIT", + expectedBranch: "pull/MOCK_PR_NUMBER/merge", + }, + }, + done + )); + + it("read branch/commit from environment", (done) => + integrationTest( + { + params: { + envVars: { + UNFLAKABLE_BRANCH: "MOCK_BRANCH_ENV", + UNFLAKABLE_COMMIT: "MOCK_COMMIT_ENV", + }, + expectedBranch: "MOCK_BRANCH_ENV", + expectedCommit: "MOCK_COMMIT_ENV", + }, + }, + done + )); + + it("read branch/commit from CLI args", (done) => + integrationTest( + { + params: { + cliArgs: [ + "--branch", + "MOCK_BRANCH_CLI", + "--commit", + "MOCK_COMMIT_CLI", + ], + envVars: { + UNFLAKABLE_BRANCH: "MOCK_BRANCH_ENV", + UNFLAKABLE_COMMIT: "MOCK_COMMIT_ENV", + }, + // CLI should override environment variables. + expectedBranch: "MOCK_BRANCH_CLI", + expectedCommit: "MOCK_COMMIT_CLI", + }, + }, + done + )); + + it("disable git auto-detection via config", (done) => + integrationTest( + { + params: { + config: { + gitAutoDetect: false, + }, + expectedBranch: undefined, + expectedCommit: undefined, + // Without a repo, paths are relative to the Cypress project root. + expectedRepoRelativePathPrefix: "", + }, + }, + done + )); + + it("disable git auto-detection via CLI", (done) => + integrationTest( + { + params: { + cliArgs: ["--no-git-auto-detect"], + expectedBranch: undefined, + expectedCommit: undefined, + // Without a repo, paths are relative to the Cypress project root. + expectedRepoRelativePathPrefix: "", + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/hook-failures.test.ts b/packages/cypress-plugin/test/integration/src/hook-failures.test.ts new file mode 100644 index 0000000..224384e --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/hook-failures.test.ts @@ -0,0 +1,363 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { integrationTest, integrationTestSuite } from "./test-wrappers"; + +integrationTestSuite(() => { + it("run should succeed when before() fails both tests are quarantined", (done) => + integrationTest( + { + params: { + quarantineHookFail: true, + quarantineHookSkip: true, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 1, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("run should succeed when beforeEach() fails both tests are quarantined", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipBeforeEachHook: false, + quarantineHookFail: true, + quarantineHookSkip: true, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 1, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("run should succeed when afterEach() fails both tests are quarantined", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipAfterEachHook: false, + quarantineHookFail: true, + quarantineHookSkip: true, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 1, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("run should fail when non-quarantined test is skipped", (done) => + integrationTest( + { + params: { + quarantineHookFail: true, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 1, + summaryTotals: { + icon: "fail", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 1, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("failed beforeEach() hook", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipBeforeEachHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 1, + summaryTotals: { + icon: "fail", + numFailing: 1, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("failed afterEach() hook", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipAfterEachHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 1, + summaryTotals: { + icon: "fail", + numFailing: 1, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("failed after() hook", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipAfterHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 1, + summaryTotals: { + icon: "fail", + numFailing: 1, + numFlaky: 0, + numPassing: 1, + numPending: 0, + numQuarantined: 0, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); + + it("run should succeed with failed after() and quarantined test", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipAfterHook: false, + quarantineHookSkip: true, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 1, + numPending: 0, + numQuarantined: 1, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); + + it("failed beforeEach() and after() hooks", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipBeforeEachHook: false, + skipAfterHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 2, + summaryTotals: { + icon: "fail", + numFailing: 2, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); + + it("run should succeed when beforeEach() and after() hooks fail and tests are both quarantined", (done) => + integrationTest( + { + params: { + quarantineHookFail: true, + quarantineHookSkip: true, + skipBeforeHook: true, + skipBeforeEachHook: false, + skipAfterHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 2, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); + + it("failed afterEach() and after() hooks", (done) => + integrationTest( + { + params: { + skipBeforeHook: true, + skipAfterHook: false, + skipAfterEachHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 2, + summaryTotals: { + icon: "fail", + numFailing: 2, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); + + it("run should succeed when afterEach() and after() hooks fail and tests are both quarantined", (done) => + integrationTest( + { + params: { + quarantineHookFail: true, + quarantineHookSkip: true, + skipBeforeHook: true, + skipAfterHook: false, + skipAfterEachHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 2, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); + + it("multiple before() hook errors", (done) => + integrationTest( + { + params: { + multipleHookErrors: true, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 1, + summaryTotals: { + icon: "fail", + numFailing: 1, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("test and afterEach() hook errors", (done) => + integrationTest( + { + params: { + hookAndTestErrors: true, + skipBeforeHook: true, + skipAfterEachHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 1, + summaryTotals: { + icon: "fail", + numFailing: 1, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 1, + numTests: 2, + }, + }, + done + )); + + it("test and after() hook errors", (done) => + integrationTest( + { + params: { + hookAndTestErrors: true, + skipBeforeHook: true, + skipAfterHook: false, + specNameStubs: ["hook-fail"], + }, + expectedExitCode: 2, + summaryTotals: { + icon: "fail", + numFailing: 2, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 0, + numTests: 2, + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/long-names.test.ts b/packages/cypress-plugin/test/integration/src/long-names.test.ts new file mode 100644 index 0000000..1a10435 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/long-names.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; +import { QuarantineMode } from "@unflakable/plugins-common"; + +integrationTestSuite(() => { + it.each(["ignore_failures", "skip_tests"] as QuarantineMode[])( + "test names longer than 4096 chars should be truncated w/ quarantineMode = %s", + (quarantineMode, done) => + integrationTest( + { + params: { + config: + quarantineMode === "skip_tests" + ? { + quarantineMode, + } + : null, + expectedFlakeTestNameSuffix: "*".repeat(4096), + expectQuarantinedTestsToBeSkipped: quarantineMode === "skip_tests", + testEnvVars: { + FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096), + }, + }, + summaryTotals: + quarantineMode === "skip_tests" + ? { + ...defaultSummaryTotals, + numPending: 4, + numQuarantined: 4, + } + : defaultSummaryTotals, + }, + done + ) + ); + + it.each(["ignore_failures", "skip_tests"] as QuarantineMode[])( + "quarantining should work for tests with names longer than 4096 chars w/ quarantineMode = %s", + (quarantineMode, done) => + integrationTest( + { + params: { + config: + quarantineMode === "skip_tests" + ? { + quarantineMode, + } + : null, + expectedFlakeTestNameSuffix: "*".repeat(4096), + expectQuarantinedTestsToBeSkipped: quarantineMode === "skip_tests", + testEnvVars: { + FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096), + }, + quarantineFlake: true, + }, + expectedExitCode: 6, + summaryTotals: { + ...defaultSummaryTotals, + numFlaky: 0, + numPending: quarantineMode === "skip_tests" ? 4 : 5, + numQuarantined: quarantineMode === "skip_tests" ? 6 : 5, + }, + }, + done + ) + ); +}); diff --git a/packages/cypress-plugin/test/integration/src/matchers.ts b/packages/cypress-plugin/test/integration/src/matchers.ts new file mode 100644 index 0000000..065a17f --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/matchers.ts @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { expect } from "@jest/globals"; +import type { MatcherFunction } from "expect"; + +const toBeAnInteger: MatcherFunction = + // `floor` and `ceiling` get types from the line above + // it is recommended to type them as `unknown` and to validate the values + function (actual: unknown) { + if (typeof actual !== "number") { + return { + message: () => + `expected ${this.utils.printReceived(actual)} to be number`, + pass: false, + }; + } else if (Number.isInteger(actual)) { + return { + message: () => + `expected ${this.utils.printReceived(actual)} not to be an integer`, + pass: true, + }; + } else { + return { + message: () => + `expected ${this.utils.printReceived(actual)} to be an integer`, + pass: false, + }; + } + }; + +expect.extend({ + toBeAnInteger, +}); + +declare module "expect" { + interface AsymmetricMatchers { + // The @jest/global definitions return AsymmetricMatcher_2 here, but we use these in places + // the expect arrays/strings. + arrayContaining(sample: Array): T[]; + + stringContaining(sample: string): string; + + stringMatching(sample: string | RegExp): string; + + // This doesn't actually return a number, but places that use it are places where we expect + // numbers and would otherwise need an explicit cast. + toBeAnInteger(): number; + } + + interface Matchers { + toBeAnInteger(): R; + } +} diff --git a/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts b/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts new file mode 100644 index 0000000..6bd9b90 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; + +integrationTestSuite(() => { + it.each(["CLI", "config"] as ("CLI" | "config")[])( + "set quarantineMode to no_quarantine via %s", + (mode, done) => + integrationTest( + { + params: { + config: { + quarantineMode: "no_quarantine", + }, + ...(mode === "CLI" + ? { + cliArgs: ["--quarantine-mode", "no_quarantine"], + } + : { + config: { + quarantineMode: "no_quarantine", + }, + }), + expectQuarantinedTestsToBeQuarantined: false, + }, + expectedExitCode: 11, + summaryTotals: { + ...defaultSummaryTotals, + numQuarantined: 0, + numFailing: 8, + numFlaky: 3, + }, + }, + done + ) + ); +}); diff --git a/packages/cypress-plugin/test/integration/src/parse-output.ts b/packages/cypress-plugin/test/integration/src/parse-output.ts new file mode 100644 index 0000000..d8e31f9 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/parse-output.ts @@ -0,0 +1,1207 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +/* eslint-disable no-control-regex */ + +import { + specProjectPath, + TEST_SPEC_STUBS, + TestCaseParams, +} from "./run-test-case"; + +export type RunStarting = { + specs: string[]; +}; + +export type AttemptInfo = { + attemptNum: number; + totalAttempts: number; +}; + +export type NonPassingTestAttempt = { + titlePath: string[]; + attempt: AttemptInfo | undefined; + errorLines: string[]; +}; + +export type FailedTest = { + attempts: NonPassingTestAttempt[]; +}; + +export type SkippedTest = { + titlePath: string[]; + isQuarantined: boolean; +}; + +export type TestCountWithFailures = { + count: number; + tests: T[]; +}; + +export type ReporterOutput = { + suitesAndTestAttempts: string[]; + passing: number; + pending: number; + quarantinedPending: TestCountWithFailures; + failures: TestCountWithFailures; + quarantinedFailures: TestCountWithFailures; + flakes: TestCountWithFailures; + quarantinedFlakes: TestCountWithFailures; + skipped: TestCountWithFailures; +}; + +export const EMPTY_REPORTER_OUTPUT_MATCH: ReporterOutput = { + suitesAndTestAttempts: [], + passing: 0, + pending: 0, + quarantinedPending: { count: 0, tests: [] }, + failures: { count: 0, tests: [] }, + quarantinedFailures: { count: 0, tests: [] }, + flakes: { count: 0, tests: [] }, + quarantinedFlakes: { count: 0, tests: [] }, + skipped: { count: 0, tests: [] }, +}; + +export type SpecResults = { + color: "pass" | "fail"; + numTests: number; + numPassing: number; + numFailing: number; + numFlaky: number; + numQuarantined: number; + numPending: number; + numSkipped: number; +}; + +export const EMPTY_RESULTS: Omit = { + numTests: 0, + numPassing: 0, + numFailing: 0, + numFlaky: 0, + numQuarantined: 0, + numPending: 0, + numSkipped: 0, +}; + +export type SpecOutput = { + // This is relative to common root. Since all the tests are in the same directory, it's just the + // basename (e.g., pass.cy.ts). + filename: string; + reporterOutput: ReporterOutput; + results: SpecResults; +}; + +export type SummaryRow = { + icon: "pass" | "fail"; + specName: string; + numTests: number; + numPassing: number; + numFailing: number; + numFlaky: number; + numQuarantined: number; + numPending: number; + numSkipped: number; +}; + +export type SummaryTotals = { + icon: "pass" | "fail"; + numTests: number; + numPassing: number; + numFailing: number; + numFlaky: number; + numQuarantined: number; + numPending: number; + numSkipped: number; +}; + +export type Summary = { + rows: SummaryRow[]; + totals: SummaryTotals; +}; + +export type ParsedOutput = { + runStarting: RunStarting | null; + specOutputs: SpecOutput[]; + summary: Summary; + unflakableReportUrl: string | null; +}; + +const TABLE_TOP_BORDER_LINE = + // eslint-disable-next-line no-control-regex + /^\x1B\[90m +┌(\x1B\[39m\x1B\[90m─)+\x1B\[39m\x1B\[90m┐\x1B\[39m$/; +const TABLE_BETWEEN_ROWS_BORDER_LINE = + /^\x1B\[90m +├(?:\x1B\[39m\x1B\[90m─)+\x1B\[39m\x1B\[90m┤\x1B\[39m$/; +const TABLE_BOTTOM_BORDER_LINE = + // eslint-disable-next-line no-control-regex + /^\x1B\[90m +└(\x1B\[39m\x1B\[90m─)+\x1B\[39m\x1B\[90m┘\x1B\[39m$/; + +const parseKeyValueTableEntries = ( + lines: string[] +): { [key in string]: string } => + lines.reduce( + ({ entries, lastKey }, line) => { + const parsedLine = line.match( + // eslint-disable-next-line no-control-regex + /^\x1B\[90m +│\x1B\[39m(?: \x1B\[90m(.+):\x1B\[39m)? +(?:\x1B\[0m)?(.*?)(?:\x1B\[0m)? +\x1B\[90m│\x1B\[39m$/ + ); + expect( + parsedLine, + `invalid key-value table row ${JSON.stringify(line)}` + ).not.toBeNull(); + const key = (parsedLine as RegExpMatchArray)[1]; + const value = (parsedLine as RegExpMatchArray)[2]; + + // Long values can span multiple lines, so we concatenate them below. + if (key === undefined) { + expect(lastKey).toBeDefined(); + return { + entries: { + ...entries, + [lastKey as string]: entries[lastKey as string] + value, + }, + lastKey, + }; + } else { + // Disallow duplicate keys. + expect(Object.keys(entries)).not.toContain(key); + return { + entries: { + ...entries, + [key]: value, + }, + lastKey: key, + }; + } + }, + { + entries: {} as { [key in string]: string }, + lastKey: undefined as string | undefined, + } + ).entries; + +const parseRunStarting = ( + params: TestCaseParams, + stdoutLines: string[] +): { + linesRead: number; + runStarting: RunStarting; +} => { + const { specNameStubs, testMode } = params; + + const runStartingLine = stdoutLines.findIndex( + (line) => + line === "\x1B[0m (\x1B[4m\x1B[1mRun Starting\x1B[22m\x1B[24m)\x1B[0m" + ); + expect(runStartingLine).not.toBe(-1); + + expect(stdoutLines[runStartingLine + 2]).toMatch(TABLE_TOP_BORDER_LINE); + const numTableLines = stdoutLines + .slice(runStartingLine + 3) + .findIndex(TABLE_BOTTOM_BORDER_LINE.test.bind(TABLE_BOTTOM_BORDER_LINE)); + expect(numTableLines).not.toBe(-1); + + const tableEntries = parseKeyValueTableEntries( + stdoutLines.slice(runStartingLine + 3, runStartingLine + 3 + numTableLines) + ); + + expect(Object.keys(tableEntries)).toStrictEqual([ + "Cypress", + "Browser", + "Node Version", + "Specs", + "Searched", + ]); + + expect(tableEntries["Cypress"]).toMatch(/^[0-9]+\.[0-9]+\.[0-9]+$/); + expect(tableEntries["Browser"]).toMatch( + /^Chrome [0-9]+ \x1B\[90m\(headless\)\x1B\[39m$/ + ); + expect(tableEntries["Node Version"]).toMatch( + new RegExp( + `^${process.version.replace( + // Replace all occurrences. + /\./g, + "\\." + )} \\x1B\\[90m\\(.+\\)\\x1B\\[39m$` + ) + ); + expect(tableEntries["Searched"]).toBe( + specNameStubs !== undefined && specNameStubs.length > 0 + ? specNameStubs.map((stub) => specProjectPath(params, stub)).join(", ") + : testMode === "component" + ? "**/*.cy.{js,jsx,ts,tsx}" + : `cypress/e2e/**/*.cy.{js,jsx,ts,tsx}` + ); + + const parsedSpecsLine = tableEntries["Specs"].match( + /^([0-9]+) found \((.*)\)$/ + ); + expect(parsedSpecsLine).not.toBeNull(); + const numSpecs = Number.parseInt((parsedSpecsLine as RegExpMatchArray)[1]); + const specs = (parsedSpecsLine as RegExpMatchArray)[2].split(", "); + expect(specs).toHaveLength(numSpecs); + + return { + linesRead: runStartingLine + 3 + numTableLines + 2, + runStarting: { + specs, + }, + }; +}; + +const REPORTER_SPEC_OUTPUT_RUNNING_LINE = + /^ *Running: +\x1B\[90m(.+)\x1B\[39m +\x1B\[90m\(([0-9]+) of ([0-9]+)\)\x1B\[39m$/; +const REPORTER_SPEC_OUTPUT_TESTS_PASSING_LINE = + /^\x1B\[92m \x1B\[0m\x1B\[32m ([0-9]+) passing\x1B\[0m\x1B\[90m \([0-9]+.+?\)\x1B\[0m$/; +const REPORTER_SPEC_OUTPUT_TESTS_PENDING_LINE = + /^\x1B\[36m {2}([0-9]+) pending\x1B\[0m$/; +const REPORTER_SPEC_OUTPUT_TESTS_QUARANTINED_PENDING_LINE = + /^\x1B\[35m {2}([0-9]+) quarantined pending\x1B\[39m$/; +const REPORTER_SPEC_OUTPUT_TESTS_FAILING_LINE = + /^\x1B\[31m {2}([0-9]+) failing\x1B\[0m$/; +const REPORTER_SPEC_OUTPUT_TESTS_QUARANTINED_FAILING_LINE = + /^\x1B\[35m {2}([0-9]+) quarantined failing\x1B\[39m$/; +const REPORTER_SPEC_OUTPUT_TESTS_FLAKY_LINE = + /^\x1B\[33m {2}([0-9]+) flaky\x1B\[0m$/; +const REPORTER_SPEC_OUTPUT_TESTS_QUARANTINED_FLAKY_LINE = + /^\x1B\[35m {2}([0-9]+) quarantined flaky\x1B\[39m$/; +const REPORTER_SPEC_OUTPUT_TESTS_SKIPPED_LINE = + /^\x1B\[34m {2}([0-9]+) skipped\x1B\[39m$/; +const REPORTER_SPEC_OUTPUT_TESTS_LINE = new RegExp( + [ + REPORTER_SPEC_OUTPUT_TESTS_PASSING_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_PENDING_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_QUARANTINED_PENDING_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_FAILING_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_QUARANTINED_FAILING_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_FLAKY_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_QUARANTINED_FLAKY_LINE.source, + REPORTER_SPEC_OUTPUT_TESTS_SKIPPED_LINE.source, + ].join("|") +); + +const REPORTER_NON_PASSING_TEST_LINE = + /^(\x1B\[0m)? +(?:([0-9]+)\) )?(.+?)(?:\x1B\[0m\x1B\[33m \(attempt ([0-9]+) of ([0-9]+)\))?(?:\x1B\[0m){0,2}(:)?$/; +const REPORTER_SKIPPED_TEST_LINE = + /^ +(?:([0-9]+)\) )?(.+?)( \x1B\[35m\[quarantined]\x1B\[39m)?$/; + +const PLUGIN_SPEC_RESULTS_LINE = + /^\x1B\[3([12])m {2}\(\x1B\[4m\x1B\[1mResults\x1B\[22m\x1B\[24m\)\x1B\[39m$/; + +const PLUGIN_SPEC_RESULTS_TABLE_VALUE = /^\x1B\[3([12])m(.*)\x1B\[39m$/; + +// We expect the output to be in a specific order, which we track with these finite states in +// increasing order. +enum ReporterTestsParseOuterState { + Init = 0, + Passing = 1, + Pending = 2, + QuarantinedPending = 3, + Failing = 4, + QuarantinedFailing = 5, + Flaky = 6, + QuarantinedFlaky = 7, + Skipped = 8, +} + +enum ReporterTestsParseInnerState { + Init = 0, + Title = 1, + Errors = 2, +} + +type ReporterTestsParser = { + innerState: ReporterTestsParseInnerState; + outerState: ReporterTestsParseOuterState; + results: Omit; +}; + +const mergeOutputTestsLine = ( + parseState: ReporterTestsParser, + nextState: ReporterTestsParseOuterState, + resultsUpdate: Partial> +): ReporterTestsParser => { + expect(parseState.outerState).toBeLessThan(nextState); + return { + ...parseState, + results: { + ...parseState.results, + ...resultsUpdate, + }, + outerState: nextState, + innerState: ReporterTestsParseInnerState.Init, + }; +}; + +const nonPassingStateToResultsField = ( + outerState: ReporterTestsParseOuterState +): + | "quarantinedPending" + | "failures" + | "quarantinedFailures" + | "flakes" + | "quarantinedFlakes" => { + switch (outerState) { + case ReporterTestsParseOuterState.Init: + case ReporterTestsParseOuterState.Passing: + case ReporterTestsParseOuterState.Pending: + case ReporterTestsParseOuterState.Skipped: + throw new Error( + `State ${outerState} should not have non-passing tests reported` + ); + case ReporterTestsParseOuterState.QuarantinedPending: + return "quarantinedPending"; + case ReporterTestsParseOuterState.Failing: + return "failures"; + case ReporterTestsParseOuterState.QuarantinedFailing: + return "quarantinedFailures"; + case ReporterTestsParseOuterState.Flaky: + return "flakes"; + case ReporterTestsParseOuterState.QuarantinedFlaky: + return "quarantinedFlakes"; + } +}; + +const withUpdatedLastElement = (arr: T[], updateFn: (elem: T) => T): T[] => [ + ...arr.slice(0, arr.length - 1), + updateFn(arr[arr.length - 1]), +]; + +const parseSpecOutputs = ( + params: TestCaseParams, + stdoutLinesAfterRunStarting: string[] +): { + linesRead: number; + specOutputs: SpecOutput[]; +} => { + const { specOffsets } = stdoutLinesAfterRunStarting.reduce( + ({ specOffsets }, line, lineIndex) => + REPORTER_SPEC_OUTPUT_RUNNING_LINE.test(line) + ? { specOffsets: [...specOffsets, lineIndex] } + : { specOffsets }, + { + specOffsets: [] as number[], + } + ); + + const specOutputs = specOffsets.map( + ( + specOffset, + specIndex + ): SpecOutput & { + lastLineRead: number; + } => { + const parsedRunning = stdoutLinesAfterRunStarting[specOffset].match( + REPORTER_SPEC_OUTPUT_RUNNING_LINE + ); + expect(parsedRunning).not.toBeNull(); + const filename = (parsedRunning as RegExpMatchArray)[1]; + try { + const specNumber = (parsedRunning as RegExpMatchArray)[2]; + const totalSpecs = (parsedRunning as RegExpMatchArray)[3]; + + expect(specNumber).toBe((specIndex + 1).toString()); + expect(totalSpecs).toBe( + params.specNameStubs !== undefined && params.specNameStubs.length > 0 + ? params.specNameStubs.length.toString() + : TEST_SPEC_STUBS.length.toString() + ); + + const stdoutLinesAfterRunning = stdoutLinesAfterRunStarting.slice( + specOffset + 2 + ); + + // NB: For component tests, webpack emits 3 lines with the number of assets, modules, and a + // summary of the compilation result. Sometimes these lines are printed above the Running + // line, and sometimes below. This seems to be non-deterministic. + + // The reporter prints a blank line with the suite chalk color code for the root suite + // (which has an empty title). This will only get printed if at least one test exists in + // the spec. + const rootSuiteLine = stdoutLinesAfterRunning.findIndex( + (line) => line === "\x1B[0m\x1B[0m" + ); + + const resultsLineIndex = stdoutLinesAfterRunning.findIndex( + PLUGIN_SPEC_RESULTS_LINE.test.bind(PLUGIN_SPEC_RESULTS_LINE) + ); + expect(resultsLineIndex).not.toBe(-1); + + const testsPassingLine = stdoutLinesAfterRunning.findIndex( + REPORTER_SPEC_OUTPUT_TESTS_PASSING_LINE.test.bind( + REPORTER_SPEC_OUTPUT_TESTS_PASSING_LINE + ) + ); + expect(testsPassingLine).not.toBe(-1); + + // NB: The spec may not have any suites or tests. + const suitesAndTestAttempts = + rootSuiteLine !== -1 && rootSuiteLine < resultsLineIndex + ? stdoutLinesAfterRunning + .slice(rootSuiteLine + 1, testsPassingLine) + .filter((line) => line !== "") + : []; + + const { results: reporterResults } = stdoutLinesAfterRunning + .slice(testsPassingLine, resultsLineIndex) + .reduce( + (parseState, line) => { + // Skip blank lines. + if (["", "\x1B[0m"].includes(line)) { + return parseState; + } + + const parsedTestsLine = line.match( + REPORTER_SPEC_OUTPUT_TESTS_LINE + ); + if (parsedTestsLine !== null) { + if (parsedTestsLine[1] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.Passing, + { passing: parseInt(parsedTestsLine[1]) } + ); + } else if (parsedTestsLine[2] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.Pending, + { pending: parseInt(parsedTestsLine[2]) } + ); + } else if (parsedTestsLine[3] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.QuarantinedPending, + { + quarantinedPending: { + count: parseInt(parsedTestsLine[3]), + tests: [], + }, + } + ); + } else if (parsedTestsLine[4] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.Failing, + { + failures: { + count: parseInt(parsedTestsLine[4]), + tests: [], + }, + } + ); + } else if (parsedTestsLine[5] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.QuarantinedFailing, + { + quarantinedFailures: { + count: parseInt(parsedTestsLine[5]), + tests: [], + }, + } + ); + } else if (parsedTestsLine[6] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.Flaky, + { + flakes: { + count: parseInt(parsedTestsLine[6]), + tests: [], + }, + } + ); + } else if (parsedTestsLine[7] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.QuarantinedFlaky, + { + quarantinedFlakes: { + count: parseInt(parsedTestsLine[7]), + tests: [], + }, + } + ); + } else if (parsedTestsLine[8] !== undefined) { + return mergeOutputTestsLine( + parseState, + ReporterTestsParseOuterState.Skipped, + { + skipped: { + count: parseInt(parsedTestsLine[8]), + tests: [], + }, + } + ); + } + } + + if ( + parseState.outerState === ReporterTestsParseOuterState.Skipped + ) { + const parsedSkippedTestLineOrNull = line.match( + REPORTER_SKIPPED_TEST_LINE + ); + expect(parsedSkippedTestLineOrNull).not.toBeNull(); + const parsedSkippedTestLine = + parsedSkippedTestLineOrNull as RegExpMatchArray; + + if (parsedSkippedTestLine[1] !== undefined) { + // Start of new test + return { + ...parseState, + results: { + ...parseState.results, + skipped: { + ...parseState.results.skipped, + tests: [ + ...parseState.results.skipped.tests, + { + titlePath: [parsedSkippedTestLine[2]], + isQuarantined: + parsedSkippedTestLine[3] !== undefined, + }, + ], + }, + }, + innerState: ReporterTestsParseInnerState.Title, + }; + } else { + expect(parseState.innerState).toBe( + ReporterTestsParseInnerState.Title + ); + // Continuation of current test title path. + return { + ...parseState, + results: { + ...parseState.results, + skipped: { + ...parseState.results.skipped, + tests: withUpdatedLastElement( + parseState.results.skipped.tests, + (test) => ({ + titlePath: [ + ...test.titlePath, + parsedSkippedTestLine[2], + ], + isQuarantined: + test.isQuarantined || + parsedSkippedTestLine[3] !== undefined, + }) + ), + }, + }, + innerState: ReporterTestsParseInnerState.Title, + }; + } + } else { + const fieldToUpdate = nonPassingStateToResultsField( + parseState.outerState + ); + + const parsedNonPassingTestLine = line.match( + REPORTER_NON_PASSING_TEST_LINE + ); + if ( + parsedNonPassingTestLine !== null && + (parsedNonPassingTestLine[1] !== undefined || + parseState.innerState !== + ReporterTestsParseInnerState.Errors) + ) { + const hasErrors = parsedNonPassingTestLine[6] !== undefined; + const attempt: AttemptInfo | undefined = + parsedNonPassingTestLine[4] !== undefined && + parsedNonPassingTestLine[5] !== undefined + ? { + attemptNum: Number.parseInt( + parsedNonPassingTestLine[4] + ), + totalAttempts: Number.parseInt( + parsedNonPassingTestLine[5] + ), + } + : undefined; + + const newInnerState = hasErrors + ? ReporterTestsParseInnerState.Errors + : ReporterTestsParseInnerState.Title; + if (parsedNonPassingTestLine[2] !== undefined) { + // Start of new test + return { + ...parseState, + results: { + ...parseState.results, + [fieldToUpdate]: { + ...parseState.results[fieldToUpdate], + tests: [ + ...parseState.results[fieldToUpdate].tests, + { + attempts: [ + { + titlePath: [parsedNonPassingTestLine[3]], + attempt, + errorLines: [], + }, + ], + }, + ], + }, + }, + innerState: newInnerState, + }; + } else if ( + parseState.innerState === ReporterTestsParseInnerState.Title + ) { + // Continuation of current test title path. + return { + ...parseState, + results: { + ...parseState.results, + [fieldToUpdate]: { + ...parseState.results[fieldToUpdate], + tests: withUpdatedLastElement( + parseState.results[fieldToUpdate].tests, + (test) => ({ + attempts: withUpdatedLastElement( + test.attempts, + (testAttempt) => ({ + titlePath: [ + ...testAttempt.titlePath, + parsedNonPassingTestLine[3], + ], + attempt, + errorLines: [], + }) + ), + }) + ), + }, + }, + innerState: newInnerState, + }; + } else { + // Start of new attempt within current test. + expect(parseState.innerState).toBe( + ReporterTestsParseInnerState.Errors + ); + return { + ...parseState, + results: { + ...parseState.results, + [fieldToUpdate]: { + ...parseState.results[fieldToUpdate], + tests: withUpdatedLastElement( + parseState.results[fieldToUpdate].tests, + (test) => ({ + attempts: [ + ...test.attempts, + { + titlePath: [parsedNonPassingTestLine[3]], + attempt, + errorLines: [], + }, + ], + }) + ), + }, + }, + innerState: newInnerState, + }; + } + } else { + // Next error line. + expect([ + ReporterTestsParseInnerState.Errors, + ReporterTestsParseInnerState.Title, + ]).toContainEqual(parseState.innerState); + + return { + ...parseState, + results: { + ...parseState.results, + [fieldToUpdate]: { + ...parseState.results[fieldToUpdate], + tests: withUpdatedLastElement( + parseState.results[fieldToUpdate].tests, + (test) => ({ + attempts: withUpdatedLastElement( + test.attempts, + (testAttempt) => ({ + ...testAttempt, + errorLines: [ + ...testAttempt.errorLines, + line.trim(), + ], + }) + ), + }) + ), + }, + }, + }; + } + } + }, + { + results: { + passing: 0, + pending: 0, + quarantinedPending: { count: 0, tests: [] }, + failures: { count: 0, tests: [] }, + quarantinedFailures: { count: 0, tests: [] }, + flakes: { count: 0, tests: [] }, + quarantinedFlakes: { count: 0, tests: [] }, + skipped: { count: 0, tests: [] }, + }, + innerState: ReporterTestsParseInnerState.Init, + outerState: ReporterTestsParseOuterState.Init, + } + ); + + const resultsLine = stdoutLinesAfterRunning[resultsLineIndex]; + const parsedResultsLine = resultsLine.match(PLUGIN_SPEC_RESULTS_LINE); + expect(parsedResultsLine).not.toBeNull(); + const resultsColor = + (parsedResultsLine as RegExpMatchArray)[1] === "2" ? "pass" : "fail"; + + expect(stdoutLinesAfterRunning[resultsLineIndex + 2]).toMatch( + TABLE_TOP_BORDER_LINE + ); + const numTableLines = stdoutLinesAfterRunning + .slice(resultsLineIndex + 3) + .findIndex( + TABLE_BOTTOM_BORDER_LINE.test.bind(TABLE_BOTTOM_BORDER_LINE) + ); + expect(numTableLines).not.toBe(-1); + + const tableEntries = Object.fromEntries( + Object.entries( + parseKeyValueTableEntries( + stdoutLinesAfterRunning.slice( + resultsLineIndex + 3, + resultsLineIndex + 3 + numTableLines + ) + ) + ).map(([key, value]) => { + // Strip chalk formatting applied to every value. + const valueInner = value.match(PLUGIN_SPEC_RESULTS_TABLE_VALUE); + expect( + valueInner, + `Value ${JSON.stringify( + value + )} for table key \`${key}\` should match /${ + PLUGIN_SPEC_RESULTS_TABLE_VALUE.source + }/` + ).not.toBeNull(); + return [ + key, + { + color: + (valueInner as RegExpMatchArray)[1] === "1" ? "red" : "green", + value: (valueInner as RegExpMatchArray)[2], + }, + ]; + }) + ); + + expect(tableEntries["Tests"]?.value).toMatch(/^[0-9]+$/); + const resultColor = tableEntries["Tests"].color; + + expect(tableEntries["Passing"]?.value).toMatch(/^[0-9]+$/); + expect(tableEntries["Passing"].color).toBe(resultColor); + + expect(tableEntries["Failing"]?.value).toMatch(/^[0-9]+$/); + expect(tableEntries["Failing"].color).toBe(resultColor); + + if ("Flaky" in tableEntries) { + expect(tableEntries["Flaky"].value).toMatch(/^[0-9]+$/); + expect(tableEntries["Flaky"].color).toBe(resultColor); + } + + if ("Quarantined" in tableEntries) { + expect(tableEntries["Quarantined"].value).toMatch(/^[0-9]+$/); + expect(tableEntries["Quarantined"].color).toBe(resultColor); + } + + expect(tableEntries["Pending"]?.value).toMatch(/^[0-9]+$/); + expect(tableEntries["Pending"].color).toBe(resultColor); + + expect(tableEntries["Skipped"]?.value).toMatch(/^[0-9]+$/); + expect(tableEntries["Skipped"].color).toBe(resultColor); + + expect(tableEntries["Screenshots"]?.value).toMatch(/^[0-9]+$/); + expect(tableEntries["Screenshots"].color).toBe(resultColor); + + expect(tableEntries["Video"]?.value).toMatch(/^true|false$/); + expect(tableEntries["Video"].color).toBe(resultColor); + + expect(tableEntries["Spec Ran"]).toBeDefined(); + expect(tableEntries["Spec Ran"].color).toBe(resultColor); + const parsedSpecRan = tableEntries["Spec Ran"].value.match( + /^\x1B\[3([12])m(.*)\x1B\[39m\x1B\[3([12])m$/ + ); + expect(parsedSpecRan).not.toBeNull(); + expect((parsedSpecRan as RegExpMatchArray)[1]).toBe( + resultColor === "red" ? "1" : "2" + ); + expect((parsedSpecRan as RegExpMatchArray)[2]).toBe(filename); + expect((parsedSpecRan as RegExpMatchArray)[3]).toBe( + resultColor === "red" ? "1" : "2" + ); + + Object.keys(tableEntries).forEach((key) => + expect([ + "Tests", + "Passing", + "Failing", + "Flaky", + "Quarantined", + "Pending", + "Skipped", + "Screenshots", + "Video", + // Hard to check duration value since we can't easily mock Cypress's clock. + "Duration", + "Spec Ran", + ]).toContain(key) + ); + + return { + filename, + reporterOutput: { + suitesAndTestAttempts, + ...reporterResults, + }, + results: { + color: resultsColor, + numTests: Number.parseInt(tableEntries["Tests"].value), + numPassing: Number.parseInt(tableEntries["Passing"].value), + numFailing: Number.parseInt(tableEntries["Failing"].value), + numFlaky: + "Flaky" in tableEntries + ? Number.parseInt(tableEntries["Flaky"].value) + : 0, + numQuarantined: + "Quarantined" in tableEntries + ? Number.parseInt(tableEntries["Quarantined"].value) + : 0, + numPending: Number.parseInt(tableEntries["Pending"].value), + numSkipped: Number.parseInt(tableEntries["Skipped"].value), + }, + lastLineRead: + specOffset + 2 + resultsLineIndex + 3 + numTableLines + 1, + }; + } catch (e) { + throw new Error(`failed to parse spec output for \`${filename}\``, { + cause: e, + }); + } + } + ); + + return { + linesRead: + specOutputs.length > 0 + ? specOutputs[specOutputs.length - 1].lastLineRead + : 0, + specOutputs: specOutputs.map((specOutput) => ({ + filename: specOutput.filename, + reporterOutput: specOutput.reporterOutput, + results: specOutput.results, + })), + }; +}; + +const PLUGIN_SUMMARY_TABLE_HEADER_LINE = + /^\x1B\[90m +\x1B\[39m +\x1B\[90mSpec\x1B\[39m +\x1B\[90mTests\x1B\[39m +\x1B\[90mPassing\x1B\[39m +\x1B\[90mFailing\x1B\[39m +\x1B\[90mFlaky\x1B\[39m +\x1B\[90mQuar\.\x1B\[39m +\x1B\[90mPending\x1B\[39m +\x1B\[90mSkipped\x1B\[39m \x1B\[90m \x1B\[39m$/; + +const RUN_FINISHED_LINE = + "\x1B[0m (\x1B[4m\x1B[1mRun Finished\x1B[22m\x1B[24m)\x1B[0m"; + +const parseSummaryTable = ( + _params: TestCaseParams, + stdoutLinesAfterLastSpecTable: string[] +): Summary => { + const runFinishedLine = stdoutLinesAfterLastSpecTable.findIndex( + (line) => line === RUN_FINISHED_LINE + ); + expect(runFinishedLine).not.toBe(-1); + + expect(stdoutLinesAfterLastSpecTable[runFinishedLine + 3]).toMatch( + PLUGIN_SUMMARY_TABLE_HEADER_LINE + ); + expect(stdoutLinesAfterLastSpecTable[runFinishedLine + 4]).toMatch( + TABLE_TOP_BORDER_LINE + ); + const numTableLines = stdoutLinesAfterLastSpecTable + .slice(runFinishedLine + 5) + .findIndex(TABLE_BOTTOM_BORDER_LINE.test.bind(TABLE_BOTTOM_BORDER_LINE)); + expect(numTableLines).not.toBe(-1); + + const summaryRows = stdoutLinesAfterLastSpecTable + .slice(runFinishedLine + 5, runFinishedLine + 5 + numTableLines) + .filter((line) => !TABLE_BETWEEN_ROWS_BORDER_LINE.test(line)) + .map((line): SummaryRow => { + const parsedRowOrNull = line.match( + /^\x1B\[90m +│\x1B\[39m \x1B\[3(?:2m(?✔)|1m(?✖))\x1B\[39m +\x1B\[0m(?.+?)\x1B\[0m +\x1B\[90m(?.+?)\x1B\[39m +\x1B\[(?:0m(?[0-9]+)\x1B\[0m|90m-\x1B\[39m) +\x1B\[(?:32m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:31m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:33m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:35m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:36m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:34m(?[0-9]+)|90m-)\x1B\[39m \x1B\[90m│\x1B\[39m$/ + ); + + expect( + parsedRowOrNull, + `invalid summary table row ${JSON.stringify(line)}` + ).not.toBeNull(); + const parsedRow = parsedRowOrNull as RegExpMatchArray; + + return { + icon: parsedRow.groups?.iconPass !== undefined ? "pass" : "fail", + specName: parsedRow.groups?.spec as string, + numTests: + parsedRow.groups?.tests !== undefined + ? Number.parseInt(parsedRow.groups.tests) + : 0, + numPassing: + parsedRow.groups?.passes !== undefined + ? Number.parseInt(parsedRow.groups.passes) + : 0, + numFailing: + parsedRow.groups?.failures !== undefined + ? Number.parseInt(parsedRow.groups.failures) + : 0, + numFlaky: + parsedRow.groups?.unquarantinedFlakes !== undefined + ? Number.parseInt(parsedRow.groups.unquarantinedFlakes) + : 0, + numQuarantined: + parsedRow.groups?.quarantined !== undefined + ? Number.parseInt(parsedRow.groups.quarantined) + : 0, + numPending: + parsedRow.groups?.unquarantinedPending !== undefined + ? Number.parseInt(parsedRow.groups.unquarantinedPending) + : 0, + numSkipped: + parsedRow.groups?.skipped !== undefined + ? Number.parseInt(parsedRow.groups.skipped) + : 0, + }; + }) + .sort((a, b) => + a.specName < b.specName ? -1 : a.specName > b.specName ? 1 : 0 + ); + + const summaryTotalsLine = + stdoutLinesAfterLastSpecTable[runFinishedLine + 5 + numTableLines + 1]; + const parsedSummaryTotalsOrNull = summaryTotalsLine.match( + /^\x1B\[90m +\x1B\[39m \x1B\[3(?:2m(?✔)|1m(?✖))\x1B\[39m +\x1B\[3(:?2mAll specs passed!|1m(?[0-9]+) of (?[0-9]+) failed \([0-9]+%\))\x1B\[39m +\x1B\[90m(?.+?)\x1B\[39m +\x1B\[(?:0m(?[0-9]+)|90m-)\x1B\[0m +\x1B\[(?:32m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:31m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:33m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:35m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:36m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:34m(?[0-9]+)|90m-)\x1B\[39m \x1B\[90m \x1B\[39m$/ + ); + expect( + parsedSummaryTotalsOrNull, + `invalid summary table totals ${JSON.stringify(summaryTotalsLine)}` + ).not.toBeNull(); + const parsedSummaryTotals = parsedSummaryTotalsOrNull as RegExpMatchArray; + + const totals: SummaryTotals = { + icon: parsedSummaryTotals.groups?.iconPass !== undefined ? "pass" : "fail", + numTests: + parsedSummaryTotals.groups?.tests !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.tests) + : 0, + numPassing: + parsedSummaryTotals.groups?.passes !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.passes) + : 0, + numFailing: + parsedSummaryTotals.groups?.failures !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.failures) + : 0, + numFlaky: + parsedSummaryTotals.groups?.unquarantinedFlakes !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.unquarantinedFlakes) + : 0, + numQuarantined: + parsedSummaryTotals.groups?.quarantined !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.quarantined) + : 0, + numPending: + parsedSummaryTotals.groups?.unquarantinedPending !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.unquarantinedPending) + : 0, + numSkipped: + parsedSummaryTotals.groups?.skipped !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.skipped) + : 0, + }; + + expect( + parsedSummaryTotals.groups?.descFailed === undefined ? "pass" : "fail" + ).toBe(totals.icon); + if (parsedSummaryTotals.groups?.descFailed !== undefined) { + expect(parsedSummaryTotals.groups.descFailed).toBe( + summaryRows.filter((row) => row.icon === "fail").length.toString() + ); + expect(parsedSummaryTotals.groups.descTotal).toBe( + summaryRows.length.toString() + ); + } + + return { + rows: summaryRows, + totals, + }; +}; + +const parsePluginDisabledSummaryTable = ( + _params: TestCaseParams, + stdoutLinesAfterLastSpecTable: string[] +): Summary => { + const runFinishedLine = stdoutLinesAfterLastSpecTable.findIndex( + (line) => line === RUN_FINISHED_LINE + ); + expect(runFinishedLine).not.toBe(-1); + + expect(stdoutLinesAfterLastSpecTable[runFinishedLine + 3]).toMatch( + /^\x1B\[90m +\x1B\[39m +\x1B\[90mSpec\x1B\[39m +\x1B\[90mTests\x1B\[39m +\x1B\[90mPassing\x1B\[39m +\x1B\[90mFailing\x1B\[39m +\x1B\[90mPending\x1B\[39m +\x1B\[90mSkipped\x1B\[39m \x1B\[90m \x1B\[39m$/ + ); + expect(stdoutLinesAfterLastSpecTable[runFinishedLine + 4]).toMatch( + TABLE_TOP_BORDER_LINE + ); + const numTableLines = stdoutLinesAfterLastSpecTable + .slice(runFinishedLine + 5) + .findIndex(TABLE_BOTTOM_BORDER_LINE.test.bind(TABLE_BOTTOM_BORDER_LINE)); + expect(numTableLines).not.toBe(-1); + + const summaryRows = stdoutLinesAfterLastSpecTable + .slice(runFinishedLine + 5, runFinishedLine + 5 + numTableLines) + .filter((line) => !TABLE_BETWEEN_ROWS_BORDER_LINE.test(line)) + .map((line): SummaryRow => { + const parsedRowOrNull = line.match( + /^\x1B\[90m +│\x1B\[39m \x1B\[3(?:2m(?✔)|1m(?✖))\x1B\[39m +\x1B\[0m(?.+?)\x1B\[0m +\x1B\[90m(?.+?)\x1B\[39m +\x1B\[(?:0m(?[0-9]+)\x1B\[0m|90m-\x1B\[39m) +\x1B\[(?:32m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:31m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:36m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:34m(?[0-9]+)|90m-)\x1B\[39m \x1B\[90m│\x1B\[39m$/ + ); + + expect( + parsedRowOrNull, + `invalid summary table row ${JSON.stringify(line)}` + ).not.toBeNull(); + const parsedRow = parsedRowOrNull as RegExpMatchArray; + + return { + icon: parsedRow.groups?.iconPass !== undefined ? "pass" : "fail", + specName: parsedRow.groups?.spec as string, + numTests: + parsedRow.groups?.tests !== undefined + ? Number.parseInt(parsedRow.groups.tests) + : 0, + numPassing: + parsedRow.groups?.passes !== undefined + ? Number.parseInt(parsedRow.groups.passes) + : 0, + numFailing: + parsedRow.groups?.failures !== undefined + ? Number.parseInt(parsedRow.groups.failures) + : 0, + numFlaky: 0, + numQuarantined: 0, + numPending: + parsedRow.groups?.pending !== undefined + ? Number.parseInt(parsedRow.groups.pending) + : 0, + numSkipped: + parsedRow.groups?.skipped !== undefined + ? Number.parseInt(parsedRow.groups.skipped) + : 0, + }; + }) + .sort((a, b) => + a.specName < b.specName ? -1 : a.specName > b.specName ? 1 : 0 + ); + const summaryTotalsLine = + stdoutLinesAfterLastSpecTable[runFinishedLine + 5 + numTableLines + 1]; + const parsedSummaryTotalsOrNull = summaryTotalsLine.match( + /^\x1B\[90m +\x1B\[39m \x1B\[3(?:2m(?✔)|1m(?✖))\x1B\[39m +\x1B\[3(:?2mAll specs passed!|1m(?[0-9]+) of (?[0-9]+) failed \([0-9]+%\))\x1B\[39m +\x1B\[90m(?.+?)\x1B\[39m +\x1B\[(?:0m(?[0-9]+)|90m-)\x1B\[0m +\x1B\[(?:32m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:31m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:36m(?[0-9]+)|90m-)\x1B\[39m +\x1B\[(?:34m(?[0-9]+)|90m-)\x1B\[39m \x1B\[90m \x1B\[39m$/ + ); + expect( + parsedSummaryTotalsOrNull, + `invalid summary table totals ${JSON.stringify(summaryTotalsLine)}` + ).not.toBeNull(); + const parsedSummaryTotals = parsedSummaryTotalsOrNull as RegExpMatchArray; + + const totals: SummaryTotals = { + icon: parsedSummaryTotals.groups?.iconPass !== undefined ? "pass" : "fail", + numTests: + parsedSummaryTotals.groups?.tests !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.tests) + : 0, + numPassing: + parsedSummaryTotals.groups?.passes !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.passes) + : 0, + numFailing: + parsedSummaryTotals.groups?.failures !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.failures) + : 0, + numFlaky: 0, + numQuarantined: 0, + numPending: + parsedSummaryTotals.groups?.pending !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.pending) + : 0, + numSkipped: + parsedSummaryTotals.groups?.skipped !== undefined + ? Number.parseInt(parsedSummaryTotals.groups.skipped) + : 0, + }; + + expect( + parsedSummaryTotals.groups?.descFailed === undefined ? "pass" : "fail" + ).toBe(totals.icon); + if (parsedSummaryTotals.groups?.descFailed !== undefined) { + expect(parsedSummaryTotals.groups.descFailed).toBe( + summaryRows.filter((row) => row.icon === "fail").length.toString() + ); + expect(parsedSummaryTotals.groups.descTotal).toBe( + summaryRows.length.toString() + ); + } + + return { + rows: summaryRows, + totals, + }; +}; + +export const parseOutput = ( + params: TestCaseParams, + stdoutLines: string[] +): ParsedOutput => { + const { expectPluginToBeEnabled } = params; + + if (expectPluginToBeEnabled) { + const { linesRead: linesReadForRunStarting, runStarting } = + parseRunStarting(params, stdoutLines); + + const { linesRead: linesReadForSpecOutputs, specOutputs } = + parseSpecOutputs(params, stdoutLines.slice(linesReadForRunStarting)); + + const summary = parseSummaryTable( + params, + stdoutLines.slice(linesReadForRunStarting + linesReadForSpecOutputs) + ); + + const parsedReport = stdoutLines[stdoutLines.length - 1].match( + /^Unflakable report: (.*)$/ + ); + + return { + runStarting, + specOutputs, + summary, + unflakableReportUrl: parsedReport !== null ? parsedReport[1] : null, + }; + } else { + return { + runStarting: null, + specOutputs: [], + summary: parsePluginDisabledSummaryTable(params, stdoutLines), + unflakableReportUrl: null, + }; + } +}; diff --git a/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts b/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts new file mode 100644 index 0000000..823d9b6 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; + +integrationTestSuite(() => { + it("run should not fail due to error fetching manifest", (done) => + integrationTest( + { + params: { + failToFetchManifest: true, + skipBeforeHook: true, + skipFailures: true, + skipFlake: true, + skipQuarantined: true, + }, + expectedExitCode: 0, + summaryTotals: { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 5, + numPending: 14, + numQuarantined: 0, + numSkipped: 0, + // No error thrown in invalid.cy.ts means Cypress doesn't create a fake test to + // represent the failure. + numTests: 19, + }, + }, + done + )); + + it("reporter should print results even if upload fails", (done) => + integrationTest( + { + params: { + failToUploadResults: true, + }, + expectedExitCode: 1, + }, + done + )); + + it("reporter should print results even if both manifest fetch and upload fail", (done) => + integrationTest( + { + params: { + expectQuarantinedTestsToBeQuarantined: false, + failToFetchManifest: true, + failToUploadResults: true, + }, + expectedExitCode: 1, + summaryTotals: { + ...defaultSummaryTotals, + numFailing: 8, + numFlaky: 3, + numQuarantined: 0, + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/retries.test.ts b/packages/cypress-plugin/test/integration/src/retries.test.ts new file mode 100644 index 0000000..317a5a2 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/retries.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; + +integrationTestSuite(() => { + describe.each(["CLI", "config"] as ("CLI" | "config")[])( + "set failureRetries via %s", + (mode) => { + it("failureRetries = 0", (done) => + integrationTest( + { + params: { + ...(mode === "CLI" + ? { + cliArgs: ["--failure-retries", "0"], + } + : { + config: { + failureRetries: 0, + }, + }), + expectedRetries: 0, + }, + summaryTotals: { + ...defaultSummaryTotals, + // No flaky tests without retries. + numFlaky: 0, + numFailing: 8, + }, + }, + done + )); + + it("failureRetries = 1", (done) => + integrationTest( + { + params: { + ...(mode === "CLI" + ? { + cliArgs: ["--failure-retries", "1"], + } + : { + config: { + failureRetries: 1, + }, + }), + expectedRetries: 1, + }, + }, + done + )); + + it("failureRetries = 3", (done) => + integrationTest( + { + params: { + ...(mode === "CLI" + ? { + cliArgs: ["--failure-retries", "3"], + } + : { + config: { + failureRetries: 3, + }, + }), + expectedRetries: 3, + }, + }, + done + )); + } + ); +}); diff --git a/packages/cypress-plugin/test/integration/src/run-test-case.ts b/packages/cypress-plugin/test/integration/src/run-test-case.ts new file mode 100644 index 0000000..d999de7 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/run-test-case.ts @@ -0,0 +1,973 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + CreateTestSuiteRunFromUploadRequest, + CreateTestSuiteRunInlineRequest, + TEST_NAME_ENTRY_MAX_LENGTH, + TestAttemptResult, + TestRunRecord, + TestSuiteManifest, + TestSuiteRunPendingSummary, +} from "@unflakable/js-api"; +import { gunzipSync } from "zlib"; +import { UnflakableConfig } from "@unflakable/plugins-common"; +import { + CompletedRequest, + getLocal as getLocalHttpServer, + MockedEndpoint, +} from "mockttp"; +import _debug from "debug"; +import { execFile, spawn } from "child_process"; +import type { + CallbackResponseMessageResult, + CallbackResponseResult, +} from "mockttp/dist/rules/requests/request-handler-definitions"; +import { promisify, TextDecoder } from "util"; +import { + CONFIG_MOCK_ENV_VAR, + CosmiconfigMockParams, +} from "cypress-integration-common/config"; +import { + GIT_MOCK_ENV_VAR, + SimpleGitMockParams, +} from "cypress-integration-common/git"; +import path from "path"; +import { SummaryTotals } from "./parse-output"; +import { expect as expectExt } from "@jest/globals"; +import "./matchers"; +import { verifyOutput } from "./verify-output"; +import treeKill from "tree-kill"; + +const debug = _debug("unflakable:integration-test:run-test-case"); + +// Jest times out after 70 seconds, so we bail early here to allow time to print the +// captured output before Jest kills the test. +const TEST_TIMEOUT_MS = 50000; + +const userAgentRegex = new RegExp( + "unflakable-js-api/(?:[-0-9.]|alpha|beta)+ unflakable-cypress-plugin/(?:[-0-9.]|alpha|beta)+ \\(Cypress [0-9]+\\.[0-9]+\\.[0-9]+; Node v[0-9]+\\.[0-9]+\\.[0-9]\\)" +); + +export type TestMode = "component" | "e2e"; + +export type TestProjectName = + | "integration-input" + | "integration-input-esm" + | "integration-input-manual"; + +export type TestProject = { + configFiles: string[]; +}; + +export const TEST_PROJECTS: { [key in TestProjectName]: TestProject } = { + "integration-input": { + configFiles: [ + "cypress.config.ts", + "cypress-config.js", + "cypress-config.mjs", + ], + }, + "integration-input-esm": { + configFiles: [ + "cypress.config.ts", + "cypress-config.cjs", + "cypress-config.js", + ], + }, + "integration-input-manual": { + configFiles: ["cypress.config.js", "cypress-config.mjs"], + }, +}; + +export const TEST_SPEC_STUBS = [ + "fail", + "flake", + "hook-fail", + "invalid", + "mixed/mixed", + "pass", + "pending", + "quarantined", +]; + +export type TestCaseParams = { + cliArgs: string[]; + config: Partial | null; + configFile: string; + envVars: { [key in string]: string | undefined }; + testEnvVars: { [key in string]: string }; + expectedApiKey: string; + expectedBranch: string | undefined; + expectedCommit: string | undefined; + expectedFlakeTestNameSuffix: string; + expectedSuiteId: string; + expectedRepoRelativePathPrefix: string; + expectedRetries: number; + expectPluginToBeEnabled: boolean; + expectResultsToBeUploaded: boolean; + expectQuarantinedTestsToBeQuarantined: boolean; + expectQuarantinedTestsToBeSkipped: boolean; + failToFetchManifest: boolean; + failToUploadResults: boolean; + git: SimpleGitMockParams; + project: TestProjectName; + quarantineFlake: boolean; + quarantineHookFail: boolean; + quarantineHookSkip: boolean; + + skipFailures: boolean; + skipFlake: boolean; + skipQuarantined: boolean; + + skipBeforeHook: boolean; + skipBeforeEachHook: boolean; + skipAfterEachHook: boolean; + skipAfterHook: boolean; + hookAndTestErrors: boolean; + multipleHookErrors: boolean; + + // Array of spec names to pass to --spec. These should just be the stubs (e.g., "pass" or "fail"). + // The full spec path becomes `cypress//.cy.`. + specNameStubs: string[] | undefined; + testMode: TestMode; +}; + +export const specFilename = ( + params: TestCaseParams, + specNameStub: string +): string => + `${specNameStub}.cy.${ + params.project === "integration-input-manual" ? "js" : "ts" + }`; + +export const specProjectPath = ( + params: TestCaseParams, + specNameStub: string +): string => `cypress/${params.testMode}/${specFilename(params, specNameStub)}`; + +const specRepoPath = (params: TestCaseParams, specNameStub: string): string => + params.expectedRepoRelativePathPrefix + specProjectPath(params, specNameStub); + +export const apiServer = getLocalHttpServer({ + // debug: true, + suggestChanges: false, +}); +export const objectStoreServer = getLocalHttpServer({ + // debug: true, + suggestChanges: false, +}); + +export const MOCK_RUN_ID = "MOCK_RUN_ID"; +const TIMESTAMP_REGEX = + /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/; + +type ExpectedRunRecord = { + name: string[]; + attemptResults: TestAttemptResult[]; +}; + +const expectSpecRuns = ( + params: TestCaseParams, + specNameStub: string, + expectedRunRecords: ExpectedRunRecord[] +): TestRunRecord[] => + params.specNameStubs === undefined || + params.specNameStubs.includes(specNameStub) + ? expectedRunRecords.map(({ name, attemptResults }) => ({ + attempts: attemptResults.map((result) => ({ + start_time: expectExt.stringMatching(TIMESTAMP_REGEX), + duration_ms: expectExt.toBeAnInteger(), + result, + })), + filename: specRepoPath(params, specNameStub), + name, + })) + : []; + +const verifyUploadResults = ( + params: TestCaseParams, + summaryTotals: SummaryTotals, + request: CompletedRequest +): void => { + const { + expectedBranch, + expectedCommit, + expectedRetries, + expectedFlakeTestNameSuffix, + expectQuarantinedTestsToBeQuarantined, + expectQuarantinedTestsToBeSkipped, + hookAndTestErrors, + quarantineFlake, + quarantineHookFail, + quarantineHookSkip, + skipBeforeHook, + skipBeforeEachHook, + skipAfterEachHook, + skipAfterHook, + skipFailures, + skipFlake, + skipQuarantined, + } = params; + + const parsedBody = JSON.parse( + gunzipSync(request.body.buffer).toString() + ) as CreateTestSuiteRunInlineRequest; + + expect(request.headers["user-agent"]).toMatch(userAgentRegex); + + parsedBody.test_runs.sort((a, b) => + a.filename < b.filename + ? -1 + : a.filename > b.filename + ? 1 + : a.name < b.name + ? -1 + : a.name > b.name + ? 1 + : a < b + ? -1 + : a > b + ? 1 + : 0 + ); + + expect(parsedBody).toStrictEqual({ + ...(expectedBranch !== undefined + ? { + branch: expectedBranch, + } + : {}), + ...(expectedCommit !== undefined + ? { + commit: expectedCommit, + } + : {}), + start_time: expectExt.stringMatching(TIMESTAMP_REGEX), + end_time: expectExt.stringMatching(TIMESTAMP_REGEX), + test_runs: [ + ...(!skipFailures + ? expectSpecRuns(params, "fail", [ + { + name: ["describe block", "inner block", "should showDiff"], + attemptResults: Array.from( + { length: expectedRetries + 1 }, + () => "fail" + ), + }, + { + name: ["describe block", "should fail"], + attemptResults: Array.from( + { length: expectedRetries + 1 }, + () => "fail" + ), + }, + { + name: ["describe block", "should fail with multiple exceptions"], + attemptResults: Array.from( + { length: expectedRetries + 1 }, + () => "fail" + ), + }, + ]) + : []), + ...(!skipFlake && (!quarantineFlake || !expectQuarantinedTestsToBeSkipped) + ? expectSpecRuns(params, "flake", [ + { + name: [ + `should be flaky${expectedFlakeTestNameSuffix}`.substring( + 0, + TEST_NAME_ENTRY_MAX_LENGTH + ), + ], + attemptResults: [ + quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? "quarantined" + : "fail", + ...(expectedRetries > 0 ? ["pass"] : []), + ] as TestAttemptResult[], + }, + ]) + : []), + ...expectSpecRuns(params, "hook-fail", [ + ...(skipBeforeHook && + ((skipBeforeEachHook && skipAfterEachHook) || !skipAfterHook) + ? [ + { + name: ["describe block", "should be skipped"], + attemptResults: [ + skipAfterHook + ? "pass" + : quarantineHookSkip + ? "quarantined" + : "fail", + ] as TestAttemptResult[], + }, + ] + : []), + { + name: ["describe block", "should fail due to hook"], + attemptResults: + skipBeforeHook && + skipBeforeEachHook && + skipAfterEachHook && + !hookAndTestErrors + ? ["pass"] + : Array.from( + { + length: + skipBeforeHook && + (!skipBeforeEachHook || + !skipAfterEachHook || + hookAndTestErrors) + ? expectedRetries + 1 + : 1, + }, + () => (quarantineHookFail ? "quarantined" : "fail") + ), + }, + ]), + ...(!skipFailures + ? expectSpecRuns(params, "invalid", [ + { + name: ["An uncaught error was detected outside of a test"], + attemptResults: Array.from( + { length: expectedRetries + 1 }, + () => "fail" + ), + }, + ]) + : []), + ...expectSpecRuns(params, "mixed/mixed", [ + ...(!skipQuarantined && !expectQuarantinedTestsToBeSkipped + ? [ + { + name: [ + "spec with mixed test results", + "mixed: failure should be quarantined", + ], + attemptResults: Array.from( + { length: expectedRetries + 1 }, + () => + expectQuarantinedTestsToBeQuarantined + ? "quarantined" + : "fail" + ), + }, + { + name: [ + "spec with mixed test results", + "mixed: flake should be quarantined", + ], + attemptResults: [ + expectQuarantinedTestsToBeQuarantined + ? "quarantined" + : "fail", + ...(expectedRetries > 0 ? ["pass"] : []), + ] as TestAttemptResult[], + }, + ] + : []), + ...(!skipFlake && + (!quarantineFlake || !expectQuarantinedTestsToBeSkipped) + ? [ + { + name: [ + "spec with mixed test results", + "mixed: should be flaky", + ], + attemptResults: [ + quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? "quarantined" + : "fail", + ...(expectedRetries > 0 ? ["pass"] : []), + ] as TestAttemptResult[], + }, + ] + : []), + ...(!skipFailures + ? [ + { + name: ["spec with mixed test results", "mixed: should fail"], + attemptResults: Array.from( + { length: expectedRetries + 1 }, + () => "fail" as TestAttemptResult + ), + }, + ] + : []), + { + name: ["spec with mixed test results", "mixed: should pass"], + attemptResults: ["pass"], + }, + ]), + ...expectSpecRuns(params, "pass", [ + { + name: ["should pass"], + attemptResults: ["pass"], + }, + { + name: ["suite name", "suite test should pass"], + attemptResults: ["pass"], + }, + ]), + ...(!skipQuarantined && !expectQuarantinedTestsToBeSkipped + ? expectSpecRuns(params, "quarantined", [ + { + name: ["describe block", "should be quarantined"], + attemptResults: Array.from({ length: expectedRetries + 1 }, () => + expectQuarantinedTestsToBeQuarantined ? "quarantined" : "fail" + ), + }, + ]) + : []), + ], + }); + // Make sure there aren't any extra tests reported. + expect(parsedBody.test_runs).toHaveLength( + summaryTotals.numFailing + + summaryTotals.numFlaky + + (!expectQuarantinedTestsToBeSkipped ? summaryTotals.numQuarantined : 0) + + summaryTotals.numPassing + ); +}; + +const addFetchMockExpectations = async ( + params: TestCaseParams, + summaryTotals: SummaryTotals, + onError: (e: unknown) => void +): Promise<{ + unmatchedApiRequestEndpoint: MockedEndpoint; + unmatchedObjectStoreRequestEndpoint: MockedEndpoint; +}> => { + const { + expectedApiKey, + expectedBranch, + expectedCommit, + expectedFlakeTestNameSuffix, + expectedSuiteId, + expectResultsToBeUploaded, + failToFetchManifest, + failToUploadResults, + quarantineFlake, + quarantineHookFail, + quarantineHookSkip, + } = params; + + const onUnmatchedRequest = ( + request: CompletedRequest + ): CallbackResponseResult => { + onError(new Error(`Unexpected request ${request.method} ${request.path}`)); + return { statusCode: 500 }; + }; + + const unmatchedApiRequestEndpoint = await apiServer + .forUnmatchedRequest() + .thenCallback(onUnmatchedRequest); + const unmatchedObjectStoreRequestEndpoint = await objectStoreServer + .forUnmatchedRequest() + .thenCallback(onUnmatchedRequest); + + if (!params.expectPluginToBeEnabled) { + return { + unmatchedApiRequestEndpoint, + unmatchedObjectStoreRequestEndpoint, + }; + } + + await apiServer + .forGet(`/api/v1/test-suites/${expectedSuiteId}/manifest`) + .times(failToFetchManifest ? 3 : 1) + .withHeaders({ + Authorization: `Bearer ${expectedApiKey}`, + }) + .thenCallback((request): CallbackResponseResult => { + try { + expect(request.headers["user-agent"]).toMatch(userAgentRegex); + + if (failToFetchManifest) { + return "reset"; + } + + const responseBody: TestSuiteManifest = { + quarantined_tests: [ + { + test_id: "TEST_QUARANTINED", + filename: specRepoPath(params, "quarantined"), + name: ["describe block", "should be quarantined"], + }, + { + test_id: "TEST_MIXED_QUARANTINED_FAIL", + filename: specRepoPath(params, "mixed/mixed"), + name: [ + "spec with mixed test results", + "mixed: failure should be quarantined", + ], + }, + { + test_id: "TEST_MIXED_QUARANTINED_FLAKE", + filename: specRepoPath(params, "mixed/mixed"), + name: [ + "spec with mixed test results", + "mixed: flake should be quarantined", + ], + }, + { + test_id: "TEST_QUARANTINED_PENDING", + filename: specRepoPath(params, "pending"), + name: [ + "suite name", + "suite test should be quarantined and pending", + ], + }, + ...(quarantineFlake + ? [ + { + test_id: "TEST_FLAKE", + filename: specRepoPath(params, "flake"), + name: [ + `should be flaky${expectedFlakeTestNameSuffix}`.substring( + 0, + TEST_NAME_ENTRY_MAX_LENGTH + ), + ], + }, + { + test_id: "TEST_MIXED_FLAKE", + filename: specRepoPath(params, "mixed/mixed"), + name: [ + "spec with mixed test results", + "mixed: should be flaky", + ], + }, + ] + : []), + ...(quarantineHookFail + ? [ + { + test_id: "TEST_HOOK_FAIL", + filename: specRepoPath(params, "hook-fail"), + name: ["describe block", "should fail due to hook"], + }, + ] + : []), + ...(quarantineHookSkip + ? [ + { + test_id: "TEST_HOOK_SKIP", + filename: specRepoPath(params, "hook-fail"), + name: ["describe block", "should be skipped"], + }, + ] + : []), + ], + }; + + return { + statusCode: 200, + json: responseBody, + }; + } catch (e: unknown) { + onError(e); + return { statusCode: 500 }; + } + }); + + if (expectResultsToBeUploaded) { + const uploadPath = + `/unflakable-backend-mock-test-uploads/teams/MOCK_TEAM_ID/suites/${expectedSuiteId}/runs/` + + `upload/MOCK_UPLOAD_ID`; + const uploadQuery = "?X-Amz-Signature=MOCK_SIGNATURE"; + + await apiServer + .forPost(`/api/v1/test-suites/${expectedSuiteId}/runs/upload`) + .once() + .withHeaders({ + Authorization: `Bearer ${expectedApiKey}`, + "Content-Type": "application/json", + }) + .thenCallback(async (request) => { + try { + expect(await request.body.getText()).toBe(""); + return { + statusCode: 201, + headers: { + Location: `http://localhost:${objectStoreServer.port}${uploadPath}${uploadQuery}`, + }, + json: { + upload_id: "MOCK_UPLOAD_ID", + }, + }; + } catch (e) { + onError(e); + return { + statusCode: 500, + }; + } + }); + + let runRequest: CreateTestSuiteRunInlineRequest | null = null; + await objectStoreServer + .forPut(uploadPath) + .once() + .withExactQuery(uploadQuery) + .withHeaders({ + "Content-Encoding": "gzip", + "Content-Type": "application/json", + }) + .thenCallback((request): CallbackResponseMessageResult => { + try { + runRequest = JSON.parse( + gunzipSync(request.body.buffer).toString() + ) as CreateTestSuiteRunInlineRequest; + + verifyUploadResults(params, summaryTotals, request); + + return { + statusCode: 200, + }; + } catch (e) { + onError(e); + return { statusCode: 500 }; + } + }); + + await apiServer + .forPost(`/api/v1/test-suites/${expectedSuiteId}/runs`) + .times(failToUploadResults ? 3 : 1) + .withHeaders({ + Authorization: `Bearer ${expectedApiKey}`, + "Content-Type": "application/json", + }) + .thenCallback(async (request): Promise => { + try { + const body = await request.body.getText(); + expect(body).not.toBeNull(); + + const parsedBody = ((): CreateTestSuiteRunFromUploadRequest => { + try { + return JSON.parse( + body as string + ) as CreateTestSuiteRunFromUploadRequest; + } catch (e) { + throw new Error(`Invalid request body: ${JSON.stringify(body)}`, { + cause: e, + }); + } + })(); + + expect(parsedBody.upload_id).toBe("MOCK_UPLOAD_ID"); + expect(runRequest).not.toBeNull(); + + if (failToUploadResults) { + return "reset"; + } + + const parsedRequest = runRequest as CreateTestSuiteRunInlineRequest; + + return { + json: { + run_id: MOCK_RUN_ID, + suite_id: expectedSuiteId, + ...(expectedBranch !== undefined + ? { + branch: expectedBranch, + } + : {}), + ...(expectedCommit !== undefined + ? { + commit: expectedCommit, + } + : {}), + start_time: parsedRequest.start_time, + end_time: parsedRequest.end_time, + num_tests: + summaryTotals.numFailing + + summaryTotals.numFlaky + + summaryTotals.numQuarantined + + summaryTotals.numPassing, + num_pass: summaryTotals.numPassing, + num_fail: summaryTotals.numFailing, + num_flake: summaryTotals.numFlaky, + num_quarantined: summaryTotals.numQuarantined, + } as TestSuiteRunPendingSummary, + statusCode: 201, + }; + } catch (e) { + onError(e); + return { + statusCode: 500, + }; + } + }); + } + + return { + unmatchedApiRequestEndpoint, + unmatchedObjectStoreRequestEndpoint, + }; +}; + +// Similar to debug's default formatter, but with timestamps instead of the +Nms at the end of each +// call, which messes up Cypress's multi-line output. +function formatDebugArgsWithTimestamp( + this: _debug.Debugger & { useColors?: boolean }, + args: unknown[] +): void { + const { namespace: name, useColors } = this; + + const now = new Date().toISOString(); + if (useColors === true) { + const c = this.color as unknown as number; + const colorCode = "\x1B[3" + (c < 8 ? c.toString() : "8;5;" + c.toString()); + const prefix = ` ${colorCode};1m${now} ${name} \x1B[0m`; + + args[0] = `${prefix}${(args[0] as string).split("\n").join(`\n${prefix}`)}`; + } else { + args[0] = `${now} ${name} ${args[0] as string}`; + } +} + +_debug.formatArgs = formatDebugArgsWithTimestamp; + +export const runTestCase = async ( + params: TestCaseParams, + expectedExitCode: number, + summaryTotals: SummaryTotals +): Promise => { + const { + skipFailures, + skipFlake, + skipQuarantined, + skipBeforeHook, + skipBeforeEachHook, + skipAfterEachHook, + skipAfterHook, + hookAndTestErrors, + multipleHookErrors, + } = params; + + const fetchMismatch = { error: undefined as unknown | undefined }; + + const { unmatchedApiRequestEndpoint, unmatchedObjectStoreRequestEndpoint } = + await addFetchMockExpectations(params, summaryTotals, (error) => { + if (fetchMismatch.error === undefined) { + fetchMismatch.error = error ?? new Error("undefined error"); + } else { + console.error("Multiple failed fetch expectations", error); + } + }); + + const projectPath = path.join("..", params.project); + const configMockParams: CosmiconfigMockParams = { + searchFrom: path.resolve(projectPath), + searchResult: + params.config !== null + ? { + config: params.config, + filepath: "MOCK_BASE/packages/cypress-plugin/test/unflakable.yml", + } + : null, + }; + + // We don't directly invoke `cypress-unflakable` because we need to pass `--require` to Node.JS + // in order to mock cosmiconfig for testing. Instead, we resolve the binary to an absolute path + // using `yarn bin` and then invoke node directly. + const cypressPluginBin = ( + await promisify(execFile)("yarn", ["bin", "cypress-unflakable"]) + ).stdout.trimEnd(); + + Object.entries(params.testEnvVars).forEach(([key, value]) => { + expect( + value, + `Environment variable ${key} must not contain commas or spaces` + ).not.toMatch(/[ ,]/); + }); + const testEnv = [ + ...(skipFailures ? ["SKIP_FAILURES=1"] : []), + ...(skipFlake ? ["SKIP_FLAKE=1"] : []), + ...(skipQuarantined ? ["SKIP_QUARANTINED=1"] : []), + ...(skipBeforeHook ? ["SKIP_BEFORE_HOOK=1"] : []), + ...(skipBeforeEachHook ? ["SKIP_BEFORE_EACH_HOOK=1"] : []), + ...(skipAfterEachHook ? ["SKIP_AFTER_EACH_HOOK=1"] : []), + ...(skipAfterHook ? ["SKIP_AFTER_HOOK=1"] : []), + ...(hookAndTestErrors ? ["HOOK_AND_TEST_ERRORS=1"] : []), + ...(multipleHookErrors ? ["MULTIPLE_HOOK_ERRORS=1"] : []), + ...Object.entries(params.testEnvVars).map( + ([key, value]) => `${key}=${value}` + ), + ].join(","); + + const args = [ + "--require", + require.resolve("cypress-integration-common/mock-cosmiconfig"), + cypressPluginBin, + ...(params.project === "integration-input-manual" + ? ["--no-auto-config", "--no-auto-support"] + : []), + ...params.cliArgs, + "--", + // e2e/component + `--${params.testMode}`, + // Chrome is faster than Electron, at least on Mac. + "--browser", + "chrome", + ...(params.specNameStubs !== undefined + ? [ + "--spec", + params.specNameStubs + .map((stub) => specProjectPath(params, stub)) + .join(","), + ] + : []), + ...(params.configFile !== "cypress.config.ts" + ? ["--config-file", params.configFile] + : []), + ...(testEnv !== "" ? ["--env", testEnv] : []), + ]; + + const env = { + ...params.envVars, + DEBUG: process.env.TEST_DEBUG, + // Enable terminal colors for debug() output. + DEBUG_COLORS: "1", + // Ensure Cypress prints output with TTY colors. + FORCE_COLOR: "1", + // NODE_OPTIONS: "--loader=testdouble", + // Needed for resolving `cypress-unflakable` path. + PATH: process.env.PATH, + UNFLAKABLE_API_BASE_URL: `http://localhost:${apiServer.port}`, + [CONFIG_MOCK_ENV_VAR]: JSON.stringify(configMockParams), + [GIT_MOCK_ENV_VAR]: JSON.stringify(params.git), + }; + + debug( + `Spawning test:\n args = %o\n environment = %o\n cwd = %s`, + args, + env, + projectPath + ); + + const cypressChild = spawn("node", args, { + cwd: projectPath, + env, + }); + + const onOutput = ( + name: string, + onLine: (line: string) => void, + escapeDebugOutput: boolean + ): ((data: Buffer) => void) => { + const debugExt = debug.extend(name); + const decoder = new TextDecoder("utf-8", { fatal: true }); + + const pending = { s: "" }; + + // Don't eat the last line of output. + cypressChild.on("exit", () => { + if (pending.s !== "") { + onLine(pending.s); + debugExt(escapeDebugOutput ? JSON.stringify(pending.s) : pending.s); + } + }); + + return (data: Buffer): void => { + // In case data terminates in the middle of a Unicode sequence, we need to use a stateful + // TextDecoder with `stream: true`. Otherwise, invalid UTF-8 sequences at the end get + // converted to 0xFFFD, which breaks the tests non-deterministically (i.e., makes them flaky). + const lines = decoder.decode(data, { stream: true }).split("\n"); + + // If the last line is empty, then `dataStr` ends in a linebreak. Otherwise, we have a + // partial line that we want to defer until the next call. + lines.slice(0, lines.length - 1).forEach((line, idx) => { + const lineWithPending = idx === 0 ? pending.s + line : line; + onLine(lineWithPending); + debugExt( + escapeDebugOutput ? JSON.stringify(lineWithPending) : lineWithPending + ); + }); + + pending.s = lines[lines.length - 1]; + }; + }; + + const stdoutLines = [] as string[]; + const combinedLines = [] as string[]; + + cypressChild.stderr.on( + "data", + onOutput( + "stderr", + combinedLines.push.bind(combinedLines), + // Don't escape stderr output since it likely comes from debug output in the subprocess, which + // is intended for human consumption and not for verifying test results. + false + ) + ); + cypressChild.stdout.on( + "data", + onOutput( + "stdout", + (line) => { + stdoutLines.push(line); + combinedLines.push(line); + }, + // Escape special characters in debug output so that we can more easily understand test + // failures related to unexpected output. + true + ) + ); + + type ChildResult = { + code: number | null; + signal: NodeJS.Signals | null; + }; + + try { + const { code, signal } = await new Promise( + (resolve, reject) => { + const watchdog = setTimeout(() => { + console.error( + `Test timed out after ${TEST_TIMEOUT_MS}ms; killing Cypress process tree` + ); + treeKill(cypressChild.pid, "SIGKILL", () => { + reject(new Error(`Test timed out after ${TEST_TIMEOUT_MS}ms`)); + }); + }, TEST_TIMEOUT_MS); + + cypressChild.on("error", (err) => { + clearTimeout(watchdog); + reject(err); + }); + cypressChild.on("exit", (code, signal) => { + clearTimeout(watchdog); + resolve({ code, signal }); + }); + } + ); + + if (fetchMismatch.error !== undefined) { + throw fetchMismatch.error; + } + + verifyOutput(params, stdoutLines, summaryTotals, apiServer.port); + + expect(signal).toBe(null); + expect(code).toBe(expectedExitCode); + + expect(await apiServer.getPendingEndpoints()).toStrictEqual([ + unmatchedApiRequestEndpoint, + ]); + expect(await objectStoreServer.getPendingEndpoints()).toStrictEqual([ + unmatchedObjectStoreRequestEndpoint, + ]); + } catch (e: unknown) { + // Jest doesn't have a built-in setting for printing console logs only for failed tests, so we + // just defer the output until this catch block and attach it to the error. See + // https://github.com/jestjs/jest/issues/4156. We don't call console.log() directly here because + // that output gets printed before the failed test, whereas the error gets printed immediately + // after, which makes it easy to associate with the corresponding test. + throw new Error(`Test failed with output:\n\n${combinedLines.join("\n")}`, { + cause: e, + }); + } +}; diff --git a/packages/cypress-plugin/test/integration/src/test-wrappers.ts b/packages/cypress-plugin/test/integration/src/test-wrappers.ts new file mode 100644 index 0000000..7f94fc2 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/test-wrappers.ts @@ -0,0 +1,152 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + apiServer, + objectStoreServer, + runTestCase, + TestCaseParams, +} from "./run-test-case"; +import path from "path"; +import _debug from "debug"; +import cypressPackage from "cypress/package.json"; +import { SummaryTotals } from "./parse-output"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + // Public typing is wrong: done.fail() no longer exists. + interface DoneCallback { + (): void; + + (reason: Error | string): never; + } + } +} + +const debug = _debug("unflakable:integration-test:test-wrappers"); + +export type TestCase = { + params: Partial; + expectedExitCode?: number; + summaryTotals?: SummaryTotals; +}; + +export const defaultExitCode = 8; +export const defaultSummaryTotals: SummaryTotals = { + icon: "fail", + numFailing: 6, + numFlaky: 2, + numPassing: 3, + numPending: 5, + numQuarantined: 3, + numSkipped: 1, + numTests: 20, +}; + +export const integrationTest = ( + testCase: TestCase, + done: jest.DoneCallback +): void => { + void runTestCase( + { + cliArgs: [], + config: null, + configFile: "cypress.config.ts", + testEnvVars: {}, + expectedApiKey: "MOCK_API_KEY", + expectedBranch: "MOCK_BRANCH", + expectedCommit: "MOCK_COMMIT", + expectedFlakeTestNameSuffix: "", + expectedSuiteId: "MOCK_SUITE_ID", + expectedRepoRelativePathPrefix: `test/${ + testCase.params.project ?? "integration-input" + }/`, + expectedRetries: 2, + expectPluginToBeEnabled: true, + expectResultsToBeUploaded: true, + expectQuarantinedTestsToBeQuarantined: true, + expectQuarantinedTestsToBeSkipped: false, + failToFetchManifest: false, + failToUploadResults: false, + git: { + abbreviatedRefs: { + HEAD: "MOCK_BRANCH", + "refs/heads/MOCK_BRANCH": "MOCK_BRANCH", + }, + refs: [{ sha: "MOCK_COMMIT", refName: "refs/heads/MOCK_BRANCH" }], + commit: "MOCK_COMMIT", + isRepo: true, + // Mock the git repo root as packages/cypress-plugin so that we're for sure testing the + // mocked output and not using real git commands. + repoRoot: path.resolve("../.."), + }, + project: "integration-input", + quarantineFlake: false, + quarantineHookFail: false, + quarantineHookSkip: false, + skipFailures: false, + skipFlake: false, + skipQuarantined: false, + + skipBeforeHook: false, + skipBeforeEachHook: true, + skipAfterEachHook: true, + skipAfterHook: true, + hookAndTestErrors: false, + multipleHookErrors: false, + + specNameStubs: undefined, + testMode: "e2e", + ...testCase.params, + envVars: { + UNFLAKABLE_API_KEY: "MOCK_API_KEY", + UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID", + ...testCase.params.envVars, + }, + }, + testCase.expectedExitCode ?? defaultExitCode, + testCase.summaryTotals ?? defaultSummaryTotals + ) + .then(done) + .catch((e) => { + done(e as string | { message: string }); + }); +}; + +export const integrationTestSuite = (runTests: () => void): void => { + beforeEach(async () => { + await apiServer.start(); + debug( + `Listening for mock API requests on http://localhost:${apiServer.port}` + ); + + await objectStoreServer.start(); + debug( + `Listening for mock S3 requests on http://localhost:${objectStoreServer.port}` + ); + }); + + afterEach(async () => { + debug(`Stopping mock API server`); + await apiServer.stop(); + + debug(`Stopping mock S3 server`); + return objectStoreServer.stop(); + }); + + const cypressMinorVersion = cypressPackage.version.match(/^[^.]+\.[^.]+/); + const nodeMajorVersion = process.version.match(/^[^.]+/); + + describe(`Cypress ${ + cypressMinorVersion !== null + ? cypressMinorVersion[0] + : cypressPackage.version + }`, () => { + // Only use Node major version for test name. + describe(`Node ${ + nodeMajorVersion !== null ? nodeMajorVersion[0] : process.version + }`, () => { + runTests(); + }); + }); +}; diff --git a/packages/cypress-plugin/test/integration/src/unicode.test.ts b/packages/cypress-plugin/test/integration/src/unicode.test.ts new file mode 100644 index 0000000..be9a10a --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/unicode.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +import { + defaultSummaryTotals, + integrationTest, + integrationTestSuite, +} from "./test-wrappers"; + +integrationTestSuite(() => { + it("emoji test names should be allowed", (done) => + integrationTest( + { + params: { + expectedFlakeTestNameSuffix: "🔥🔥🔥", + testEnvVars: { + FLAKE_TEST_NAME_SUFFIX: "🔥🔥🔥", + }, + }, + }, + done + )); + + it("emoji test names should support quarantining", (done) => + integrationTest( + { + params: { + expectedFlakeTestNameSuffix: "🔥🔥🔥", + testEnvVars: { + FLAKE_TEST_NAME_SUFFIX: "🔥🔥🔥", + }, + quarantineFlake: true, + }, + expectedExitCode: 6, + summaryTotals: { + ...defaultSummaryTotals, + numFlaky: 0, + numQuarantined: 5, + }, + }, + done + )); +}); diff --git a/packages/cypress-plugin/test/integration/src/verify-output.ts b/packages/cypress-plugin/test/integration/src/verify-output.ts new file mode 100644 index 0000000..ec5b440 --- /dev/null +++ b/packages/cypress-plugin/test/integration/src/verify-output.ts @@ -0,0 +1,1568 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +// Make sure expected output is present and chalk-formatted correctly. +import { + MOCK_RUN_ID, + specFilename, + TEST_SPEC_STUBS, + TestCaseParams, +} from "./run-test-case"; +import { + EMPTY_REPORTER_OUTPUT_MATCH, + EMPTY_RESULTS, + NonPassingTestAttempt, + parseOutput, + ReporterOutput, + RunStarting, + SpecOutput, + SpecResults, + SummaryRow, + SummaryTotals, +} from "./parse-output"; +import { expect as expectExt } from "@jest/globals"; +import escapeStringRegexp from "escape-string-regexp"; +import { TestAttemptResult } from "@unflakable/js-api"; + +const THROWN_ERROR = "\x1B[0m\x1B[31m Error\x1B[0m\x1B[90m"; + +const verifySpecOutput = ( + params: TestCaseParams, + specOutputs: SpecOutput[], + specNameStub: string, + expectedReporterOutput: ReporterOutput, + expectedResults: SpecResults +): void => { + const { specNameStubs } = params; + const specOutput = specOutputs.find( + (spec) => spec.filename === specFilename(params, specNameStub) + ); + if (specNameStubs === undefined || specNameStubs.includes(specNameStub)) { + expect( + specOutput, + `no output found for spec \`${specNameStub}\`` + ).toBeDefined(); + const spec = specOutput as SpecOutput; + expect( + spec.reporterOutput, + `unexpected reporter output for spec \`${specNameStub}\`` + ).toStrictEqual(expectedReporterOutput); + expect( + spec.results, + `unexpected results for spec \`${specNameStub}\`` + ).toStrictEqual(expectedResults); + } else { + expect( + specOutput, + `unexpected output found for spec \`${specNameStub}\`` + ).toBeUndefined(); + } +}; + +const createAttempts = ( + { expectedRetries }: TestCaseParams, + expectedTitlePath: string[], + expectedErrorLines: string[] +): NonPassingTestAttempt[] => + Array.from({ length: expectedRetries + 1 }, (_, idx) => ({ + titlePath: expectedTitlePath, + attempt: + expectedRetries > 0 + ? { + attemptNum: idx + 1, + totalAttempts: expectedRetries + 1, + } + : undefined, + errorLines: expectedErrorLines, + })); + +const verifySpecOutputs = ( + params: TestCaseParams, + specOutputs: SpecOutput[] +): void => { + const { + expectPluginToBeEnabled, + expectQuarantinedTestsToBeQuarantined, + expectQuarantinedTestsToBeSkipped, + expectedFlakeTestNameSuffix, + expectedRetries, + hookAndTestErrors, + multipleHookErrors, + quarantineFlake, + quarantineHookFail, + quarantineHookSkip, + skipFailures, + skipFlake, + skipQuarantined, + skipBeforeHook, + skipBeforeEachHook, + skipAfterEachHook, + skipAfterHook, + } = params; + + verifySpecOutput( + params, + specOutputs, + "mixed/mixed", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: [ + "\x1B[0m spec with mixed test results\x1B[0m", + ...(!skipQuarantined + ? expectQuarantinedTestsToBeQuarantined + ? expectQuarantinedTestsToBeSkipped + ? [ + " \x1B[36m - mixed: failure should be quarantined\x1B[0m\x1B[35m [quarantined]\x1B[39m", + " \x1B[36m - mixed: flake should be quarantined\x1B[0m\x1B[35m [quarantined]\x1B[39m", + ] + : [ + ...Array.from( + // Last retry has different output. + { length: expectedRetries }, + (_, idx) => + ` \x1B[31m ✖ mixed: failure should be quarantined\x1B[0m\x1B[33m (attempt ${ + idx + 1 + } of ${expectedRetries + 1})\x1B[0m` + ), + ` \x1B[35m ✖ mixed: failure should be quarantined [failed, quarantined]\x1B[39m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${expectedRetries + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }`, + ...(expectedRetries > 0 + ? [ + ` \x1B[31m ✖ mixed: flake should be quarantined\x1B[0m\x1B[33m (attempt 1 of ${ + expectedRetries + 1 + })\x1B[0m`, + expectExt.stringMatching( + new RegExp( + // eslint-disable-next-line no-control-regex + `^ {2}\x1B\\[35m {2}✓\x1B\\[39m\x1B\\[90m mixed: flake should be quarantined\x1B\\[0m\x1B\\[35m \\[flaky, quarantined]\x1B\\[39m\x1B\\[33m \\(attempt 2 of ${ + expectedRetries + 1 + }\\)\x1B\\[0m\x1B\\[90m \\([0-9]+.+?\\)\x1B\\[0m$` + ) + ), + ] + : [ + " \x1B[35m ✖ mixed: flake should be quarantined [failed, quarantined]\x1B[39m", + ]), + ] + : [ + ...Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + ` \x1B[31m ✖ mixed: failure should be quarantined\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ), + ` \x1B[31m ✖ mixed: flake should be quarantined\x1B[0m\x1B[33m (attempt 1 of ${ + expectedRetries + 1 + })\x1B[0m`, + expectExt.stringMatching( + new RegExp( + // eslint-disable-next-line no-control-regex + `^ {2}\x1B\\[33m {2}✓\x1B\\[0m\x1B\\[90m mixed: flake should be quarantined\x1B\\[0m\x1B\\[33m \\[flaky]\x1B\\[0m\x1B\\[33m \\(attempt 2 of ${ + expectedRetries + 1 + }\\)\x1B\\[0m\x1B\\[90m \\([0-9]+.+?\\)\x1B\\[0m$` + ) + ), + ] + : [ + " \x1B[36m - mixed: failure should be quarantined\x1B[0m", + " \x1B[36m - mixed: flake should be quarantined\x1B[0m", + ]), + ...(!skipFailures + ? Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + ` \x1B[31m ✖ mixed: should fail\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ) + : [" \x1B[36m - mixed: should fail\x1B[0m"]), + ...(!skipFlake + ? quarantineFlake && + expectQuarantinedTestsToBeQuarantined && + expectQuarantinedTestsToBeSkipped + ? [ + " \x1B[36m - mixed: should be flaky\x1B[0m\x1B[35m [quarantined]\x1B[39m", + ] + : expectedRetries > 0 + ? [ + ` \x1B[31m ✖ mixed: should be flaky\x1B[0m\x1B[33m (attempt 1 of ${ + expectedRetries + 1 + })\x1B[0m`, + quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? expectExt.stringMatching( + new RegExp( + // eslint-disable-next-line no-control-regex + `^ {2}\x1B\\[35m {2}✓\x1B\\[39m\x1B\\[90m mixed: should be flaky\x1B\\[0m\x1B\\[35m \\[flaky, quarantined]\x1B\\[39m\x1B\\[33m \\(attempt 2 of ${ + expectedRetries + 1 + }\\)\x1B\\[0m\x1B\\[90m \\([0-9]+.+?\\)\x1B\\[0m$` + ) + ) + : expectExt.stringMatching( + new RegExp( + // eslint-disable-next-line no-control-regex + `^ {2}\x1B\\[33m {2}✓\x1B\\[0m\x1B\\[90m mixed: should be flaky\x1B\\[0m\x1B\\[33m \\[flaky]\x1B\\[0m\x1B\\[33m \\(attempt 2 of ${ + expectedRetries + 1 + }\\)\x1B\\[0m\x1B\\[90m \\([0-9]+.+?\\)\x1B\\[0m$` + ) + ), + ] + : [" \x1B[31m ✖ mixed: should be flaky\x1B[0m"] + : [" \x1B[36m - mixed: should be flaky\x1B[0m"]), + expectExt.stringMatching( + // eslint-disable-next-line no-control-regex + /^ {2}\x1B\[32m {2}✓\x1B\[0m\x1B\[90m mixed: should pass\x1B\[0m\x1B\[90m \([0-9]+.+?\)\x1B\[0m$/ + ), + " \x1B[36m - mixed: should be skipped\x1B[0m", + ], + failures: { + count: + (!skipFailures ? 1 : 0) + + (!skipFlake && expectedRetries === 0 ? 1 : 0) + + (!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + !expectQuarantinedTestsToBeQuarantined + ? 1 + : 0), + tests: [ + ...(!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + !expectQuarantinedTestsToBeQuarantined + ? [ + { + attempts: createAttempts( + params, + [ + "spec with mixed test results", + "mixed: failure should be quarantined", + ], + expectExt.arrayContaining([THROWN_ERROR]) + ), + }, + ] + : []), + ...(!skipFailures + ? [ + { + attempts: createAttempts( + params, + ["spec with mixed test results", "mixed: should fail"], + expectExt.arrayContaining([THROWN_ERROR]) + ), + }, + ] + : []), + ...(!skipFlake && expectedRetries === 0 + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: should be flaky", + ], + attempt: undefined, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ] + : []), + ], + }, + flakes: { + count: + (!skipFlake && + (!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries > 0 + ? 1 + : 0) + + (!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + !expectQuarantinedTestsToBeQuarantined && + expectedRetries > 0 + ? 1 + : 0), + tests: [ + ...(!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + !expectQuarantinedTestsToBeQuarantined && + expectedRetries > 0 + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: flake should be quarantined", + ], + attempt: { + attemptNum: 1, + totalAttempts: 2, + }, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ] + : []), + ...(!skipFlake && + (!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries > 0 + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: should be flaky", + ], + attempt: { + attemptNum: 1, + totalAttempts: 2, + }, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ] + : []), + ], + }, + passing: 1, + pending: + 1 + + (!skipFailures ? 0 : 1) + + (!skipFlake ? 0 : 1) + + (!skipQuarantined ? 0 : 2), + quarantinedFailures: { + count: + !skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? 1 + (expectedRetries === 0 ? 1 : 0) + : 0, + tests: [ + ...(!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? [ + { + attempts: createAttempts( + params, + [ + "spec with mixed test results", + "mixed: failure should be quarantined", + ], + expectExt.arrayContaining([THROWN_ERROR]) + ), + }, + ...(expectedRetries === 0 + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: flake should be quarantined", + ], + attempt: undefined, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ] + : []), + ] + : []), + ], + }, + quarantinedFlakes: { + count: + (!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined && + expectedRetries > 0 + ? 1 + : 0) + + (!skipFlake && + quarantineFlake && + expectQuarantinedTestsToBeQuarantined && + !expectQuarantinedTestsToBeSkipped && + expectedRetries > 0 + ? 1 + : 0), + tests: [ + ...(!skipQuarantined && + !expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined && + expectedRetries > 0 + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: flake should be quarantined", + ], + attempt: { + attemptNum: 1, + totalAttempts: 2, + }, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ] + : []), + ...(!skipFlake && + quarantineFlake && + expectQuarantinedTestsToBeQuarantined && + !expectQuarantinedTestsToBeSkipped && + expectedRetries > 0 + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: should be flaky", + ], + attempt: { + attemptNum: 1, + totalAttempts: 2, + }, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ] + : []), + ], + }, + quarantinedPending: { + count: + (!skipQuarantined && + expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? 2 + : 0) + + (!skipFlake && + quarantineFlake && + expectQuarantinedTestsToBeQuarantined && + expectQuarantinedTestsToBeSkipped + ? 1 + : 0), + tests: [ + ...(!skipQuarantined && + expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: failure should be quarantined", + ], + attempt: undefined, + errorLines: [], + }, + ], + }, + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: flake should be quarantined", + ], + attempt: undefined, + errorLines: [], + }, + ], + }, + ] + : []), + ...(!skipFlake && + quarantineFlake && + expectQuarantinedTestsToBeQuarantined && + expectQuarantinedTestsToBeSkipped + ? [ + { + attempts: [ + { + titlePath: [ + "spec with mixed test results", + "mixed: should be flaky", + ], + attempt: undefined, + errorLines: [], + }, + ], + }, + ] + : []), + ], + }, + }, + { + ...EMPTY_RESULTS, + color: + skipFailures && + (skipFlake || + (quarantineFlake && expectQuarantinedTestsToBeQuarantined)) && + expectQuarantinedTestsToBeQuarantined + ? "pass" + : "fail", + numTests: 6, + numFailing: + // fail + (!skipFailures ? 1 : 0) + + // flake + (!skipFlake && + (!expectPluginToBeEnabled || + (!quarantineFlake && expectedRetries === 0)) + ? 1 + : 0) + + // quarantined fail + (!expectPluginToBeEnabled || !expectQuarantinedTestsToBeQuarantined + ? 1 + : 0) + + // quarantined flake + (!skipFlake && + (!expectPluginToBeEnabled || + (!expectQuarantinedTestsToBeQuarantined && expectedRetries === 0)) + ? 1 + : 0), + numFlaky: + // flake + (!skipFlake && + expectPluginToBeEnabled && + (!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries > 0 + ? 1 + : 0) + + // quarantined flake + (!skipFlake && + expectPluginToBeEnabled && + !expectQuarantinedTestsToBeQuarantined && + expectedRetries > 0 + ? 1 + : 0), + numPassing: 1, + numPending: + 1 + + (!skipFailures ? 0 : 1) + + (!skipFlake ? 0 : 1) + + (!skipQuarantined ? 0 : 2), + numQuarantined: + expectPluginToBeEnabled && expectQuarantinedTestsToBeQuarantined + ? (!skipQuarantined ? 2 : 0) + (!skipFlake && quarantineFlake ? 1 : 0) + : 0, + } + ); + + verifySpecOutput( + params, + specOutputs, + "fail", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: [ + "\x1B[0m describe block\x1B[0m", + ...(!skipFailures + ? Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + ` \x1B[31m ✖ should fail\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ) + : [" \x1B[36m - should fail\x1B[0m"]), + ...(!skipFailures + ? Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + ` \x1B[31m ✖ should fail with multiple exceptions\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ) + : [" \x1B[36m - should fail with multiple exceptions\x1B[0m"]), + "\x1B[0m inner block\x1B[0m", + ...(!skipFailures + ? Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + ` \x1B[31m ✖ should showDiff\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ) + : [" \x1B[36m - should showDiff\x1B[0m"]), + ], + + failures: { + count: !skipFailures ? 3 : 0, + tests: !skipFailures + ? [ + { + attempts: createAttempts( + params, + ["describe block", "should fail"], + expectExt.arrayContaining([THROWN_ERROR]) + ), + }, + { + attempts: createAttempts( + params, + ["describe block", "should fail with multiple exceptions"], + expectExt.arrayContaining([ + "> first", + "\x1B[31m Error: second (and Mocha's done() called multiple times)\x1B[0m\x1B[90m", + ]) + ), + }, + { + attempts: createAttempts( + params, + ["describe block", "inner block", "should showDiff"], + expectExt.arrayContaining([ + "\x1B[0m\x1B[31m AssertionError: expected 'foobar' to equal 'foo'", + ]) + ), + }, + ] + : [], + }, + pending: !skipFailures ? 0 : 3, + }, + { + ...EMPTY_RESULTS, + color: skipFailures ? "pass" : "fail", + numTests: 3, + numFailing: !skipFailures ? 3 : 0, + numPending: !skipFailures ? 0 : 3, + } + ); + + verifySpecOutput( + params, + specOutputs, + "flake", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: + !skipFlake && + (!quarantineFlake || + !expectQuarantinedTestsToBeQuarantined || + !expectQuarantinedTestsToBeSkipped) + ? expectedRetries > 0 + ? [ + `\x1B[31m ✖ should be flaky${expectedFlakeTestNameSuffix}\x1B[0m\x1B[33m (attempt 1 of ${ + expectedRetries + 1 + })\x1B[0m`, + quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? expectExt.stringMatching( + new RegExp( + // eslint-disable-next-line no-control-regex + `^\x1B\\[35m {2}✓\x1B\\[39m\x1B\\[90m should be flaky${escapeStringRegexp( + expectedFlakeTestNameSuffix + )}\x1B\\[0m\x1B\\[35m \\[flaky, quarantined]\x1B\\[39m\x1B\\[33m \\(attempt 2 of ${ + expectedRetries + 1 + }\\)\x1B\\[0m\x1B\\[90m \\([0-9]+.+?\\)\x1B\\[0m$` + ) + ) + : expectExt.stringMatching( + new RegExp( + // eslint-disable-next-line no-control-regex + `^\x1B\\[33m {2}✓\x1B\\[0m\x1B\\[90m should be flaky${escapeStringRegexp( + expectedFlakeTestNameSuffix + )}\x1B\\[0m\x1B\\[33m \\[flaky]\x1B\\[0m\x1B\\[33m \\(attempt 2 of ${ + expectedRetries + 1 + }\\)\x1B\\[0m\x1B\\[90m \\([0-9]+.+?\\)\x1B\\[0m$` + ) + ), + ] + : [ + `\x1B[31m ✖ should be flaky${expectedFlakeTestNameSuffix}\x1B[0m`, + ] + : [ + `\x1B[36m - should be flaky${expectedFlakeTestNameSuffix}\x1B[0m${ + quarantineFlake && + expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? "\x1B[35m [quarantined]\x1B[39m" + : "" + }`, + ], + ...(skipFlake + ? { + pending: 1, + } + : expectedRetries > 0 + ? { + [quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? expectQuarantinedTestsToBeSkipped + ? "quarantinedPending" + : "quarantinedFlakes" + : "flakes"]: { + count: 1, + tests: [ + { + attempts: [ + { + titlePath: [ + `should be flaky${expectedFlakeTestNameSuffix}`, + ], + attempt: + !quarantineFlake || !expectQuarantinedTestsToBeSkipped + ? { + attemptNum: 1, + totalAttempts: 2, + } + : undefined, + errorLines: + !quarantineFlake || !expectQuarantinedTestsToBeSkipped + ? expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]) + : [], + }, + ], + }, + ], + }, + } + : { + [quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? "quarantinedFailures" + : "failures"]: { + count: 1, + tests: [ + { + attempts: [ + { + titlePath: [ + `should be flaky${expectedFlakeTestNameSuffix}`, + ], + attempt: undefined, + errorLines: expectExt.arrayContaining([ + "\x1B[0m\x1B[31m Error: first try should fail\x1B[0m\x1B[90m", + ]), + }, + ], + }, + ], + }, + }), + }, + { + ...EMPTY_RESULTS, + color: + skipFlake || (quarantineFlake && expectQuarantinedTestsToBeQuarantined) + ? "pass" + : "fail", + numTests: 1, + numFailing: + !skipFlake && + (!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries === 0 + ? 1 + : 0, + numFlaky: + !skipFlake && + (!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries > 0 + ? 1 + : 0, + numPending: !skipFlake ? 0 : 1, + numQuarantined: + !skipFlake && quarantineFlake && expectQuarantinedTestsToBeQuarantined + ? 1 + : 0, + } + ); + + const hookFailResult: TestAttemptResult = + skipBeforeHook && + skipBeforeEachHook && + skipAfterEachHook && + !hookAndTestErrors + ? "pass" + : quarantineHookFail + ? "quarantined" + : "fail"; + const hookSkipResult: TestAttemptResult | "skip" = + skipBeforeHook && + ((skipBeforeEachHook && skipAfterEachHook) || !skipAfterHook) + ? skipAfterHook + ? "pass" + : quarantineHookSkip + ? "quarantined" + : "fail" + : "skip"; + + const hookFailTestFailure = { + attempts: Array.from( + { + length: + skipBeforeHook && + (!skipBeforeEachHook || !skipAfterEachHook || hookAndTestErrors) + ? expectedRetries + 1 + : 1, + }, + (_, idx) => ({ + titlePath: ["describe block", "should fail due to hook"], + attempt: + skipBeforeHook && + (!skipBeforeEachHook || !skipAfterEachHook || hookAndTestErrors) + ? { + attemptNum: idx + 1, + totalAttempts: expectedRetries + 1, + } + : undefined, + errorLines: expectExt.arrayContaining([ + ...(hookAndTestErrors + ? [ + expectExt.stringMatching( + // eslint-disable-next-line no-control-regex + /^(?:\x1B\[0m)?\x1B\[31m {5}Error: test error\x1B\[0m\x1B\[90m$/ + ), + ] + : []), + ...(!skipBeforeHook || !skipBeforeEachHook || !skipAfterEachHook + ? [ + expectExt.stringMatching( + new RegExp( + `^(?:\x1B\\[0m)?\x1B\\[31m {5}Error: "${ + !skipBeforeHook + ? "before all" + : !skipBeforeEachHook + ? "before each" + : "after each" + }" hook failed:$` + ) + ), + expectExt.stringMatching( + new RegExp( + `^ *> ${ + !skipBeforeHook + ? "before" + : !skipBeforeEachHook + ? "beforeEach" + : "afterEach" + } Error #1$` + ) + ), + ...(multipleHookErrors + ? [ + expectExt.stringMatching( + new RegExp( + `^(?:\x1B\\[31m)?(?: {5})?(?:Error: )?${ + !skipBeforeHook + ? "before" + : !skipBeforeEachHook + ? "beforeEach" + : "afterEach" + } Error #2 \\(and Mocha's done\\(\\) called multiple times\\)$` + ) + ), + ] + : []), + ] + : []), + ]), + }) + ), + }; + const hookSkipTestFailure = { + attempts: [ + { + titlePath: ["describe block", "should be skipped"], + attempt: undefined, + errorLines: expectExt.arrayContaining([ + ...(!skipAfterHook + ? ['\x1B[0m\x1B[31m Error: "after all" hook failed:'] + : []), + `> after Error #1`, + ]), + }, + ], + }; + + verifySpecOutput( + params, + specOutputs, + "hook-fail", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: [ + "\x1B[0m describe block\x1B[0m", + ...(hookFailResult === "pass" + ? [ + expectExt.stringMatching( + // eslint-disable-next-line no-control-regex + /^ {2}\x1B\[32m {2}✓\x1B\[0m\x1B\[90m should fail due to hook\x1B\[0m\x1B\[90m \([0-9]+.+?\)\x1B\[0m$/ + ), + ] + : Array.from( + { + length: + skipBeforeHook && + (!skipBeforeEachHook || + !skipAfterEachHook || + hookAndTestErrors) + ? expectedRetries + 1 + : 1, + }, + (_, idx) => + // NB: The default Mocha reporter prints `"" hook for ""` as a + // sort of fake test name, but it uses the same test ID as the associated test. + // Dealing with multiple names for a single test leads to inconsistencies and also + // makes it hard to quarantine tests that fail due to hook failures. Instead, we just + // treat hook failures as failures of the associated test, and Cypress's error + // message already mentions the hook that failed. See: + // https://github.com/mochajs/mocha/blob/0be3f78491bbbcdc4dcea660ee7bfd557a225d9c/lib/runner.js#L332 + (hookFailResult === "quarantined" && + (!skipBeforeHook || idx === expectedRetries) + ? " \x1B[35m ✖ should fail due to hook [failed, quarantined]\x1B[39m" + : " \x1B[31m ✖ should fail due to hook\x1B[0m") + + (skipBeforeHook && expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "") + )), + ...(hookSkipResult === "pass" + ? [ + expectExt.stringMatching( + // eslint-disable-next-line no-control-regex + /^ {2}\x1B\[32m {2}✓\x1B\[0m\x1B\[90m should be skipped\x1B\[0m\x1B\[90m \([0-9]+.+?\)\x1B\[0m$/ + ), + ] + : hookSkipResult === "fail" + ? [" \x1B[31m ✖ should be skipped\x1B[0m"] + : hookSkipResult === "quarantined" + ? [" \x1B[35m ✖ should be skipped [failed, quarantined]\x1B[39m"] + : []), + ], + passing: + (hookFailResult === "pass" ? 1 : 0) + + (hookSkipResult === "pass" ? 1 : 0), + failures: { + count: + (hookFailResult === "fail" ? 1 : 0) + + (hookSkipResult === "fail" ? 1 : 0), + tests: [ + ...(hookFailResult === "fail" ? [hookFailTestFailure] : []), + ...(hookSkipResult === "fail" ? [hookSkipTestFailure] : []), + ], + }, + quarantinedFailures: { + count: + (hookFailResult === "quarantined" ? 1 : 0) + + (hookSkipResult === "quarantined" ? 1 : 0), + tests: [ + ...(hookFailResult === "quarantined" ? [hookFailTestFailure] : []), + ...(hookSkipResult === "quarantined" ? [hookSkipTestFailure] : []), + ], + }, + skipped: { + count: hookSkipResult === "skip" ? 1 : 0, + tests: + hookSkipResult === "skip" + ? [ + { + titlePath: ["describe block", "should be skipped"], + isQuarantined: quarantineHookSkip, + }, + ] + : [], + }, + }, + { + ...EMPTY_RESULTS, + color: + hookFailResult !== "fail" && + hookSkipResult !== "fail" && + (hookSkipResult !== "skip" || quarantineHookSkip) + ? "pass" + : "fail", + numTests: 2, + numFailing: + (hookFailResult === "fail" ? 1 : 0) + + (hookSkipResult === "fail" ? 1 : 0), + numPassing: + (hookFailResult === "pass" ? 1 : 0) + + (hookSkipResult === "pass" ? 1 : 0), + numQuarantined: + (hookFailResult === "quarantined" ? 1 : 0) + + (hookSkipResult === "quarantined" ? 1 : 0), + numSkipped: hookSkipResult === "skip" ? 1 : 0, + } + ); + + verifySpecOutput( + params, + specOutputs, + "invalid", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: !skipFailures + ? Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + `\x1B[31m ✖ An uncaught error was detected outside of a test\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ) + : [], + failures: { + count: !skipFailures ? 1 : 0, + tests: !skipFailures + ? [ + { + attempts: createAttempts( + params, + ["An uncaught error was detected outside of a test"], + expectExt.arrayContaining(["> invalid test file"]) + ), + }, + ] + : [], + }, + }, + { + ...EMPTY_RESULTS, + color: skipFailures ? "pass" : "fail", + numTests: !skipFailures ? 1 : 0, + numFailing: !skipFailures ? 1 : 0, + } + ); + + verifySpecOutput( + params, + specOutputs, + "pass", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: [ + // This gets printed before the test title. + "called consoleLog command", + expectExt.stringMatching( + // eslint-disable-next-line no-control-regex + /^\x1B\[32m +✓\x1B\[0m\x1B\[90m should pass\x1B\[0m\x1B\[90m \([0-9]+.+?\)\x1B\[0m$/ + ), + "\x1B[0m suite name\x1B[0m", + expectExt.stringMatching( + // eslint-disable-next-line no-control-regex + /^ {2}\x1B\[32m {2}✓\x1B\[0m\x1B\[90m suite test should pass\x1B\[0m\x1B\[90m \([0-9]+.+?\)\x1B\[0m$/ + ), + ], + passing: 2, + }, + { + ...EMPTY_RESULTS, + color: "pass", + numTests: 2, + numPassing: 2, + } + ); + + verifySpecOutput( + params, + specOutputs, + "pending", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: [ + "\x1B[36m - stub should be pending\x1B[0m", + "\x1B[36m - should be pending\x1B[0m", + "\x1B[0m suite name\x1B[0m", + " \x1B[36m - suite test should be pending\x1B[0m", + // NB: Calling it.skip() on a quarantined test with quarantineMode set to skip_tests is + // indistinguishable from calling it() and having our plugin convert the call to it.skip(). + ` \x1B[36m - suite test should be quarantined and pending\x1B[0m${ + expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? "\x1B[35m [quarantined]\x1B[39m" + : "" + }`, + ], + pending: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? 4 + : 3, + quarantinedPending: { + count: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? 0 + : 1, + tests: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? [] + : [ + { + attempts: [ + { + titlePath: [ + "suite name", + "suite test should be quarantined and pending", + ], + attempt: undefined, + errorLines: [], + }, + ], + }, + ], + }, + }, + { + ...EMPTY_RESULTS, + color: "pass", + numTests: 4, + numPending: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? 4 + : 3, + numQuarantined: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? 0 + : 1, + } + ); + + verifySpecOutput( + params, + specOutputs, + "quarantined", + { + ...EMPTY_REPORTER_OUTPUT_MATCH, + suitesAndTestAttempts: [ + "\x1B[0m describe block\x1B[0m", + ...(!skipQuarantined + ? expectQuarantinedTestsToBeQuarantined + ? !expectQuarantinedTestsToBeSkipped + ? [ + ...Array.from( + // Last retry has different output. + { length: expectedRetries }, + (_, idx) => + ` \x1B[31m ✖ should be quarantined\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ), + ` \x1B[35m ✖ should be quarantined [failed, quarantined]\x1B[39m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${expectedRetries + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }`, + ] + : [ + " \x1B[36m - should be quarantined\x1B[0m\x1B[35m [quarantined]\x1B[39m", + ] + : Array.from( + { length: expectedRetries + 1 }, + (_, idx) => + ` \x1B[31m ✖ should be quarantined\x1B[0m${ + expectedRetries > 0 + ? `\x1B[33m (attempt ${idx + 1} of ${ + expectedRetries + 1 + })\x1B[0m` + : "" + }` + ) + : [" \x1B[36m - should be quarantined\x1B[0m"]), + ], + ...(skipQuarantined + ? { + pending: 1, + } + : expectQuarantinedTestsToBeSkipped && + expectQuarantinedTestsToBeQuarantined + ? { + quarantinedPending: { + count: 1, + tests: [ + { + attempts: [ + { + titlePath: ["describe block", "should be quarantined"], + attempt: undefined, + errorLines: [], + }, + ], + }, + ], + }, + } + : { + [expectQuarantinedTestsToBeQuarantined + ? "quarantinedFailures" + : "failures"]: { + count: 1, + tests: [ + { + attempts: createAttempts( + params, + ["describe block", "should be quarantined"], + expectExt.arrayContaining([THROWN_ERROR]) + ), + }, + ], + }, + }), + }, + { + ...EMPTY_RESULTS, + color: + skipQuarantined || expectQuarantinedTestsToBeQuarantined + ? "pass" + : "fail", + numTests: 1, + numPending: skipQuarantined ? 1 : 0, + numQuarantined: + skipQuarantined || !expectQuarantinedTestsToBeQuarantined ? 0 : 1, + numFailing: + skipQuarantined || expectQuarantinedTestsToBeQuarantined ? 0 : 1, + } + ); +}; + +export const verifyOutput = ( + params: TestCaseParams, + stdoutLines: string[], + summaryTotals: SummaryTotals, + apiServerPort: number +): void => { + const { + expectPluginToBeEnabled, + expectQuarantinedTestsToBeQuarantined, + expectQuarantinedTestsToBeSkipped, + expectResultsToBeUploaded, + expectedSuiteId, + expectedRetries, + failToUploadResults, + hookAndTestErrors, + quarantineFlake, + quarantineHookFail, + quarantineHookSkip, + skipFailures, + skipFlake, + skipQuarantined, + skipBeforeHook, + skipBeforeEachHook, + skipAfterEachHook, + skipAfterHook, + // hookAndTestErrors, + // multipleHookErrors, + specNameStubs, + } = params; + + // process.stdout.write( + // "stdoutLines:\n" + + // stdoutLines.map((line) => `${JSON.stringify(line)}`).join(",\n") + + // "\n" + // ); + + const parsedOutput = parseOutput(params, stdoutLines); + // console.log("PARSED OUTPUT", prettyFormat(parsedOutput)); + + const expectedSpecs = (specNameStubs ?? TEST_SPEC_STUBS) + .map((stub) => specFilename(params, stub)) + .sort(); + + if (expectPluginToBeEnabled) { + expect(parsedOutput.runStarting).not.toBeNull(); + + expect( + [...(parsedOutput.runStarting as RunStarting).specs].sort() + ).toStrictEqual(expectedSpecs); + + // Make sure there are no unexpected specs. + expect(expectedSpecs).toStrictEqual( + parsedOutput.specOutputs.map((spec) => spec.filename).sort() + ); + + verifySpecOutputs(params, parsedOutput.specOutputs); + } + + const summaryRows = expectedSpecs.map((spec): SummaryRow => { + switch (spec) { + case specFilename(params, "fail"): + return { + icon: !skipFailures ? "fail" : "pass", + numFailing: !skipFailures ? 3 : 0, + numFlaky: 0, + numPassing: 0, + numPending: !skipFailures ? 0 : 3, + numQuarantined: 0, + numSkipped: 0, + numTests: 3, + specName: specFilename(params, "fail"), + }; + case specFilename(params, "flake"): + return { + icon: !skipFlake && !quarantineFlake ? "fail" : "pass", + numFailing: + !skipFlake && + (!expectPluginToBeEnabled || + ((!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries === 0)) + ? 1 + : 0, + numFlaky: + !skipFlake && + expectPluginToBeEnabled && + !quarantineFlake && + expectedRetries > 0 + ? 1 + : 0, + numPassing: 0, + numPending: !skipFlake ? 0 : 1, + numQuarantined: + !skipFlake && + expectPluginToBeEnabled && + expectQuarantinedTestsToBeQuarantined && + quarantineFlake + ? 1 + : 0, + numSkipped: 0, + numTests: 1, + specName: specFilename(params, "flake"), + }; + case specFilename(params, "hook-fail"): { + const hookFailResult: TestAttemptResult = + skipBeforeHook && + skipBeforeEachHook && + skipAfterEachHook && + !hookAndTestErrors + ? "pass" + : quarantineHookFail + ? "quarantined" + : "fail"; + const hookSkipResult: TestAttemptResult | "skip" = + skipBeforeHook && + ((skipBeforeEachHook && skipAfterEachHook) || !skipAfterHook) + ? skipAfterHook + ? "pass" + : quarantineHookSkip + ? "quarantined" + : "fail" + : "skip"; + return { + icon: + hookFailResult !== "fail" && + hookSkipResult !== "fail" && + (hookSkipResult !== "skip" || quarantineHookSkip) + ? "pass" + : "fail", + numFailing: + (hookFailResult === "fail" ? 1 : 0) + + (hookSkipResult === "fail" ? 1 : 0), + numFlaky: 0, + numPassing: + (hookFailResult === "pass" ? 1 : 0) + + (hookSkipResult === "pass" ? 1 : 0), + numPending: 0, + numQuarantined: + (hookFailResult === "quarantined" ? 1 : 0) + + (hookSkipResult === "quarantined" ? 1 : 0), + numSkipped: hookSkipResult === "skip" ? 1 : 0, + numTests: 2, + specName: specFilename(params, "hook-fail"), + }; + } + case specFilename(params, "invalid"): + return { + icon: !skipFailures ? "fail" : "pass", + numFailing: !skipFailures ? 1 : 0, + numFlaky: 0, + numPassing: 0, + numPending: 0, + numQuarantined: 0, + numSkipped: 0, + numTests: !skipFailures ? 1 : 0, + specName: specFilename(params, "invalid"), + }; + case specFilename(params, "pass"): + return { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 2, + numPending: 0, + numQuarantined: 0, + numSkipped: 0, + numTests: 2, + specName: specFilename(params, "pass"), + }; + case specFilename(params, "pending"): + return { + icon: "pass", + numFailing: 0, + numFlaky: 0, + numPassing: 0, + numPending: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? 4 + : 3, + numQuarantined: + !expectQuarantinedTestsToBeSkipped || + !expectQuarantinedTestsToBeQuarantined + ? 0 + : 1, + numSkipped: 0, + numTests: 4, + specName: specFilename(params, "pending"), + }; + case specFilename(params, "quarantined"): + return { + icon: + expectPluginToBeEnabled && expectQuarantinedTestsToBeQuarantined + ? "pass" + : "fail", + numFailing: + !skipQuarantined && + (!expectPluginToBeEnabled || !expectQuarantinedTestsToBeQuarantined) + ? 1 + : 0, + numFlaky: 0, + numPassing: 0, + numPending: skipQuarantined ? 1 : 0, + numQuarantined: + !skipQuarantined && + expectPluginToBeEnabled && + expectQuarantinedTestsToBeQuarantined + ? 1 + : 0, + numSkipped: 0, + numTests: 1, + specName: specFilename(params, "quarantined"), + }; + case specFilename(params, "mixed/mixed"): { + const numFailing = + // fail + (!skipFailures ? 1 : 0) + + // flake + (!skipFlake && + (!expectPluginToBeEnabled || + (!quarantineFlake && expectedRetries === 0)) + ? 1 + : 0) + + // quarantined fail + (!expectPluginToBeEnabled || !expectQuarantinedTestsToBeQuarantined + ? 1 + : 0) + + // quarantined flake + (!skipFlake && + (!expectPluginToBeEnabled || + (!expectQuarantinedTestsToBeQuarantined && expectedRetries === 0)) + ? 1 + : 0); + const numFlaky = + (!skipFlake && + expectPluginToBeEnabled && + (!quarantineFlake || !expectQuarantinedTestsToBeQuarantined) && + expectedRetries > 0 + ? 1 + : 0) + + (!skipFlake && + expectPluginToBeEnabled && + !expectQuarantinedTestsToBeQuarantined && + expectedRetries > 0 + ? 1 + : 0); + return { + icon: numFailing + numFlaky > 0 ? "fail" : "pass", + numFailing, + numFlaky, + numPassing: 1, + numPending: + 1 + + (!skipFailures ? 0 : 1) + + (!skipFlake ? 0 : 1) + + (!skipQuarantined ? 0 : 2), + numQuarantined: + expectPluginToBeEnabled && expectQuarantinedTestsToBeQuarantined + ? (!skipQuarantined ? 2 : 0) + + (!skipFlake && quarantineFlake ? 1 : 0) + : 0, + numSkipped: 0, + numTests: 6, + specName: specFilename(params, "mixed/mixed"), + }; + } + default: + throw new Error(`Unexpected spec ${spec}`); + } + }); + expect(parsedOutput.summary).toStrictEqual({ + rows: summaryRows, + totals: summaryTotals, + }); + + if ( + expectPluginToBeEnabled && + expectResultsToBeUploaded && + !failToUploadResults + ) { + expect(parsedOutput.unflakableReportUrl).toBe( + `http://localhost:${apiServerPort}/test-suites/${expectedSuiteId}/runs/${MOCK_RUN_ID}` + ); + } else { + expect(parsedOutput.unflakableReportUrl).toBeNull(); + } +}; diff --git a/packages/cypress-plugin/test/integration/tsconfig.json b/packages/cypress-plugin/test/integration/tsconfig.json new file mode 100644 index 0000000..75be32b --- /dev/null +++ b/packages/cypress-plugin/test/integration/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "skipLibCheck": true, + // The Cypress integration tests themselves use Jest since we can't easily run integration tests + // that involve lots of Node.JS processes from inside the browser. + "types": ["jest", "jest-expect-message", "node"] + }, + "include": [".eslintrc.js", "jest.config.js", "src"] +} diff --git a/packages/cypress-plugin/tsconfig.json b/packages/cypress-plugin/tsconfig.json new file mode 100644 index 0000000..9e9a7c8 --- /dev/null +++ b/packages/cypress-plugin/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/", + "declaration": true, + // Required by Rollup. + "module": "esnext", + // Ensure Rollup build fails if there are type errors. + "noEmitOnError": true, + // Removes DOM types since most of the plugin runs in Node. + "lib": ["ES2019"], + // Cypress itself requires DOM types to be globally defined, but we don't want that for our own + // code. + "skipLibCheck": true, + // Avoids conflicting global definitions from, e.g., jest. + "types": ["node"] + }, + "include": ["mocha.d.ts", "rollup.config.mjs"] +} diff --git a/packages/jest-plugin/test/integration-input/package.json b/packages/jest-plugin/test/integration-input/package.json index c3476b6..73e67cf 100644 --- a/packages/jest-plugin/test/integration-input/package.json +++ b/packages/jest-plugin/test/integration-input/package.json @@ -5,7 +5,8 @@ "@unflakable/jest-plugin": "workspace:^", "@unflakable/js-api": "workspace:^", "jest": "25.1.0 - 29", - "jest-each": "25.1.0 - 29" + "jest-each": "25.1.0 - 29", + "jest-environment-node": "25.1.0 - 29" }, "scripts": { "test": "jest", diff --git a/packages/jest-plugin/test/integration/package.json b/packages/jest-plugin/test/integration/package.json index 6baccf0..5a1e554 100644 --- a/packages/jest-plugin/test/integration/package.json +++ b/packages/jest-plugin/test/integration/package.json @@ -9,6 +9,8 @@ "deep-equal": "^2.0.5", "fetch-mock-jest": "^1.5.1", "jest": "25.1.0 - 29", + "jest-cli": "25.1.0 - 29", + "jest-environment-node": "25.1.0 - 29", "temp": "^0.9.4" }, "scripts": { diff --git a/packages/jest-plugin/test/integration/src/runTestCase.ts b/packages/jest-plugin/test/integration/src/runTestCase.ts index ed51094..6d5afd6 100644 --- a/packages/jest-plugin/test/integration/src/runTestCase.ts +++ b/packages/jest-plugin/test/integration/src/runTestCase.ts @@ -8,7 +8,7 @@ import { MockResponse, MockMatcher, } from "fetch-mock"; -import { run } from "jest"; +import { run } from "jest-cli"; import escapeStringRegexp from "escape-string-regexp"; import { CreateTestSuiteRunFromUploadRequest, diff --git a/packages/js-api/package.json b/packages/js-api/package.json index 1e746da..6ea366b 100644 --- a/packages/js-api/package.json +++ b/packages/js-api/package.json @@ -11,6 +11,16 @@ "version": "0.3.0", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./consts": { + "types": "./dist/consts.d.ts", + "default": "./dist/consts.js" + } + }, "files": [ "README.md", "dist/**/*.js", diff --git a/packages/js-api/src/consts.ts b/packages/js-api/src/consts.ts new file mode 100644 index 0000000..cf19eab --- /dev/null +++ b/packages/js-api/src/consts.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2023 Developer Innovations, LLC + +export const JS_API_VERSION: string = + // eslint-disable-next-line @typescript-eslint/no-var-requires + (require("../package.json") as { version: string }).version; + +export const TEST_NAME_ENTRY_MAX_LENGTH = 4096; diff --git a/packages/js-api/src/index.ts b/packages/js-api/src/index.ts index b47ec7d..778720c 100644 --- a/packages/js-api/src/index.ts +++ b/packages/js-api/src/index.ts @@ -5,6 +5,9 @@ import _debug = require("debug"); import { gzip } from "zlib"; import { promisify } from "util"; import retry from "async-retry"; +import { JS_API_VERSION } from "./consts"; + +export { JS_API_VERSION, TEST_NAME_ENTRY_MAX_LENGTH } from "./consts"; const debug = _debug("unflakable:api"); @@ -37,12 +40,6 @@ const fetch = (url: string, init?: RequestInit): Promise => { const BASE_URL = "https://app.unflakable.com"; -export const TEST_NAME_ENTRY_MAX_LENGTH = 4096; - -export const JS_API_VERSION: string = - // eslint-disable-next-line @typescript-eslint/no-var-requires - (require("../package.json") as { version: string }).version; - export type TestRef = { test_id: string; filename: string; diff --git a/packages/plugins-common/package.json b/packages/plugins-common/package.json index 114d4ea..ac4dd1b 100644 --- a/packages/plugins-common/package.json +++ b/packages/plugins-common/package.json @@ -10,8 +10,16 @@ "homepage": "https://unflakable.com", "license": "MIT", "version": "0.1.0", - "exports": "./dist/index.js", - "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./quarantine": { + "types": "./dist/quarantine.d.ts", + "default": "./dist/quarantine.js" + } + }, "dependencies": { "@unflakable/js-api": "workspace:^", "cosmiconfig": "^7.0.1", diff --git a/packages/plugins-common/src/quarantine.ts b/packages/plugins-common/src/quarantine.ts index ee7ee08..0fc27f5 100644 --- a/packages/plugins-common/src/quarantine.ts +++ b/packages/plugins-common/src/quarantine.ts @@ -2,10 +2,8 @@ // Avoid depending on the core of the js-api, which includes a bunch of Node dependencies. We need // this module to work in the browser for the Cypress plugin's skip-tests module. -import { - TEST_NAME_ENTRY_MAX_LENGTH, - TestSuiteManifest, -} from "@unflakable/js-api"; +import { TEST_NAME_ENTRY_MAX_LENGTH } from "@unflakable/js-api/consts"; +import type { TestSuiteManifest } from "@unflakable/js-api"; import deepEqual from "deep-equal"; // If the last test name entry is too long, truncate it to prevent the backend from diff --git a/tsconfig.json b/tsconfig.json index a502989..28c3e64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "noUnusedParameters": true /* Report errors on unused parameters. */, /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + // Support sub-path exports from package.json. + "moduleResolution": "node16", "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ diff --git a/yarn.lock b/yarn.lock index 72b138d..3a9fdd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,54 +15,54 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/code-frame@npm:7.22.5" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.21.4": + version: 7.21.4 + resolution: "@babel/code-frame@npm:7.21.4" dependencies: - "@babel/highlight": ^7.22.5 - checksum: cfe804f518f53faaf9a1d3e0f9f74127ab9a004912c3a16fda07fb6a633393ecb9918a053cb71804204c1b7ec3d49e1699604715e2cfb0c9f7bc4933d324ebb6 + "@babel/highlight": ^7.18.6 + checksum: e5390e6ec1ac58dcef01d4f18eaf1fd2f1325528661ff6d4a5de8979588b9f5a8e852a54a91b923846f7a5c681b217f0a45c2524eb9560553160cd963b7d592c languageName: node linkType: hard -"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.21.5, @babel/compat-data@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/compat-data@npm:7.22.5" - checksum: eb1a47ebf79ae268b4a16903e977be52629339806e248455eb9973897c503a04b701f36a9de64e19750d6e081d0561e77a514c8dc470babbeba59ae94298ed18 +"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.21.5, @babel/compat-data@npm:^7.22.0": + version: 7.22.3 + resolution: "@babel/compat-data@npm:7.22.3" + checksum: eb001646f41459f42ccb0d39ee8bb3c3c495bc297234817044c0002689c625e3159a6678c53fd31bd98cf21f31472b73506f350fc6906e3bdfa49cb706e2af8d languageName: node linkType: hard "@babel/core@npm:^7.0.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.5": - version: 7.22.5 - resolution: "@babel/core@npm:7.22.5" + version: 7.22.1 + resolution: "@babel/core@npm:7.22.1" dependencies: "@ampproject/remapping": ^2.2.0 - "@babel/code-frame": ^7.22.5 - "@babel/generator": ^7.22.5 - "@babel/helper-compilation-targets": ^7.22.5 - "@babel/helper-module-transforms": ^7.22.5 - "@babel/helpers": ^7.22.5 - "@babel/parser": ^7.22.5 - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.5 - "@babel/types": ^7.22.5 + "@babel/code-frame": ^7.21.4 + "@babel/generator": ^7.22.0 + "@babel/helper-compilation-targets": ^7.22.1 + "@babel/helper-module-transforms": ^7.22.1 + "@babel/helpers": ^7.22.0 + "@babel/parser": ^7.22.0 + "@babel/template": ^7.21.9 + "@babel/traverse": ^7.22.1 + "@babel/types": ^7.22.0 convert-source-map: ^1.7.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.2 semver: ^6.3.0 - checksum: 173ae426958c90c7bbd7de622c6f13fcab8aef0fac3f138e2d47bc466d1cd1f86f71ca82ae0acb9032fd8794abed8efb56fea55c031396337eaec0d673b69d56 + checksum: bbe45e791f223a7e692d2ea6597a73f48050abd24b119c85c48ac6504c30ce63343a2ea3f79b5847bf4b409ddd8a68b6cdc4f0272ded1d2ef6f6b1e9663432f0 languageName: node linkType: hard -"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/generator@npm:7.22.5" +"@babel/generator@npm:^7.22.0, @babel/generator@npm:^7.22.3, @babel/generator@npm:^7.7.2": + version: 7.22.3 + resolution: "@babel/generator@npm:7.22.3" dependencies: - "@babel/types": ^7.22.5 + "@babel/types": ^7.22.3 "@jridgewell/gen-mapping": ^0.3.2 "@jridgewell/trace-mapping": ^0.3.17 jsesc: ^2.5.1 - checksum: efa64da70ca88fe69f05520cf5feed6eba6d30a85d32237671488cc355fdc379fe2c3246382a861d49574c4c2f82a317584f8811e95eb024e365faff3232b49d + checksum: ccb6426ca5b5a38f0d47a3ac9628e223d2aaaa489cbf90ffab41468795c22afe86855f68a58667f0f2673949f1810d4d5a57b826c17984eab3e28fdb34a909e6 languageName: node linkType: hard @@ -84,18 +84,18 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.18.9, @babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.21.5, @babel/helper-compilation-targets@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-compilation-targets@npm:7.22.5" +"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.18.9, @babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.21.5, @babel/helper-compilation-targets@npm:^7.22.1": + version: 7.22.1 + resolution: "@babel/helper-compilation-targets@npm:7.22.1" dependencies: - "@babel/compat-data": ^7.22.5 - "@babel/helper-validator-option": ^7.22.5 + "@babel/compat-data": ^7.22.0 + "@babel/helper-validator-option": ^7.21.0 browserslist: ^4.21.3 lru-cache: ^5.1.1 semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: a479460615acffa0f4fd0a29b740eafb53a93694265207d23a6038ccd18d183a382cacca515e77b7c9b042c3ba80b0aca0da5f1f62215140e81660d2cf721b68 + checksum: a686a01bd3288cf95ca26faa27958d34c04e2501c4b0858c3a6558776dec20317b5635f33d64c5a635b6fbdfe462a85c30d4bfa0ae7e7ffe3467e4d06442d7c8 languageName: node linkType: hard @@ -147,29 +147,29 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.18.9, @babel/helper-environment-visitor@npm:^7.21.5, @babel/helper-environment-visitor@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-environment-visitor@npm:7.22.5" - checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 +"@babel/helper-environment-visitor@npm:^7.18.9, @babel/helper-environment-visitor@npm:^7.21.5, @babel/helper-environment-visitor@npm:^7.22.1": + version: 7.22.1 + resolution: "@babel/helper-environment-visitor@npm:7.22.1" + checksum: a6b4bb5505453bff95518d361ac1de393f0029aeb8b690c70540f4317934c53c43cc4afcda8c752ffa8c272e63ed6b929a56eca28e4978424177b24238b21bf9 languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.18.9, @babel/helper-function-name@npm:^7.19.0, @babel/helper-function-name@npm:^7.21.0, @babel/helper-function-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-function-name@npm:7.22.5" +"@babel/helper-function-name@npm:^7.18.9, @babel/helper-function-name@npm:^7.19.0, @babel/helper-function-name@npm:^7.21.0": + version: 7.21.0 + resolution: "@babel/helper-function-name@npm:7.21.0" dependencies: - "@babel/template": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + "@babel/template": ^7.20.7 + "@babel/types": ^7.21.0 + checksum: d63e63c3e0e3e8b3138fa47b0cd321148a300ef12b8ee951196994dcd2a492cc708aeda94c2c53759a5c9177fffaac0fd8778791286746f72a000976968daf4e languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.18.6, @babel/helper-hoist-variables@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-hoist-variables@npm:7.22.5" +"@babel/helper-hoist-variables@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/helper-hoist-variables@npm:7.18.6" dependencies: - "@babel/types": ^7.22.5 - checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + "@babel/types": ^7.18.6 + checksum: fd9c35bb435fda802bf9ff7b6f2df06308a21277c6dec2120a35b09f9de68f68a33972e2c15505c1a1a04b36ec64c9ace97d4a9e26d6097b76b4396b7c5fa20f languageName: node linkType: hard @@ -182,28 +182,28 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.18.6, @babel/helper-module-imports@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-module-imports@npm:7.22.5" +"@babel/helper-module-imports@npm:^7.18.6, @babel/helper-module-imports@npm:^7.21.4": + version: 7.21.4 + resolution: "@babel/helper-module-imports@npm:7.21.4" dependencies: - "@babel/types": ^7.22.5 - checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad + "@babel/types": ^7.21.4 + checksum: bd330a2edaafeb281fbcd9357652f8d2666502567c0aad71db926e8499c773c9ea9c10dfaae30122452940326d90c8caff5c649ed8e1bf15b23f858758d3abc6 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.20.11, @babel/helper-module-transforms@npm:^7.21.5, @babel/helper-module-transforms@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-module-transforms@npm:7.22.5" +"@babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.20.11, @babel/helper-module-transforms@npm:^7.21.5, @babel/helper-module-transforms@npm:^7.22.1": + version: 7.22.1 + resolution: "@babel/helper-module-transforms@npm:7.22.1" dependencies: - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-module-imports": ^7.22.5 - "@babel/helper-simple-access": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: 8985dc0d971fd17c467e8b84fe0f50f3dd8610e33b6c86e5b3ca8e8859f9448bcc5c84e08a2a14285ef388351c0484797081c8f05a03770bf44fc27bf4900e68 + "@babel/helper-environment-visitor": ^7.22.1 + "@babel/helper-module-imports": ^7.21.4 + "@babel/helper-simple-access": ^7.21.5 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/helper-validator-identifier": ^7.19.1 + "@babel/template": ^7.21.9 + "@babel/traverse": ^7.22.1 + "@babel/types": ^7.22.0 + checksum: dfa084211a93c9f0174ab07385fdbf7831bbf5c1ff3d4f984effc489f48670825ad8b817b9e9d2ec6492fde37ed6518c15944e9dd7a60b43a3d9874c9250f5f8 languageName: node linkType: hard @@ -216,10 +216,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.21.5, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.22.5 - resolution: "@babel/helper-plugin-utils@npm:7.22.5" - checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.21.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": + version: 7.21.5 + resolution: "@babel/helper-plugin-utils@npm:7.21.5" + checksum: 6f086e9a84a50ea7df0d5639c8f9f68505af510ea3258b3c8ac8b175efdfb7f664436cb48996f71791a1350ba68f47ad3424131e8e718c5e2ad45564484cbb36 languageName: node linkType: hard @@ -251,12 +251,12 @@ __metadata: languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.21.5, @babel/helper-simple-access@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-simple-access@npm:7.22.5" +"@babel/helper-simple-access@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-simple-access@npm:7.21.5" dependencies: - "@babel/types": ^7.22.5 - checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2 + "@babel/types": ^7.21.5 + checksum: ad212beaa24be3864c8c95bee02f840222457ccf5419991e2d3e3e39b0f75b77e7e857e0bf4ed428b1cd97acefc87f3831bdb0b9696d5ad0557421f398334fc3 languageName: node linkType: hard @@ -269,33 +269,33 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.18.6, @babel/helper-split-export-declaration@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-split-export-declaration@npm:7.22.5" +"@babel/helper-split-export-declaration@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/helper-split-export-declaration@npm:7.18.6" dependencies: - "@babel/types": ^7.22.5 - checksum: d10e05a02f49c1f7c578cea63d2ac55356501bbf58856d97ac9bfde4957faee21ae97c7f566aa309e38a256eef58b58e5b670a7f568b362c00e93dfffe072650 + "@babel/types": ^7.18.6 + checksum: c6d3dede53878f6be1d869e03e9ffbbb36f4897c7cc1527dc96c56d127d834ffe4520a6f7e467f5b6f3c2843ea0e81a7819d66ae02f707f6ac057f3d57943a2b languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-string-parser@npm:7.22.5" - checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 +"@babel/helper-string-parser@npm:^7.21.5": + version: 7.21.5 + resolution: "@babel/helper-string-parser@npm:7.21.5" + checksum: 36c0ded452f3858e67634b81960d4bde1d1cd2a56b82f4ba2926e97864816021c885f111a7cf81de88a0ed025f49d84a393256700e9acbca2d99462d648705d8 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.19.1, @babel/helper-validator-identifier@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-validator-identifier@npm:7.22.5" - checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea +"@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": + version: 7.19.1 + resolution: "@babel/helper-validator-identifier@npm:7.19.1" + checksum: 0eca5e86a729162af569b46c6c41a63e18b43dbe09fda1d2a3c8924f7d617116af39cac5e4cd5d431bb760b4dca3c0970e0c444789b1db42bcf1fa41fbad0a3a languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.21.0, @babel/helper-validator-option@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-validator-option@npm:7.22.5" - checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 +"@babel/helper-validator-option@npm:^7.21.0": + version: 7.21.0 + resolution: "@babel/helper-validator-option@npm:7.21.0" + checksum: 8ece4c78ffa5461fd8ab6b6e57cc51afad59df08192ed5d84b475af4a7193fc1cb794b59e3e7be64f3cdc4df7ac78bf3dbb20c129d7757ae078e6279ff8c2f07 languageName: node linkType: hard @@ -311,34 +311,34 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helpers@npm:7.22.5" +"@babel/helpers@npm:^7.22.0": + version: 7.22.3 + resolution: "@babel/helpers@npm:7.22.3" dependencies: - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: a96e785029dff72f171190943df895ab0f76e17bf3881efd630bc5fae91215042d1c2e9ed730e8e4adf4da6f28b24bd1f54ed93b90ffbca34c197351872a084e + "@babel/template": ^7.21.9 + "@babel/traverse": ^7.22.1 + "@babel/types": ^7.22.3 + checksum: 385289ee8b87cf9af448bbb9fcf747f6e67600db5f7f64eb4ad97761ee387819bf2212b6a757008286c6bfacf4f3fc0b6de88686f2e517a70fb59996bdfbd1e9 languageName: node linkType: hard -"@babel/highlight@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/highlight@npm:7.22.5" +"@babel/highlight@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/highlight@npm:7.18.6" dependencies: - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.18.6 chalk: ^2.0.0 js-tokens: ^4.0.0 - checksum: f61ae6de6ee0ea8d9b5bcf2a532faec5ab0a1dc0f7c640e5047fc61630a0edb88b18d8c92eb06566d30da7a27db841aca11820ecd3ebe9ce514c9350fbed39c4 + checksum: 92d8ee61549de5ff5120e945e774728e5ccd57fd3b2ed6eace020ec744823d4a98e242be1453d21764a30a14769ecd62170fba28539b211799bbaf232bbb2789 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/parser@npm:7.22.5" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.9, @babel/parser@npm:^7.22.0, @babel/parser@npm:^7.22.4": + version: 7.22.4 + resolution: "@babel/parser@npm:7.22.4" bin: parser: ./bin/babel-parser.js - checksum: 470ebba516417ce8683b36e2eddd56dcfecb32c54b9bb507e28eb76b30d1c3e618fd0cfeee1f64d8357c2254514e1a19e32885cfb4e73149f4ae875436a6d59c + checksum: 0ca6d3a2d9aae2504ba1bc494704b64a83140884f7379f609de69bd39b60adb58a4f8ec692fe53fef8657dd82705d01b7e6efb65e18296326bdd66f71d52d9a9 languageName: node linkType: hard @@ -655,13 +655,13 @@ __metadata: linkType: hard "@babel/plugin-syntax-jsx@npm:^7.21.4, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" + version: 7.21.4 + resolution: "@babel/plugin-syntax-jsx@npm:7.21.4" dependencies: - "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-plugin-utils": ^7.20.2 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + checksum: bb7309402a1d4e155f32aa0cf216e1fa8324d6c4cfd248b03280028a015a10e46b6efd6565f515f8913918a3602b39255999c06046f7d4b8a5106be2165d724a languageName: node linkType: hard @@ -1289,43 +1289,43 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": - version: 7.22.5 - resolution: "@babel/template@npm:7.22.5" +"@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.21.9, @babel/template@npm:^7.3.3": + version: 7.21.9 + resolution: "@babel/template@npm:7.21.9" dependencies: - "@babel/code-frame": ^7.22.5 - "@babel/parser": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 + "@babel/code-frame": ^7.21.4 + "@babel/parser": ^7.21.9 + "@babel/types": ^7.21.5 + checksum: 6ec2c60d4d53b2a9230ab82c399ba6525df87e9a4e01e4b111e071cbad283b1362e7c99a1bc50027073f44f2de36a495a89c27112c4e7efe7ef9c8d9c84de2ec languageName: node linkType: hard -"@babel/traverse@npm:^7.20.5, @babel/traverse@npm:^7.21.5, @babel/traverse@npm:^7.22.5, @babel/traverse@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/traverse@npm:7.22.5" +"@babel/traverse@npm:^7.20.5, @babel/traverse@npm:^7.21.5, @babel/traverse@npm:^7.22.1, @babel/traverse@npm:^7.7.2": + version: 7.22.4 + resolution: "@babel/traverse@npm:7.22.4" dependencies: - "@babel/code-frame": ^7.22.5 - "@babel/generator": ^7.22.5 - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 - "@babel/helper-hoist-variables": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.5 - "@babel/parser": ^7.22.5 - "@babel/types": ^7.22.5 + "@babel/code-frame": ^7.21.4 + "@babel/generator": ^7.22.3 + "@babel/helper-environment-visitor": ^7.22.1 + "@babel/helper-function-name": ^7.21.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.22.4 + "@babel/types": ^7.22.4 debug: ^4.1.0 globals: ^11.1.0 - checksum: 560931422dc1761f2df723778dcb4e51ce0d02e560cf2caa49822921578f49189a5a7d053b78a32dca33e59be886a6b2200a6e24d4ae9b5086ca0ba803815694 + checksum: 9560ae22092d5a7c52849145dd3e5aed2ffb73d61255e70e19e3fbd06bcbafbbdecea28df40a42ee3b60b01e85a42224ec841df93e867547e329091cc2f2bb6f languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.5, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.5, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.22.5 - resolution: "@babel/types@npm:7.22.5" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.5, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.4, @babel/types@npm:^7.21.5, @babel/types@npm:^7.22.0, @babel/types@npm:^7.22.3, @babel/types@npm:^7.22.4, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.22.4 + resolution: "@babel/types@npm:7.22.4" dependencies: - "@babel/helper-string-parser": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-string-parser": ^7.21.5 + "@babel/helper-validator-identifier": ^7.19.1 to-fast-properties: ^2.0.0 - checksum: c13a9c1dc7d2d1a241a2f8363540cb9af1d66e978e8984b400a20c4f38ba38ca29f06e26a0f2d49a70bad9e57615dac09c35accfddf1bb90d23cd3e0a0bab892 + checksum: ffe36bb4f4a99ad13c426a98c3b508d70736036cae4e471d9c862e3a579847ed4f480686af0fce2633f6f7c0f0d3bf02da73da36e7edd3fde0b2061951dcba9a languageName: node linkType: hard @@ -1336,6 +1336,13 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:1.5.0": + version: 1.5.0 + resolution: "@colors/colors@npm:1.5.0" + checksum: d64d5260bed1d5012ae3fc617d38d1afc0329fec05342f4e6b838f46998855ba56e0a73833f4a80fa8378c84810da254f76a8a19c39d038260dc06dc4e007425 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -1345,6 +1352,42 @@ __metadata: languageName: node linkType: hard +"@cypress/request@npm:^2.88.10": + version: 2.88.11 + resolution: "@cypress/request@npm:2.88.11" + dependencies: + aws-sign2: ~0.7.0 + aws4: ^1.8.0 + caseless: ~0.12.0 + combined-stream: ~1.0.6 + extend: ~3.0.2 + forever-agent: ~0.6.1 + form-data: ~2.3.2 + http-signature: ~1.3.6 + is-typedarray: ~1.0.0 + isstream: ~0.1.2 + json-stringify-safe: ~5.0.1 + mime-types: ~2.1.19 + performance-now: ^2.1.0 + qs: ~6.10.3 + safe-buffer: ^5.1.2 + tough-cookie: ~2.5.0 + tunnel-agent: ^0.6.0 + uuid: ^8.3.2 + checksum: e4b3f62e0c41c4ccca6c942828461d8ea717e752fd918d685e9f74e2ebcfa8b7942427f7ce971e502635c3bf3d40011476db84dc753d3dc360c6d08350da6f93 + languageName: node + linkType: hard + +"@cypress/xvfb@npm:^1.2.4": + version: 1.2.4 + resolution: "@cypress/xvfb@npm:1.2.4" + dependencies: + debug: ^3.1.0 + lodash.once: ^4.1.1 + checksum: 7bdcdaeb1bb692ec9d9bf8ec52538aa0bead6764753f4a067a171a511807a43fab016f7285a56bef6a606c2467ff3f1365e1ad2d2d583b81beed849ee1573fd1 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -1394,6 +1437,94 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/merge@npm:8.3.1": + version: 8.3.1 + resolution: "@graphql-tools/merge@npm:8.3.1" + dependencies: + "@graphql-tools/utils": 8.9.0 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 16af6be2249f4f500a4c2f5d3db2e0efd56ad69b5e10499649c6fc979c257af12e131112304a16699654b54daab37a80737e0538478bc45a0053b9bc859a7ac1 + languageName: node + linkType: hard + +"@graphql-tools/schema@npm:^8.5.0": + version: 8.5.1 + resolution: "@graphql-tools/schema@npm:8.5.1" + dependencies: + "@graphql-tools/merge": 8.3.1 + "@graphql-tools/utils": 8.9.0 + tslib: ^2.4.0 + value-or-promise: 1.0.11 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 91363cd4371e347af40ef66f7d903b5d4f5998bfaec9214768e6a795136ef6372f9f225e05e18daacd929e23695811f15e791c6cbe082bf5b5d03b16b1f874f8 + languageName: node + linkType: hard + +"@graphql-tools/utils@npm:8.9.0": + version: 8.9.0 + resolution: "@graphql-tools/utils@npm:8.9.0" + dependencies: + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 8d1d8a11722e211dc8723cd3fd7a97fa5401ab22146e4240a0f9d45547792476c34814ff914524578beec961db7b0ff23a6ddff8fe059764537e594cff35c906 + languageName: node + linkType: hard + +"@graphql-tools/utils@npm:^8.8.0": + version: 8.13.1 + resolution: "@graphql-tools/utils@npm:8.13.1" + dependencies: + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: ff04fdeb29e9ac596ea53386cd5b23cd741bb14c1997c6b0ba3c34ca165bd82b528a355e8c8e2ba726eb39e833ba9cbb0851ba0addb8c6d367089a1145bf9a49 + languageName: node + linkType: hard + +"@httptoolkit/httpolyglot@npm:^2.1.1": + version: 2.1.1 + resolution: "@httptoolkit/httpolyglot@npm:2.1.1" + dependencies: + "@types/node": ^16.7.10 + checksum: 138ccd61355de334c509e2fc4ac9ade9e1aa6aa770ed2271e0bd1d883ed815eb742d0a4de37837edd03a9a243c05d6da32c5febe970f4518c46e2d76e6ff10d5 + languageName: node + linkType: hard + +"@httptoolkit/subscriptions-transport-ws@npm:^0.11.2": + version: 0.11.2 + resolution: "@httptoolkit/subscriptions-transport-ws@npm:0.11.2" + dependencies: + backo2: ^1.0.2 + eventemitter3: ^3.1.0 + iterall: ^1.2.1 + symbol-observable: ^1.0.4 + ws: ^8.8.0 + peerDependencies: + graphql: ^15.7.2 || ^16.0.0 + checksum: a2d99b4d8e46b46fd5d4fac3456fa685dba7d876908e632c73af014fdcc92ae1f77f8c542e8b63ae747a164e9d2e4be95c5046665f9e7b5622f02dc6d7d04549 + languageName: node + linkType: hard + +"@httptoolkit/websocket-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "@httptoolkit/websocket-stream@npm:6.0.1" + dependencies: + "@types/ws": "*" + duplexify: ^3.5.1 + inherits: ^2.0.1 + isomorphic-ws: ^4.0.1 + readable-stream: ^2.3.3 + safe-buffer: ^5.1.2 + ws: "*" + xtend: ^4.0.0 + checksum: e70059c24499abab695e7bc269aefc1a751d161296975a4af932577497c4ecd66b7745dc0c63608e06989442db996d76e563bce08156563bac7bc3411ad9bcee + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.8": version: 0.11.8 resolution: "@humanwhocodes/config-array@npm:0.11.8" @@ -1679,14 +1810,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.2 - resolution: "@jridgewell/gen-mapping@npm:0.3.2" +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" dependencies: "@jridgewell/set-array": ^1.0.1 "@jridgewell/sourcemap-codec": ^1.4.10 "@jridgewell/trace-mapping": ^0.3.9 - checksum: 1832707a1c476afebe4d0fbbd4b9434fdb51a4c3e009ab1e9938648e21b7a97049fa6009393bdf05cab7504108413441df26d8a3c12193996e65493a4efb6882 + checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab languageName: node linkType: hard @@ -1711,6 +1842,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/source-map@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/source-map@npm:0.3.3" + dependencies: + "@jridgewell/gen-mapping": ^0.3.0 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: ae1302146339667da5cd6541260ecbef46ae06819a60f88da8f58b3e64682f787c09359933d050dea5d2173ea7fa40f40dd4d4e7a8d325c5892cccd99aaf8959 + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:1.4.14": version: 1.4.14 resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" @@ -1874,7 +2015,7 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-typescript@npm:^11.1.0, @rollup/plugin-typescript@npm:^11.1.1": +"@rollup/plugin-typescript@npm:^11.1.1": version: 11.1.1 resolution: "@rollup/plugin-typescript@npm:11.1.1" dependencies: @@ -1926,11 +2067,18 @@ __metadata: linkType: hard "@sinonjs/fake-timers@npm:^10.0.2": - version: 10.1.0 - resolution: "@sinonjs/fake-timers@npm:10.1.0" + version: 10.2.0 + resolution: "@sinonjs/fake-timers@npm:10.2.0" dependencies: "@sinonjs/commons": ^3.0.0 - checksum: f8f7e23a136e32ba0128493207e4223f453e033471257a971acb43840927e738a0838004b1e4fa046279609762a2dd8d700606616e9264dc3891c4f8d45889a2 + checksum: 586c76e1dd90d03b0c4e754f2011325b38ac6055878c81c52434c900f36d9d245438c96ef69e08e28d9fbecf2335fb347b67850962d8b6e539dd7359d8c62802 + languageName: node + linkType: hard + +"@tootallnate/once@npm:1": + version: 1.1.2 + resolution: "@tootallnate/once@npm:1.1.2" + checksum: e1fb1bbbc12089a0cb9433dc290f97bddd062deadb6178ce9bcb93bb7c1aecde5e60184bc7065aec42fe1663622a213493c48bbd4972d931aae48315f18e1be9 languageName: node linkType: hard @@ -2028,6 +2176,15 @@ __metadata: languageName: node linkType: hard +"@types/cors@npm:^2.8.6": + version: 2.8.13 + resolution: "@types/cors@npm:2.8.13" + dependencies: + "@types/node": "*" + checksum: 7ef197ea19d2e5bf1313b8416baa6f3fd6dd887fd70191da1f804f557395357dafd8bc8bed0ac60686923406489262a7c8a525b55748f7b2b8afa686700de907 + languageName: node + linkType: hard + "@types/debug@npm:^4.1.7": version: 4.1.7 resolution: "@types/debug@npm:4.1.7" @@ -2044,6 +2201,33 @@ __metadata: languageName: node linkType: hard +"@types/es6-promisify@npm:^6.0.1": + version: 6.0.1 + resolution: "@types/es6-promisify@npm:6.0.1" + checksum: a717113805ed9d35f02f3fdf9a4c00b0589ca32e8a66bd22b950418e21900fda060a5578fa9ad37bd7c62f17ed327bd7570cea5dac522254d720cc1a861ecf12 + languageName: node + linkType: hard + +"@types/eslint-scope@npm:^3.7.3": + version: 3.7.4 + resolution: "@types/eslint-scope@npm:3.7.4" + dependencies: + "@types/eslint": "*" + "@types/estree": "*" + checksum: ea6a9363e92f301cd3888194469f9ec9d0021fe0a397a97a6dd689e7545c75de0bd2153dfb13d3ab532853a278b6572c6f678ce846980669e41029d205653460 + languageName: node + linkType: hard + +"@types/eslint@npm:*": + version: 8.40.0 + resolution: "@types/eslint@npm:8.40.0" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: bab41d7f590182e743853cdd5bf5359cbc4240df986223457c8a5f5674743a3fe2a8626704b65bf9121dfa0ce0a0efd760da8339cc329018f229d4d2d6ee1c43 + languageName: node + linkType: hard + "@types/estree@npm:*, @types/estree@npm:^1.0.0": version: 1.0.1 resolution: "@types/estree@npm:1.0.1" @@ -2094,7 +2278,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:25.1.0 - 29": +"@types/jest@npm:25.1.0 - 29, @types/jest@npm:^29.5.2": version: 29.5.2 resolution: "@types/jest@npm:29.5.2" dependencies: @@ -2111,10 +2295,10 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.9": - version: 7.0.9 - resolution: "@types/json-schema@npm:7.0.9" - checksum: 259d0e25f11a21ba5c708f7ea47196bd396e379fddb79c76f9f4f62c945879dc21657904914313ec2754e443c5018ea8372362f323f30e0792897fdb2098a705 +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": + version: 7.0.12 + resolution: "@types/json-schema@npm:7.0.12" + checksum: 00239e97234eeb5ceefb0c1875d98ade6e922bfec39dd365ec6bd360b5c2f825e612ac4f6e5f1d13601b8b30f378f15e6faa805a3a732f4a1bbe61915163d293 languageName: node linkType: hard @@ -2125,6 +2309,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.14.194": + version: 4.14.194 + resolution: "@types/lodash@npm:4.14.194" + checksum: 113f34831c461469d91feca2dde737f88487732898b4d25e9eb23b087bb193985f864d1e1e0f3b777edc5022e460443588b6000a3b2348c966f72d17eedc35ea + languageName: node + linkType: hard + "@types/ms@npm:*": version: 0.7.31 resolution: "@types/ms@npm:0.7.31" @@ -2142,7 +2333,14 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^14.18.43": +"@types/node@npm:*, @types/node@npm:^16.7.10": + version: 16.18.34 + resolution: "@types/node@npm:16.18.34" + checksum: 35c0ffe09687578d002ceb7e706d0ba450546aeb3d2716f28691f2af0063bd274dbde0f741d087ea217f2a8db413eb700d22dfb4f08a67986ff801423bd7be8d + languageName: node + linkType: hard + +"@types/node@npm:^14.14.31, @types/node@npm:^14.18.43": version: 14.18.43 resolution: "@types/node@npm:14.18.43" checksum: 3ef43eda265c346597bc5c8329605255550f7a92300a76e9e3c48e07fd685b4a48d47465997bf6f1a68b235c7339e6e583d73515769efcad70e4f8f4c7f47fd4 @@ -2156,6 +2354,13 @@ __metadata: languageName: node linkType: hard +"@types/path-browserify@npm:^1.0.0": + version: 1.0.0 + resolution: "@types/path-browserify@npm:1.0.0" + checksum: e3f98ef9638fb75dc7547c3cde5fa86bc5986e38403ec91234abcc7b911e30711af7c6abc674d617d27c4a6c09e7f8e9d77662b1cc45d00d4708d94764202b61 + languageName: node + linkType: hard + "@types/prettier@npm:^2.1.5": version: 2.4.4 resolution: "@types/prettier@npm:2.4.4" @@ -2163,6 +2368,33 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.5 + resolution: "@types/prop-types@npm:15.7.5" + checksum: 5b43b8b15415e1f298243165f1d44390403bb2bd42e662bca3b5b5633fdd39c938e91b7fce3a9483699db0f7a715d08cef220c121f723a634972fdf596aec980 + languageName: node + linkType: hard + +"@types/react-dom@npm:^18.2.4": + version: 18.2.4 + resolution: "@types/react-dom@npm:18.2.4" + dependencies: + "@types/react": "*" + checksum: 8301f35cf1cbfec8c723e9477aecf87774e3c168bd457d353b23c45064737213d3e8008b067c6767b7b08e4f2b3823ee239242a6c225fc91e7f8725ef8734124 + languageName: node + linkType: hard + +"@types/react@npm:*, @types/react@npm:^18.2.7": + version: 18.2.7 + resolution: "@types/react@npm:18.2.7" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: caa5da4cf929766738ec789301dc6fb6624bd48dd317d851c4c9b84b1f47cd8ebe17fe01398cadaa0bc938cd4d502d67f4b9de9ff771dc132096bdc86228efba + languageName: node + linkType: hard + "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -2177,6 +2409,13 @@ __metadata: languageName: node linkType: hard +"@types/scheduler@npm:*": + version: 0.16.3 + resolution: "@types/scheduler@npm:0.16.3" + checksum: 2b0aec39c24268e3ce938c5db2f2e77f5c3dd280e05c262d9c2fe7d890929e4632a6b8e94334017b66b45e4f92a5aa42ba3356640c2a1175fa37bef2f5200767 + languageName: node + linkType: hard + "@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.0 resolution: "@types/semver@npm:7.5.0" @@ -2184,6 +2423,20 @@ __metadata: languageName: node linkType: hard +"@types/sinonjs__fake-timers@npm:8.1.1": + version: 8.1.1 + resolution: "@types/sinonjs__fake-timers@npm:8.1.1" + checksum: ca09d54d47091d87020824a73f026300fa06b17cd9f2f9b9387f28b549364b141ef194ee28db762f6588de71d8febcd17f753163cb7ea116b8387c18e80ebd5c + languageName: node + linkType: hard + +"@types/sizzle@npm:^2.3.2": + version: 2.3.3 + resolution: "@types/sizzle@npm:2.3.3" + checksum: 586a9fb1f6ff3e325e0f2cc1596a460615f0bc8a28f6e276ac9b509401039dd242fa8b34496d3a30c52f5b495873922d09a9e76c50c2ab2bcc70ba3fb9c4e160 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.1 resolution: "@types/stack-utils@npm:2.0.1" @@ -2200,6 +2453,22 @@ __metadata: languageName: node linkType: hard +"@types/tmp@npm:^0.2.3": + version: 0.2.3 + resolution: "@types/tmp@npm:0.2.3" + checksum: 0ca45e99b3b3c6959d5c4f4555f73c8007db540cfb0fbbb9373217f9ab85e67eef75193f51a1d6564b0ee6c6f5ffa259d1034d7f7530a5b7ce80acb94cfc4daa + languageName: node + linkType: hard + +"@types/ws@npm:*": + version: 8.5.4 + resolution: "@types/ws@npm:8.5.4" + dependencies: + "@types/node": "*" + checksum: fefbad20d211929bb996285c4e6f699b12192548afedbe4930ab4384f8a94577c9cd421acaad163cacd36b88649509970a05a0b8f20615b30c501ed5269038d1 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -2207,7 +2476,7 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.8": +"@types/yargs@npm:^17.0.24, @types/yargs@npm:^17.0.8": version: 17.0.24 resolution: "@types/yargs@npm:17.0.24" dependencies: @@ -2216,6 +2485,15 @@ __metadata: languageName: node linkType: hard +"@types/yauzl@npm:^2.9.1": + version: 2.10.0 + resolution: "@types/yauzl@npm:2.10.0" + dependencies: + "@types/node": "*" + checksum: 55d27ae5d346ea260e40121675c24e112ef0247649073848e5d4e03182713ae4ec8142b98f61a1c6cbe7d3b72fa99bbadb65d8b01873e5e605cdc30f1ff70ef2 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.59.1": version: 5.59.1 resolution: "@typescript-eslint/eslint-plugin@npm:5.59.1" @@ -2337,6 +2615,55 @@ __metadata: languageName: node linkType: hard +"@unflakable/cypress-plugin@workspace:^, @unflakable/cypress-plugin@workspace:packages/cypress-plugin": + version: 0.0.0-use.local + resolution: "@unflakable/cypress-plugin@workspace:packages/cypress-plugin" + dependencies: + "@rollup/plugin-commonjs": ^24.1.0 + "@rollup/plugin-json": ^6.0.0 + "@rollup/plugin-node-resolve": ^15.0.2 + "@rollup/plugin-typescript": ^11.1.1 + "@types/debug": ^4.1.7 + "@types/deep-equal": ^1.0.1 + "@types/es6-promisify": ^6.0.1 + "@types/lodash": ^4.14.194 + "@types/path-browserify": ^1.0.0 + "@types/tmp": ^0.2.3 + "@types/yargs": ^17.0.24 + "@unflakable/js-api": "workspace:^" + "@unflakable/plugins-common": "workspace:" + ansi-styles: ^4.3.0 + chalk: ^4.1.0 + cli-table3: 0.5.1 + cosmiconfig: ^7.0.1 + cross-env: ^7.0.3 + cypress: 10 - 12 + cypress-multi-reporters: ^1.6.3 + dayjs: ^1.10.4 + debug: ^4.3.3 + deep-equal: ^2.0.5 + es6-promisify: ^7.0.0 + jest: ^29.5.0 + jest-environment-node: ^29.5.0 + lodash: ^4.17.21 + log-symbols: ^4.1.0 + mocha: =7.0.1 + ms: 2.1.1 + path-browserify: ^1.0.1 + rollup: ^3.21.1 + simple-git: ^3.16.0 + term-size: 2.1.0 + tmp: ~0.2.1 + typescript: ^4.9.5 + widest-line: 3.1.0 + yargs: ^17.7.2 + peerDependencies: + cypress: 10 - 12 + bin: + cypress-unflakable: ./dist/main.js + languageName: unknown + linkType: soft + "@unflakable/jest-plugin@workspace:^, @unflakable/jest-plugin@workspace:packages/jest-plugin": version: 0.0.0-use.local resolution: "@unflakable/jest-plugin@workspace:packages/jest-plugin" @@ -2386,7 +2713,7 @@ __metadata: languageName: unknown linkType: soft -"@unflakable/plugins-common@workspace:^, @unflakable/plugins-common@workspace:packages/plugins-common": +"@unflakable/plugins-common@workspace:, @unflakable/plugins-common@workspace:^, @unflakable/plugins-common@workspace:packages/plugins-common": version: 0.0.0-use.local resolution: "@unflakable/plugins-common@workspace:packages/plugins-common" dependencies: @@ -2397,6 +2724,171 @@ __metadata: languageName: unknown linkType: soft +"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/ast@npm:1.11.6" + dependencies: + "@webassemblyjs/helper-numbers": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + checksum: 38ef1b526ca47c210f30975b06df2faf1a8170b1636ce239fc5738fc231ce28389dd61ecedd1bacfc03cbe95b16d1af848c805652080cb60982836eb4ed2c6cf + languageName: node + linkType: hard + +"@webassemblyjs/floating-point-hex-parser@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.11.6" + checksum: 29b08758841fd8b299c7152eda36b9eb4921e9c584eb4594437b5cd90ed6b920523606eae7316175f89c20628da14326801090167cc7fbffc77af448ac84b7e2 + languageName: node + linkType: hard + +"@webassemblyjs/helper-api-error@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-api-error@npm:1.11.6" + checksum: e8563df85161096343008f9161adb138a6e8f3c2cc338d6a36011aa55eabb32f2fd138ffe63bc278d009ada001cc41d263dadd1c0be01be6c2ed99076103689f + languageName: node + linkType: hard + +"@webassemblyjs/helper-buffer@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-buffer@npm:1.11.6" + checksum: b14d0573bf680d22b2522e8a341ec451fddd645d1f9c6bd9012ccb7e587a2973b86ab7b89fe91e1c79939ba96095f503af04369a3b356c8023c13a5893221644 + languageName: node + linkType: hard + +"@webassemblyjs/helper-numbers@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-numbers@npm:1.11.6" + dependencies: + "@webassemblyjs/floating-point-hex-parser": 1.11.6 + "@webassemblyjs/helper-api-error": 1.11.6 + "@xtuc/long": 4.2.2 + checksum: f4b562fa219f84368528339e0f8d273ad44e047a07641ffcaaec6f93e5b76fd86490a009aa91a294584e1436d74b0a01fa9fde45e333a4c657b58168b04da424 + languageName: node + linkType: hard + +"@webassemblyjs/helper-wasm-bytecode@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.11.6" + checksum: 3535ef4f1fba38de3475e383b3980f4bbf3de72bbb631c2b6584c7df45be4eccd62c6ff48b5edd3f1bcff275cfd605a37679ec199fc91fd0a7705d7f1e3972dc + languageName: node + linkType: hard + +"@webassemblyjs/helper-wasm-section@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-section@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/helper-buffer": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/wasm-gen": 1.11.6 + checksum: b2cf751bf4552b5b9999d27bbb7692d0aca75260140195cb58ea6374d7b9c2dc69b61e10b211a0e773f66209c3ddd612137ed66097e3684d7816f854997682e9 + languageName: node + linkType: hard + +"@webassemblyjs/ieee754@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/ieee754@npm:1.11.6" + dependencies: + "@xtuc/ieee754": ^1.2.0 + checksum: 13574b8e41f6ca39b700e292d7edf102577db5650fe8add7066a320aa4b7a7c09a5056feccac7a74eb68c10dea9546d4461412af351f13f6b24b5f32379b49de + languageName: node + linkType: hard + +"@webassemblyjs/leb128@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/leb128@npm:1.11.6" + dependencies: + "@xtuc/long": 4.2.2 + checksum: 7ea942dc9777d4b18a5ebfa3a937b30ae9e1d2ce1fee637583ed7f376334dd1d4274f813d2e250056cca803e0952def4b954913f1a3c9068bcd4ab4ee5143bf0 + languageName: node + linkType: hard + +"@webassemblyjs/utf8@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/utf8@npm:1.11.6" + checksum: 807fe5b5ce10c390cfdd93e0fb92abda8aebabb5199980681e7c3743ee3306a75729bcd1e56a3903980e96c885ee53ef901fcbaac8efdfa480f9c0dae1d08713 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-edit@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-edit@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/helper-buffer": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/helper-wasm-section": 1.11.6 + "@webassemblyjs/wasm-gen": 1.11.6 + "@webassemblyjs/wasm-opt": 1.11.6 + "@webassemblyjs/wasm-parser": 1.11.6 + "@webassemblyjs/wast-printer": 1.11.6 + checksum: 29ce75870496d6fad864d815ebb072395a8a3a04dc9c3f4e1ffdc63fc5fa58b1f34304a1117296d8240054cfdbc38aca88e71fb51483cf29ffab0a61ef27b481 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-gen@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-gen@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/ieee754": 1.11.6 + "@webassemblyjs/leb128": 1.11.6 + "@webassemblyjs/utf8": 1.11.6 + checksum: a645a2eecbea24833c3260a249704a7f554ef4a94c6000984728e94bb2bc9140a68dfd6fd21d5e0bbb09f6dfc98e083a45760a83ae0417b41a0196ff6d45a23a + languageName: node + linkType: hard + +"@webassemblyjs/wasm-opt@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-opt@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/helper-buffer": 1.11.6 + "@webassemblyjs/wasm-gen": 1.11.6 + "@webassemblyjs/wasm-parser": 1.11.6 + checksum: b4557f195487f8e97336ddf79f7bef40d788239169aac707f6eaa2fa5fe243557c2d74e550a8e57f2788e70c7ae4e7d32f7be16101afe183d597b747a3bdd528 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-parser@npm:1.11.6, @webassemblyjs/wasm-parser@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-parser@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": 1.11.6 + "@webassemblyjs/helper-api-error": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/ieee754": 1.11.6 + "@webassemblyjs/leb128": 1.11.6 + "@webassemblyjs/utf8": 1.11.6 + checksum: 8200a8d77c15621724a23fdabe58d5571415cda98a7058f542e670ea965dd75499f5e34a48675184947c66f3df23adf55df060312e6d72d57908e3f049620d8a + languageName: node + linkType: hard + +"@webassemblyjs/wast-printer@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wast-printer@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": 1.11.6 + "@xtuc/long": 4.2.2 + checksum: d2fa6a4c427325ec81463e9c809aa6572af6d47f619f3091bf4c4a6fc34f1da3df7caddaac50b8e7a457f8784c62cd58c6311b6cb69b0162ccd8d4c072f79cf8 + languageName: node + linkType: hard + +"@xtuc/ieee754@npm:^1.2.0": + version: 1.2.0 + resolution: "@xtuc/ieee754@npm:1.2.0" + checksum: ac56d4ca6e17790f1b1677f978c0c6808b1900a5b138885d3da21732f62e30e8f0d9120fcf8f6edfff5100ca902b46f8dd7c1e3f903728634523981e80e2885a + languageName: node + linkType: hard + +"@xtuc/long@npm:4.2.2": + version: 4.2.2 + resolution: "@xtuc/long@npm:4.2.2" + checksum: 8ed0d477ce3bc9c6fe2bf6a6a2cc316bb9c4127c5a7827bae947fa8ec34c7092395c5a283cc300c05b5fa01cbbfa1f938f410a7bf75db7c7846fea41949989ec + languageName: node + linkType: hard + "abbrev@npm:1": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -2404,6 +2896,25 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^1.3.7, accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: ~2.1.34 + negotiator: 0.6.3 + checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 + languageName: node + linkType: hard + +"acorn-import-assertions@npm:^1.9.0": + version: 1.9.0 + resolution: "acorn-import-assertions@npm:1.9.0" + peerDependencies: + acorn: ^8 + checksum: 944fb2659d0845c467066bdcda2e20c05abe3aaf11972116df457ce2627628a81764d800dd55031ba19de513ee0d43bb771bc679cc0eda66dc8b4fade143bc0c + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -2413,14 +2924,14 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.1.1": +"acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0": version: 8.2.0 resolution: "acorn-walk@npm:8.2.0" checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 languageName: node linkType: hard -"acorn@npm:^8.4.1, acorn@npm:^8.8.0": +"acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0": version: 8.8.2 resolution: "acorn@npm:8.8.2" bin: @@ -2459,7 +2970,16 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.12.4": +"ajv-keywords@npm:^3.5.2": + version: 3.5.2 + resolution: "ajv-keywords@npm:3.5.2" + peerDependencies: + ajv: ^6.9.1 + checksum: 7dc5e5931677a680589050f79dcbe1fefbb8fea38a955af03724229139175b433c63c68f7ae5f86cf8f65d55eb7c25f75a046723e2e58296707617ca690feae9 + languageName: node + linkType: hard + +"ajv@npm:^6.10.0, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -2471,15 +2991,43 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1": - version: 4.3.2 - resolution: "ansi-escapes@npm:4.3.2" - dependencies: - type-fest: ^0.21.3 +"ansi-colors@npm:3.2.3": + version: 3.2.3 + resolution: "ansi-colors@npm:3.2.3" + checksum: 018a92fbf8b143feb9e00559655072598902ff2cdfa07dbe24b933c70ae04845e3dda2c091ab128920fc50b3db06c3f09947f49fcb287d53beb6c5869b8bb32b + languageName: node + linkType: hard + +"ansi-colors@npm:^4.1.1": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: a9c2ec842038a1fabc7db9ece7d3177e2fe1c5dc6f0c51ecfbf5f39911427b89c00b5dc6b8bd95f82a26e9b16aaae2e83d45f060e98070ce4d1333038edceb0e + languageName: node + linkType: hard + +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: ^0.21.3 checksum: 93111c42189c0a6bed9cdb4d7f2829548e943827ee8479c74d6e0b22ee127b2a21d3f8b5ca57723b8ef78ce011fbfc2784350eb2bde3ccfccf2f575fa8489815 languageName: node linkType: hard +"ansi-regex@npm:^3.0.0": + version: 3.0.1 + resolution: "ansi-regex@npm:3.0.1" + checksum: 09daf180c5f59af9850c7ac1bd7fda85ba596cc8cbeb210826e90755f06c818af86d9fa1e6e8322fab2c3b9e9b03f56c537b42241139f824dd75066a1e7257cc + languageName: node + linkType: hard + +"ansi-regex@npm:^4.1.0": + version: 4.1.1 + resolution: "ansi-regex@npm:4.1.1" + checksum: b1a6ee44cb6ecdabaa770b2ed500542714d4395d71c7e5c25baa631f680fb2ad322eb9ba697548d498a6fd366949fc8b5bfcf48d49a32803611f648005b01888 + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -2487,7 +3035,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": +"ansi-styles@npm:^3.2.0, ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" dependencies: @@ -2496,7 +3044,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0, ansi-styles@npm:^4.3.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" dependencies: @@ -2512,7 +3060,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3": +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.1": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -2529,6 +3077,13 @@ __metadata: languageName: node linkType: hard +"arch@npm:^2.2.0": + version: 2.2.0 + resolution: "arch@npm:2.2.0" + checksum: e21b7635029fe8e9cdd5a026f9a6c659103e63fff423834323cdf836a1bb240a72d0c39ca8c470f84643385cf581bd8eda2cad8bf493e27e54bd9783abe9101f + languageName: node + linkType: hard + "are-we-there-yet@npm:^3.0.0": version: 3.0.0 resolution: "are-we-there-yet@npm:3.0.0" @@ -2572,6 +3127,13 @@ __metadata: languageName: node linkType: hard +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: a9925bf3512d9dce202112965de90c222cd59a4fbfce68a0951d25d965cf44642931f40aac72309c41f12df19afa010ecadceb07cfff9ccc1621e99d89ab5f3b + languageName: node + linkType: hard + "array-includes@npm:^3.1.6": version: 3.1.6 resolution: "array-includes@npm:3.1.6" @@ -2616,6 +3178,51 @@ __metadata: languageName: node linkType: hard +"array.prototype.reduce@npm:^1.0.5": + version: 1.0.5 + resolution: "array.prototype.reduce@npm:1.0.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.20.4 + es-array-method-boxes-properly: ^1.0.0 + is-string: ^1.0.7 + checksum: f44691395f9202aba5ec2446468d4c27209bfa81464f342ae024b7157dbf05b164e47cca01250b8c7c2a8219953fb57651cca16aab3d16f43b85c0d92c26eef3 + languageName: node + linkType: hard + +"asn1@npm:~0.2.3": + version: 0.2.6 + resolution: "asn1@npm:0.2.6" + dependencies: + safer-buffer: ~2.1.0 + checksum: 39f2ae343b03c15ad4f238ba561e626602a3de8d94ae536c46a4a93e69578826305366dc09fbb9b56aec39b4982a463682f259c38e59f6fa380cd72cd61e493d + languageName: node + linkType: hard + +"assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": + version: 1.0.0 + resolution: "assert-plus@npm:1.0.0" + checksum: 19b4340cb8f0e6a981c07225eacac0e9d52c2644c080198765d63398f0075f83bbc0c8e95474d54224e297555ad0d631c1dcd058adb1ddc2437b41a6b424ac64 + languageName: node + linkType: hard + +"ast-types@npm:^0.13.2": + version: 0.13.4 + resolution: "ast-types@npm:0.13.4" + dependencies: + tslib: ^2.0.1 + checksum: 5a51f7b70588ecced3601845a0e203279ca2f5fdc184416a0a1640c93ec0a267241d6090a328e78eebb8de81f8754754e0a4f1558ba2a3d638f8ccbd0b1f0eff + languageName: node + linkType: hard + +"astral-regex@npm:^2.0.0": + version: 2.0.0 + resolution: "astral-regex@npm:2.0.0" + checksum: 876231688c66400473ba505731df37ea436e574dd524520294cc3bbc54ea40334865e01fa0d074d74d036ee874ee7e62f486ea38bc421ee8e6a871c06f011766 + languageName: node + linkType: hard + "async-retry@npm:^1.3.3": version: 1.3.3 resolution: "async-retry@npm:1.3.3" @@ -2625,6 +3232,22 @@ __metadata: languageName: node linkType: hard +"async@npm:^2.6.2": + version: 2.6.4 + resolution: "async@npm:2.6.4" + dependencies: + lodash: ^4.17.14 + checksum: a52083fb32e1ebe1d63e5c5624038bb30be68ff07a6c8d7dfe35e47c93fc144bd8652cbec869e0ac07d57dde387aa5f1386be3559cdee799cb1f789678d88e19 + languageName: node + linkType: hard + +"async@npm:^3.2.0": + version: 3.2.4 + resolution: "async@npm:3.2.4" + checksum: 43d07459a4e1d09b84a20772414aa684ff4de085cbcaec6eea3c7a8f8150e8c62aa6cd4e699fe8ee93c3a5b324e777d34642531875a0817a35697522c1b02e89 + languageName: node + linkType: hard + "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -2632,6 +3255,13 @@ __metadata: languageName: node linkType: hard +"at-least-node@npm:^1.0.0": + version: 1.0.0 + resolution: "at-least-node@npm:1.0.0" + checksum: 463e2f8e43384f1afb54bc68485c436d7622acec08b6fad269b421cb1d29cebb5af751426793d0961ed243146fe4dc983402f6d5a51b720b277818dbf6f2e49e + languageName: node + linkType: hard + "available-typed-arrays@npm:^1.0.5": version: 1.0.5 resolution: "available-typed-arrays@npm:1.0.5" @@ -2639,6 +3269,20 @@ __metadata: languageName: node linkType: hard +"aws-sign2@npm:~0.7.0": + version: 0.7.0 + resolution: "aws-sign2@npm:0.7.0" + checksum: b148b0bb0778098ad8cf7e5fc619768bcb51236707ca1d3e5b49e41b171166d8be9fdc2ea2ae43d7decf02989d0aaa3a9c4caa6f320af95d684de9b548a71525 + languageName: node + linkType: hard + +"aws4@npm:^1.8.0": + version: 1.12.0 + resolution: "aws4@npm:1.12.0" + checksum: 68f79708ac7c335992730bf638286a3ee0a645cf12575d557860100767c500c08b30e24726b9f03265d74116417f628af78509e1333575e9f8d52a80edfe8cbc + languageName: node + linkType: hard + "babel-jest@npm:^29.5.0": version: 29.5.0 resolution: "babel-jest@npm:29.5.0" @@ -2751,6 +3395,13 @@ __metadata: languageName: node linkType: hard +"backo2@npm:^1.0.2": + version: 1.0.2 + resolution: "backo2@npm:1.0.2" + checksum: fda8d0a0f4810068d23715f2f45153146d6ee8f62dd827ce1e0b6cc3c8328e84ad61e11399a83931705cef702fe7cbb457856bf99b9bd10c4ed57b0786252385 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -2758,6 +3409,90 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^0.1.5": + version: 0.1.5 + resolution: "base64-arraybuffer@npm:0.1.5" + checksum: 44588c1b4460faf59643cf3bcf346a7ede9df70d97aec6dbee4fbae15f6b6220d679b8db076771ea4ef5713dd710e7db7a4a3f81bbb04c71fb06764697d9a021 + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"bcrypt-pbkdf@npm:^1.0.0": + version: 1.0.2 + resolution: "bcrypt-pbkdf@npm:1.0.2" + dependencies: + tweetnacl: ^0.14.3 + checksum: 4edfc9fe7d07019609ccf797a2af28351736e9d012c8402a07120c4453a3b789a15f2ee1530dc49eee8f7eb9379331a8dd4b3766042b9e502f74a68e7f662291 + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.2.0 + resolution: "binary-extensions@npm:2.2.0" + checksum: ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8 + languageName: node + linkType: hard + +"blob-util@npm:^2.0.2": + version: 2.0.2 + resolution: "blob-util@npm:2.0.2" + checksum: d543e6b92e4ca715ca33c78e89a07a2290d43e5b2bc897d7ec588c5c7bbf59df93e45225ac0c9258aa6ce4320358990f99c9288f1c48280f8ec5d7a2e088d19b + languageName: node + linkType: hard + +"bluebird@npm:^3.7.2": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef + languageName: node + linkType: hard + +"body-parser@npm:1.20.1": + version: 1.20.1 + resolution: "body-parser@npm:1.20.1" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.4 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: ~1.6.18 + unpipe: 1.0.0 + checksum: f1050dbac3bede6a78f0b87947a8d548ce43f91ccc718a50dd774f3c81f2d8b04693e52acf62659fad23101827dd318da1fb1363444ff9a8482b886a3e4a5266 + languageName: node + linkType: hard + +"body-parser@npm:^1.15.2": + version: 1.20.2 + resolution: "body-parser@npm:1.20.2" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: ~1.6.18 + unpipe: 1.0.0 + checksum: 14d37ec638ab5c93f6099ecaed7f28f890d222c650c69306872e00b9efa081ff6c596cd9afb9930656aae4d6c4e1c17537bea12bb73c87a217cb3cfea8896737 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -2777,7 +3512,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.1": +"braces@npm:^3.0.2, braces@npm:~3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" dependencies: @@ -2786,7 +3521,21 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.21.3, browserslist@npm:^4.21.5": +"brotli-wasm@npm:^1.1.0": + version: 1.3.1 + resolution: "brotli-wasm@npm:1.3.1" + checksum: ec2931a989ee6f0bb52c2aabf23a0d230232d3bd69fb68ee3dab9542fc9ae2d4085d0e5338f71520c25a4a26cf1cfc991ce02910c24d63d42c7915c5722a3713 + languageName: node + linkType: hard + +"browser-stdout@npm:1.3.1": + version: 1.3.1 + resolution: "browser-stdout@npm:1.3.1" + checksum: b717b19b25952dd6af483e368f9bcd6b14b87740c3d226c2977a65e84666ffd67000bddea7d911f111a9b6ddc822b234de42d52ab6507bce4119a4cc003ef7b3 + languageName: node + linkType: hard + +"browserslist@npm:^4.14.5, browserslist@npm:^4.21.3, browserslist@npm:^4.21.5": version: 4.21.5 resolution: "browserslist@npm:4.21.5" dependencies: @@ -2800,6 +3549,15 @@ __metadata: languageName: node linkType: hard +"bs-logger@npm:0.x": + version: 0.2.6 + resolution: "bs-logger@npm:0.2.6" + dependencies: + fast-json-stable-stringify: 2.x + checksum: d34bdaf68c64bd099ab97c3ea608c9ae7d3f5faa1178b3f3f345acd94e852e608b2d4f9103fb2e503f5e69780e98293df41691b84be909b41cf5045374d54606 + languageName: node + linkType: hard + "bser@npm:2.1.1": version: 2.1.1 resolution: "bser@npm:2.1.1" @@ -2809,6 +3567,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -2816,6 +3581,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^5.6.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: ^1.3.1 + ieee754: ^1.1.13 + checksum: e2cf8429e1c4c7b8cbd30834ac09bd61da46ce35f5c22a78e6c2f04497d6d25541b16881e30a019c6fd3154150650ccee27a308eff3e26229d788bbdeb08ab84 + languageName: node + linkType: hard + "builtin-modules@npm:^3.3.0": version: 3.3.0 resolution: "builtin-modules@npm:3.3.0" @@ -2823,6 +3598,13 @@ __metadata: languageName: node linkType: hard +"bytes@npm:3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e + languageName: node + linkType: hard + "cacache@npm:^15.3.0": version: 15.3.0 resolution: "cacache@npm:15.3.0" @@ -2849,6 +3631,20 @@ __metadata: languageName: node linkType: hard +"cacheable-lookup@npm:^6.0.0": + version: 6.1.0 + resolution: "cacheable-lookup@npm:6.1.0" + checksum: 4e37afe897219b1035335b0765106a2c970ffa930497b43cac5000b860f3b17f48d004187279fae97e2e4cbf6a3693709b6d64af65279c7d6c8453321d36d118 + languageName: node + linkType: hard + +"cachedir@npm:^2.3.0": + version: 2.3.0 + resolution: "cachedir@npm:2.3.0" + checksum: ec90cb0f2e6336e266aa748dbadf3da9e0b20e843e43f1591acab7a3f1451337dc2f26cb9dd833ae8cfefeffeeb43ef5b5ff62782a685f4e3c2305dd98482fcb + languageName: node + linkType: hard + "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": version: 1.0.2 resolution: "call-bind@npm:1.0.2" @@ -2866,7 +3662,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.3.1": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -2887,6 +3683,13 @@ __metadata: languageName: node linkType: hard +"caseless@npm:~0.12.0": + version: 0.12.0 + resolution: "caseless@npm:0.12.0" + checksum: b43bd4c440aa1e8ee6baefee8063b4850fd0d7b378f6aabc796c9ec8cb26d27fb30b46885350777d9bd079c5256c0e1329ad0dc7c2817e0bb466810ebb353751 + languageName: node + linkType: hard + "chalk@npm:*": version: 5.0.0 resolution: "chalk@npm:5.0.0" @@ -2894,7 +3697,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.0.0": +"chalk@npm:^2.0.0, chalk@npm:^2.0.1": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -2905,7 +3708,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^3.0.0 || ^4.0.0, chalk@npm:^4.0.0": +"chalk@npm:^3.0.0 || ^4.0.0, chalk@npm:^4.0.0, chalk@npm:^4.1.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -2922,6 +3725,39 @@ __metadata: languageName: node linkType: hard +"charenc@npm:0.0.2": + version: 0.0.2 + resolution: "charenc@npm:0.0.2" + checksum: 81dcadbe57e861d527faf6dd3855dc857395a1c4d6781f4847288ab23cffb7b3ee80d57c15bba7252ffe3e5e8019db767757ee7975663ad2ca0939bb8fcaf2e5 + languageName: node + linkType: hard + +"check-more-types@npm:^2.24.0": + version: 2.24.0 + resolution: "check-more-types@npm:2.24.0" + checksum: b09080ec3404d20a4b0ead828994b2e5913236ef44ed3033a27062af0004cf7d2091fbde4b396bf13b7ce02fb018bc9960b48305e6ab2304cd82d73ed7a51ef4 + languageName: node + linkType: hard + +"chokidar@npm:3.3.0": + version: 3.3.0 + resolution: "chokidar@npm:3.3.0" + dependencies: + anymatch: ~3.1.1 + braces: ~3.0.2 + fsevents: ~2.1.1 + glob-parent: ~5.1.0 + is-binary-path: ~2.1.0 + is-glob: ~4.0.1 + normalize-path: ~3.0.0 + readdirp: ~3.2.0 + dependenciesMeta: + fsevents: + optional: true + checksum: e9863256ebb29dbc5e58a7e2637439814beb63b772686cb9e94478312c24dcaf3d0570220c5e75ea29029f43b664f9956d87b716120d38cf755f32124f047e8e + languageName: node + linkType: hard + "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -2929,6 +3765,13 @@ __metadata: languageName: node linkType: hard +"chrome-trace-event@npm:^1.0.2": + version: 1.0.3 + resolution: "chrome-trace-event@npm:1.0.3" + checksum: cb8b1fc7e881aaef973bd0c4a43cd353c2ad8323fb471a041e64f7c2dd849cde4aad15f8b753331a32dda45c973f032c8a03b8177fc85d60eaa75e91e08bfb97 + languageName: node + linkType: hard + "ci-info@npm:^3.2.0": version: 3.3.0 resolution: "ci-info@npm:3.3.0" @@ -2950,6 +3793,63 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: ^3.1.0 + checksum: 2692784c6cd2fd85cfdbd11f53aea73a463a6d64a77c3e098b2b4697a20443f430c220629e1ca3b195ea5ac4a97a74c2ee411f3807abf6df2b66211fec0c0a29 + languageName: node + linkType: hard + +"cli-table3@npm:0.5.1": + version: 0.5.1 + resolution: "cli-table3@npm:0.5.1" + dependencies: + colors: ^1.1.2 + object-assign: ^4.1.0 + string-width: ^2.1.1 + dependenciesMeta: + colors: + optional: true + checksum: 3ff8c821440a2a0e655a01f04e5b54a0365b3814676cd93cec2b2b0b9952a08311797ad242a181733fcff714fa7d776f8bb45ad812f296390bfa5ef584fb231d + languageName: node + linkType: hard + +"cli-table3@npm:~0.6.1": + version: 0.6.3 + resolution: "cli-table3@npm:0.6.3" + dependencies: + "@colors/colors": 1.5.0 + string-width: ^4.2.0 + dependenciesMeta: + "@colors/colors": + optional: true + checksum: 09897f68467973f827c04e7eaadf13b55f8aec49ecd6647cc276386ea660059322e2dd8020a8b6b84d422dbdd619597046fa89cbbbdc95b2cea149a2df7c096c + languageName: node + linkType: hard + +"cli-truncate@npm:^2.1.0": + version: 2.1.0 + resolution: "cli-truncate@npm:2.1.0" + dependencies: + slice-ansi: ^3.0.0 + string-width: ^4.2.0 + checksum: bf1e4e6195392dc718bf9cd71f317b6300dc4a9191d052f31046b8773230ece4fa09458813bf0e3455a5e68c0690d2ea2c197d14a8b85a7b5e01c97f4b5feb5d + languageName: node + linkType: hard + +"cliui@npm:^5.0.0": + version: 5.0.0 + resolution: "cliui@npm:5.0.0" + dependencies: + string-width: ^3.1.0 + strip-ansi: ^5.2.0 + wrap-ansi: ^5.1.0 + checksum: 0bb8779efe299b8f3002a73619eaa8add4081eb8d1c17bc4fedc6240557fb4eacdc08fe87c39b002eacb6cfc117ce736b362dbfd8bf28d90da800e010ee97df4 + languageName: node + linkType: hard + "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -3016,7 +3916,21 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.8": +"colorette@npm:^2.0.16": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 0c016fea2b91b733eb9f4bcdb580018f52c0bc0979443dad930e5037a968237ac53d9beb98e218d2e9235834f8eebce7f8e080422d6194e957454255bde71d3d + languageName: node + linkType: hard + +"colors@npm:^1.1.2": + version: 1.4.0 + resolution: "colors@npm:1.4.0" + checksum: 98aa2c2418ad87dedf25d781be69dc5fc5908e279d9d30c34d8b702e586a0474605b3a189511482b9d5ed0d20c867515d22749537f7bc546256c6014f3ebdcec + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -3025,6 +3939,27 @@ __metadata: languageName: node linkType: hard +"commander@npm:^2.20.0": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e + languageName: node + linkType: hard + +"commander@npm:^6.2.1": + version: 6.2.1 + resolution: "commander@npm:6.2.1" + checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 + languageName: node + linkType: hard + +"common-tags@npm:^1.8.0": + version: 1.8.2 + resolution: "common-tags@npm:1.8.2" + checksum: 767a6255a84bbc47df49a60ab583053bb29a7d9687066a18500a516188a062c4e4cd52de341f22de0b07062e699b1b8fe3cfa1cb55b241cb9301aeb4f45b4dff + languageName: node + linkType: hard + "commondir@npm:^1.0.1": version: 1.0.1 resolution: "commondir@npm:1.0.1" @@ -3039,6 +3974,18 @@ __metadata: languageName: node linkType: hard +"connect@npm:^3.7.0": + version: 3.7.0 + resolution: "connect@npm:3.7.0" + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: ~1.3.3 + utils-merge: 1.0.1 + checksum: 96e1c4effcf219b065c7823e57351c94366d2e2a6952fa95e8212bffb35c86f1d5a3f9f6c5796d4cd3a5fdda628368b1c3cc44bf19c66cfd68fe9f9cab9177e2 + languageName: node + linkType: hard + "console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -3046,6 +3993,22 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: 5.2.1 + checksum: afb9d545e296a5171d7574fcad634b2fdf698875f4006a9dd04a3e1333880c5c0c98d47b560d01216fb6505a54a2ba6a843ee3a02ec86d7e911e8315255f56c3 + languageName: node + linkType: hard + +"content-type@npm:^1.0.4, content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 + languageName: node + linkType: hard + "convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" @@ -3060,6 +4023,20 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: f4e1b0a98a27a0e6e66fd7ea4e4e9d8e038f624058371bf4499cfcd8f3980be9a121486995202ba3fca74fbed93a407d6d54d43a43f96fd28d0bd7a06761591a + languageName: node + linkType: hard + +"cookie@npm:0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 + languageName: node + linkType: hard + "core-js-compat@npm:^3.25.1": version: 3.30.1 resolution: "core-js-compat@npm:3.30.1" @@ -3076,6 +4053,37 @@ __metadata: languageName: node linkType: hard +"core-util-is@npm:1.0.2": + version: 1.0.2 + resolution: "core-util-is@npm:1.0.2" + checksum: 7a4c925b497a2c91421e25bf76d6d8190f0b2359a9200dbeed136e63b2931d6294d3b1893eda378883ed363cd950f44a12a401384c609839ea616befb7927dab + languageName: node + linkType: hard + +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 + languageName: node + linkType: hard + +"cors-gate@npm:^1.1.3": + version: 1.1.3 + resolution: "cors-gate@npm:1.1.3" + checksum: 8480e24ccc77a0a150c3cb555ae07fc4e2fa0034a2585c0c91efa3c44b91936d31abf1c5a87b09726253b491e0b66ed491face942502bbc38f87bb309f931fc6 + languageName: node + linkType: hard + +"cors@npm:^2.8.4": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: ^4 + vary: ^1 + checksum: ced838404ccd184f61ab4fdc5847035b681c90db7ac17e428f3d81d69e2989d2b680cc254da0e2554f5ed4f8a341820a1ce3d1c16b499f6e2f47a1b9b07b5006 + languageName: node + linkType: hard + "cosmiconfig@npm:^7.0.1": version: 7.0.1 resolution: "cosmiconfig@npm:7.0.1" @@ -3108,7 +4116,16 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-fetch@npm:^3.1.5": + version: 3.1.6 + resolution: "cross-fetch@npm:3.1.6" + dependencies: + node-fetch: ^2.6.11 + checksum: 704b3519ab7de488328cc49a52cf1aa14132ec748382be5b9557b22398c33ffa7f8c2530e8a97ed8cb55da52b0a9740a9791d361271c4591910501682d981d9c + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -3119,27 +4136,253 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": - version: 4.3.4 - resolution: "debug@npm:4.3.4" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 +"crypt@npm:0.0.2": + version: 0.0.2 + resolution: "crypt@npm:0.0.2" + checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34 languageName: node linkType: hard -"debug@npm:^3.2.7": - version: 3.2.7 - resolution: "debug@npm:3.2.7" +"csstype@npm:^3.0.2": + version: 3.1.2 + resolution: "csstype@npm:3.1.2" + checksum: e1a52e6c25c1314d6beef5168da704ab29c5186b877c07d822bd0806717d9a265e8493a2e35ca7e68d0f5d472d43fac1cdce70fd79fd0853dff81f3028d857b5 + languageName: node + linkType: hard + +"cypress-integration-common@workspace:^, cypress-integration-common@workspace:packages/cypress-plugin/test/integration-common": + version: 0.0.0-use.local + resolution: "cypress-integration-common@workspace:packages/cypress-plugin/test/integration-common" + dependencies: + "@rollup/plugin-commonjs": ^24.1.0 + "@rollup/plugin-node-resolve": ^15.0.2 + "@rollup/plugin-typescript": ^11.1.1 + "@unflakable/plugins-common": "workspace:^" + debug: ^4.3.3 + expect: ^29.5.0 + rollup: ^3.21.1 + simple-git: ^3.16.0 + typescript: ^4.9.5 + languageName: unknown + linkType: soft + +"cypress-integration-input-esm@workspace:packages/cypress-plugin/test/integration-input-esm": + version: 0.0.0-use.local + resolution: "cypress-integration-input-esm@workspace:packages/cypress-plugin/test/integration-input-esm" + dependencies: + "@types/react": ^18.2.7 + "@types/react-dom": ^18.2.4 + "@unflakable/cypress-plugin": "workspace:^" + cypress: 10 - 12 + cypress-integration-common: "workspace:^" + mocha: =7.0.1 + mocha-junit-reporter: ^2.2.0 + process: ^0.11.10 + react: ^18.2.0 + react-dom: ^18.2.0 + ts-loader: ^9.4.3 + typescript: ^4.9.5 + webpack: ^5.84.1 + languageName: unknown + linkType: soft + +"cypress-integration-input-manual@workspace:packages/cypress-plugin/test/integration-input-manual": + version: 0.0.0-use.local + resolution: "cypress-integration-input-manual@workspace:packages/cypress-plugin/test/integration-input-manual" + dependencies: + "@types/react": ^18.2.7 + "@types/react-dom": ^18.2.4 + "@unflakable/cypress-plugin": "workspace:^" + cypress: 10 - 12 + cypress-integration-common: "workspace:^" + cypress-multi-reporters: ^1.6.3 + mocha: =7.0.1 + mocha-junit-reporter: ^2.2.0 + process: ^0.11.10 + react: ^18.2.0 + react-dom: ^18.2.0 + ts-loader: ^9.4.3 + typescript: ^4.9.5 + webpack: ^5.84.1 + languageName: unknown + linkType: soft + +"cypress-integration-input@workspace:packages/cypress-plugin/test/integration-input": + version: 0.0.0-use.local + resolution: "cypress-integration-input@workspace:packages/cypress-plugin/test/integration-input" + dependencies: + "@types/react": ^18.2.7 + "@types/react-dom": ^18.2.4 + "@unflakable/cypress-plugin": "workspace:^" + cypress: 10 - 12 + cypress-integration-common: "workspace:^" + mocha: =7.0.1 + mocha-junit-reporter: ^2.2.0 + process: ^0.11.10 + react: ^18.2.0 + react-dom: ^18.2.0 + ts-loader: ^9.4.3 + typescript: ^4.9.5 + webpack: ^5.84.1 + languageName: unknown + linkType: soft + +"cypress-integration@workspace:packages/cypress-plugin/test/integration": + version: 0.0.0-use.local + resolution: "cypress-integration@workspace:packages/cypress-plugin/test/integration" + dependencies: + "@types/jest": ^29.5.2 + "@unflakable/cypress-plugin": "workspace:^" + "@unflakable/jest-plugin": "workspace:^" + "@unflakable/js-api": "workspace:^" + "@unflakable/plugins-common": "workspace:^" + cypress: 10 - 12 + cypress-integration-common: "workspace:^" + debug: ^4.3.3 + escape-string-regexp: ^4.0.0 + jest: ^29.5.0 + jest-environment-node: ^29.5.0 + jest-expect-message: ^1.1.3 + mockttp: ^3.7.5 + tree-kill: ^1.2.2 + ts-jest: ^29.1.0 + typescript: ^4.9.5 + languageName: unknown + linkType: soft + +"cypress-multi-reporters@npm:^1.6.3": + version: 1.6.3 + resolution: "cypress-multi-reporters@npm:1.6.3" + dependencies: + debug: ^4.3.4 + lodash: ^4.17.21 + peerDependencies: + mocha: ">=3.1.2" + checksum: fe8038ea86ade399eb67c8695951052e81298922cea53e47c87fb558456b1aca30c4624b20fed0156445d011bdaf2eb83b665ebfe029c773f031504f463e34ee + languageName: node + linkType: hard + +"cypress@npm:10 - 12": + version: 12.14.0 + resolution: "cypress@npm:12.14.0" + dependencies: + "@cypress/request": ^2.88.10 + "@cypress/xvfb": ^1.2.4 + "@types/node": ^14.14.31 + "@types/sinonjs__fake-timers": 8.1.1 + "@types/sizzle": ^2.3.2 + arch: ^2.2.0 + blob-util: ^2.0.2 + bluebird: ^3.7.2 + buffer: ^5.6.0 + cachedir: ^2.3.0 + chalk: ^4.1.0 + check-more-types: ^2.24.0 + cli-cursor: ^3.1.0 + cli-table3: ~0.6.1 + commander: ^6.2.1 + common-tags: ^1.8.0 + dayjs: ^1.10.4 + debug: ^4.3.4 + enquirer: ^2.3.6 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: ^4.1.1 + extract-zip: 2.0.1 + figures: ^3.2.0 + fs-extra: ^9.1.0 + getos: ^3.2.1 + is-ci: ^3.0.0 + is-installed-globally: ~0.4.0 + lazy-ass: ^1.6.0 + listr2: ^3.8.3 + lodash: ^4.17.21 + log-symbols: ^4.0.0 + minimist: ^1.2.8 + ospath: ^1.2.2 + pretty-bytes: ^5.6.0 + proxy-from-env: 1.0.0 + request-progress: ^3.0.0 + semver: ^7.3.2 + supports-color: ^8.1.1 + tmp: ~0.2.1 + untildify: ^4.0.0 + yauzl: ^2.10.0 + bin: + cypress: bin/cypress + checksum: c98d33dc50f5e48a215f41e3a5139514816d2320e433d143a352a460f74941d3106a1f7485f61172f544cf125176c1225ac8edec31645d5a89cd394f72f93588 + languageName: node + linkType: hard + +"dashdash@npm:^1.12.0": + version: 1.14.1 + resolution: "dashdash@npm:1.14.1" + dependencies: + assert-plus: ^1.0.0 + checksum: 3634c249570f7f34e3d34f866c93f866c5b417f0dd616275decae08147dcdf8fccfaa5947380ccfb0473998ea3a8057c0b4cd90c875740ee685d0624b2983598 + languageName: node + linkType: hard + +"data-uri-to-buffer@npm:3": + version: 3.0.1 + resolution: "data-uri-to-buffer@npm:3.0.1" + checksum: c59c3009686a78c071806b72f4810856ec28222f0f4e252aa495ec027ed9732298ceea99c50328cf59b151dd34cbc3ad6150bbb43e41fc56fa19f48c99e9fc30 + languageName: node + linkType: hard + +"dayjs@npm:^1.10.4": + version: 1.11.7 + resolution: "dayjs@npm:1.11.7" + checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb + languageName: node + linkType: hard + +"debug@npm:2.6.9": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: 2.0.0 + checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 + languageName: node + linkType: hard + +"debug@npm:3.2.6": + version: 3.2.6 + resolution: "debug@npm:3.2.6" + dependencies: + ms: ^2.1.1 + checksum: 07bc8b3a13ef3cfa6c06baf7871dfb174c291e5f85dbf566f086620c16b9c1a0e93bb8f1935ebbd07a683249e7e30286f2966e2ef461e8fd17b1b60732062d6b + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: 2.1.2 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 + languageName: node + linkType: hard + +"debug@npm:^3.1.0, debug@npm:^3.1.1, debug@npm:^3.2.7": + version: 3.2.7 + resolution: "debug@npm:3.2.7" dependencies: ms: ^2.1.1 checksum: b3d8c5940799914d30314b7c3304a43305fd0715581a919dacb8b3176d024a782062368405b47491516d2091d6462d4d11f2f4974a405048094f8bfebfa3071c languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" @@ -3170,7 +4413,7 @@ __metadata: languageName: node linkType: hard -"deep-is@npm:^0.1.3": +"deep-is@npm:^0.1.3, deep-is@npm:~0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" checksum: edb65dd0d7d1b9c40b2f50219aef30e116cedd6fc79290e740972c132c09106d2e80aa0bc8826673dd5a00222d4179c84b36a790eef63a4c4bca75a37ef90804 @@ -3191,7 +4434,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4": +"define-properties@npm:^1.1.2, define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0": version: 1.2.0 resolution: "define-properties@npm:1.2.0" dependencies: @@ -3201,6 +4444,18 @@ __metadata: languageName: node linkType: hard +"degenerator@npm:^3.0.2": + version: 3.0.4 + resolution: "degenerator@npm:3.0.4" + dependencies: + ast-types: ^0.13.2 + escodegen: ^1.8.1 + esprima: ^4.0.0 + vm2: ^3.9.17 + checksum: 99c27c9456095e32c4f6e01091d2b5c249f246b574487c52bca571e1e586b02d4b74a0ea7f22f30cc953c914383d02e2038d7d476a22f2704a8c1e88b671007d + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -3215,13 +4470,36 @@ __metadata: languageName: node linkType: hard -"depd@npm:^1.1.2": +"depd@npm:2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: abbe19c768c97ee2eed6282d8ce3031126662252c58d711f646921c9623f9052e3e1906443066beec1095832f534e57c523b7333f8e7e0d93051ab6baef5ab3a + languageName: node + linkType: hard + +"depd@npm:^1.1.2, depd@npm:~1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 languageName: node linkType: hard +"destroy@npm:1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 + languageName: node + linkType: hard + +"destroyable-server@npm:^1.0.0": + version: 1.0.0 + resolution: "destroyable-server@npm:1.0.0" + dependencies: + "@types/node": "*" + checksum: ac81b26f616a9d0aaa9cb759fa5a5a186f887025362329f7ddc909f53090f4aea0d1b75c4dda23e210faee536e2d7352de017261e1625b7b18108f0e630efa1f + languageName: node + linkType: hard + "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -3236,6 +4514,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:3.5.0": + version: 3.5.0 + resolution: "diff@npm:3.5.0" + checksum: 00842950a6551e26ce495bdbce11047e31667deea546527902661f25cc2e73358967ebc78cf86b1a9736ec3e14286433225f9970678155753a6291c3bca5227b + languageName: node + linkType: hard + "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -3270,6 +4555,35 @@ __metadata: languageName: node linkType: hard +"duplexify@npm:^3.5.1": + version: 3.7.1 + resolution: "duplexify@npm:3.7.1" + dependencies: + end-of-stream: ^1.0.0 + inherits: ^2.0.1 + readable-stream: ^2.0.0 + stream-shift: ^1.0.0 + checksum: 3c2ed2223d956a5da713dae12ba8295acb61d9acd966ccbba938090d04f4574ca4dca75cca089b5077c2d7e66101f32e6ea9b36a78ca213eff574e7a8b8accf2 + languageName: node + linkType: hard + +"ecc-jsbn@npm:~0.1.1": + version: 0.1.2 + resolution: "ecc-jsbn@npm:0.1.2" + dependencies: + jsbn: ~0.1.0 + safer-buffer: ^2.1.0 + checksum: 22fef4b6203e5f31d425f5b711eb389e4c6c2723402e389af394f8411b76a488fa414d309d866e2b577ce3e8462d344205545c88a8143cc21752a5172818888a + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 1b4cac778d64ce3b582a7e26b218afe07e207a0f9bfe13cc7395a6d307849cfe361e65033c3251e00c27dd060cab43014c2d6b2647676135e18b77d2d05b3f4f + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.284": version: 1.4.328 resolution: "electron-to-chromium@npm:1.4.328" @@ -3284,6 +4598,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^7.0.1": + version: 7.0.3 + resolution: "emoji-regex@npm:7.0.3" + checksum: 9159b2228b1511f2870ac5920f394c7e041715429a68459ebe531601555f11ea782a8e1718f969df2711d38c66268174407cbca57ce36485544f695c2dfdc96e + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -3291,6 +4612,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:~1.0.2": + version: 1.0.2 + resolution: "encodeurl@npm:1.0.2" + checksum: e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -3300,13 +4628,31 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.12.0": - version: 5.13.0 - resolution: "enhanced-resolve@npm:5.13.0" +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: ^1.4.0 + checksum: 530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.12.0, enhanced-resolve@npm:^5.14.1": + version: 5.14.1 + resolution: "enhanced-resolve@npm:5.14.1" dependencies: graceful-fs: ^4.2.4 tapable: ^2.2.0 - checksum: 76d6844c4393d76beed5b3ce6cf5a98dee3ad5c84a9887f49ccde1224e3b7af201dfbd5a57ebf2b49f623b74883df262d50ff480d3cc02fc2881fc58b84e1bbe + checksum: ad2a31928b6649eed40d364838449587f731baa63863e83d2629bebaa8be1eabac18b90f89c1784bc805b0818363e99b22547159edd485d7e5ccf18cdc640642 + languageName: node + linkType: hard + +"enquirer@npm:^2.3.6": + version: 2.3.6 + resolution: "enquirer@npm:2.3.6" + dependencies: + ansi-colors: ^4.1.1 + checksum: 1c0911e14a6f8d26721c91e01db06092a5f7675159f0261d69c403396a385afd13dd76825e7678f66daffa930cfaa8d45f506fb35f818a2788463d022af1b884 languageName: node linkType: hard @@ -3333,7 +4679,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.19.0, es-abstract@npm:^1.20.4": +"es-abstract@npm:^1.19.0, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2": version: 1.21.2 resolution: "es-abstract@npm:1.21.2" dependencies: @@ -3375,6 +4721,13 @@ __metadata: languageName: node linkType: hard +"es-array-method-boxes-properly@npm:^1.0.0": + version: 1.0.0 + resolution: "es-array-method-boxes-properly@npm:1.0.0" + checksum: 2537fcd1cecf187083890bc6f5236d3a26bf39237433587e5bf63392e88faae929dbba78ff0120681a3f6f81c23fe3816122982c160d63b38c95c830b633b826 + languageName: node + linkType: hard + "es-get-iterator@npm:^1.1.1": version: 1.1.2 resolution: "es-get-iterator@npm:1.1.2" @@ -3391,6 +4744,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.2.1": + version: 1.2.1 + resolution: "es-module-lexer@npm:1.2.1" + checksum: c4145b853e1491eaa5d591e4580926d242978c38071ad3d09165c3b6d50314cc0ae3bf6e1dec81a9e53768b9299df2063d2e4a67d7742a5029ddeae6c4fc26f0 + languageName: node + linkType: hard + "es-set-tostringtag@npm:^2.0.1": version: 2.0.1 resolution: "es-set-tostringtag@npm:2.0.1" @@ -3422,6 +4782,13 @@ __metadata: languageName: node linkType: hard +"es6-promisify@npm:^7.0.0": + version: 7.0.0 + resolution: "es6-promisify@npm:7.0.0" + checksum: 461305147b62bb00624cbfa986227eb2d7a12c21a696bb30e7f3f3182d899ea3436a9500900cd63e35ddbc68119ad305e243f25b4e0d1b207347361ab4f4579e + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -3429,7 +4796,14 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": +"escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 + languageName: node + linkType: hard + +"escape-string-regexp@npm:1.0.5, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -3450,6 +4824,25 @@ __metadata: languageName: node linkType: hard +"escodegen@npm:^1.8.1": + version: 1.14.3 + resolution: "escodegen@npm:1.14.3" + dependencies: + esprima: ^4.0.1 + estraverse: ^4.2.0 + esutils: ^2.0.2 + optionator: ^0.8.1 + source-map: ~0.6.1 + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 381cdc4767ecdb221206bbbab021b467bbc2a6f5c9a99c9e6353040080bdd3dfe73d7604ad89a47aca6ea7d58bc635f6bd3fbc8da9a1998e9ddfa8372362ccd0 + languageName: node + linkType: hard + "eslint-import-resolver-node@npm:^0.3.7": version: 0.3.7 resolution: "eslint-import-resolver-node@npm:0.3.7" @@ -3492,6 +4885,17 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-cypress@npm:^2.13.3": + version: 2.13.3 + resolution: "eslint-plugin-cypress@npm:2.13.3" + dependencies: + globals: ^11.12.0 + peerDependencies: + eslint: ">= 3.2.1" + checksum: 9affbcee29e030a4251c4794f7533e8e8c0e3b98ab3470a2c730ed059f733c5857a04c7ac214cc0ca7aeef1b11242e72595de7fc1f6b8b4d4578d9eca10af203 + languageName: node + linkType: hard + "eslint-plugin-import@npm:^2.27.5": version: 2.27.5 resolution: "eslint-plugin-import@npm:2.27.5" @@ -3534,7 +4938,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^5.1.1": +"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -3622,7 +5026,7 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0": +"esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" bin: @@ -3650,7 +5054,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1": +"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": version: 4.3.0 resolution: "estraverse@npm:4.3.0" checksum: a6299491f9940bb246124a8d44b7b7a413a8336f5436f9837aaa9330209bd9ee8af7e91a654a3545aee9c54b3308e78ee360cef1d777d37cfef77d2fa33b5827 @@ -3678,6 +5082,51 @@ __metadata: languageName: node linkType: hard +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff + languageName: node + linkType: hard + +"eventemitter2@npm:6.4.7": + version: 6.4.7 + resolution: "eventemitter2@npm:6.4.7" + checksum: 1b36a77e139d6965ebf3a36c01fa00c089ae6b80faa1911e52888f40b3a7057b36a2cc45dcd1ad87cda3798fe7b97a0aabcbb8175a8b96092a23bb7d0f039e66 + languageName: node + linkType: hard + +"eventemitter3@npm:^3.1.0": + version: 3.1.2 + resolution: "eventemitter3@npm:3.1.2" + checksum: 81e4e82b8418f5cfd986d2b4a2fa5397ac4eb8134e09bcb47005545e22fdf8e9e61d5c053d34651112245aae411bdfe6d0ad5511da0400743fef5fc38bfcfbe3 + languageName: node + linkType: hard + +"events@npm:^3.2.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780 + languageName: node + linkType: hard + +"execa@npm:4.1.0": + version: 4.1.0 + resolution: "execa@npm:4.1.0" + dependencies: + cross-spawn: ^7.0.0 + get-stream: ^5.0.0 + human-signals: ^1.1.1 + is-stream: ^2.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^4.0.0 + onetime: ^5.1.0 + signal-exit: ^3.0.2 + strip-final-newline: ^2.0.0 + checksum: e30d298934d9c52f90f3847704fd8224e849a081ab2b517bbc02f5f7732c24e56a21f14cb96a08256deffeb2d12b2b7cb7e2b014a12fb36f8d3357e06417ed55 + languageName: node + linkType: hard + "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -3695,6 +5144,15 @@ __metadata: languageName: node linkType: hard +"executable@npm:^4.1.1": + version: 4.1.1 + resolution: "executable@npm:4.1.1" + dependencies: + pify: ^2.2.0 + checksum: f01927ce59bccec804e171bf859a26e362c1f50aa9ebc69f7cafdcce3859d29d4b6267fd47237c18b0a1830614bd3f0ee14b7380d9bad18a4e7af9b5f0b6984f + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -3715,6 +5173,97 @@ __metadata: languageName: node linkType: hard +"express-graphql@npm:^0.11.0": + version: 0.11.0 + resolution: "express-graphql@npm:0.11.0" + dependencies: + accepts: ^1.3.7 + content-type: ^1.0.4 + http-errors: 1.8.0 + raw-body: ^2.4.1 + peerDependencies: + graphql: ^14.7.0 || ^15.3.0 + checksum: 212b96fd4cda13144fbd98f7d455fbe64687e71ccf505b912ff2234c93b334e2d666e96c9eb9567e3fc684542c06b3121c9be109bb9e60571fec34666832592b + languageName: node + linkType: hard + +"express@npm:^4.14.0": + version: 4.18.2 + resolution: "express@npm:4.18.2" + dependencies: + accepts: ~1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: ~1.0.4 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: ~1.1.2 + on-finished: 2.4.1 + parseurl: ~1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: ~2.0.7 + qs: 6.11.0 + range-parser: ~1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: ~1.6.18 + utils-merge: 1.0.1 + vary: ~1.1.2 + checksum: 3c4b9b076879442f6b968fe53d85d9f1eeacbb4f4c41e5f16cc36d77ce39a2b0d81b3f250514982110d815b2f7173f5561367f9110fcc541f9371948e8c8b037 + languageName: node + linkType: hard + +"extend@npm:~3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 + languageName: node + linkType: hard + +"extract-zip@npm:2.0.1": + version: 2.0.1 + resolution: "extract-zip@npm:2.0.1" + dependencies: + "@types/yauzl": ^2.9.1 + debug: ^4.1.1 + get-stream: ^5.1.0 + yauzl: ^2.10.0 + dependenciesMeta: + "@types/yauzl": + optional: true + bin: + extract-zip: cli.js + checksum: 8cbda9debdd6d6980819cc69734d874ddd71051c9fe5bde1ef307ebcedfe949ba57b004894b585f758b7c9eeeea0e3d87f2dda89b7d25320459c2c9643ebb635 + languageName: node + linkType: hard + +"extsprintf@npm:1.3.0": + version: 1.3.0 + resolution: "extsprintf@npm:1.3.0" + checksum: cee7a4a1e34cffeeec18559109de92c27517e5641991ec6bab849aa64e3081022903dd53084f2080d0d2530803aa5ee84f1e9de642c365452f9e67be8f958ce2 + languageName: node + linkType: hard + +"extsprintf@npm:^1.2.0": + version: 1.4.1 + resolution: "extsprintf@npm:1.4.1" + checksum: a2f29b241914a8d2bad64363de684821b6b1609d06ae68d5b539e4de6b28659715b5bea94a7265201603713b7027d35399d10b0548f09071c5513e65e8323d33 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -3735,14 +5284,14 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb languageName: node linkType: hard -"fast-levenshtein@npm:^2.0.6": +"fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" checksum: 92cfec0a8dfafd9c7a15fba8f2cc29cd0b62b85f056d99ce448bbcd9f708e18ab2764bda4dd5158364f4145a7c72788538994f0d1787b956ef0d1062b0f7c24c @@ -3767,6 +5316,15 @@ __metadata: languageName: node linkType: hard +"fd-slicer@npm:~1.1.0": + version: 1.1.0 + resolution: "fd-slicer@npm:1.1.0" + dependencies: + pend: ~1.2.0 + checksum: c8585fd5713f4476eb8261150900d2cb7f6ff2d87f8feb306ccc8a1122efd152f1783bdb2b8dc891395744583436bfd8081d8e63ece0ec8687eeefea394d4ff2 + languageName: node + linkType: hard + "fetch-mock-jest@npm:^1.5.1": version: 1.5.1 resolution: "fetch-mock-jest@npm:1.5.1" @@ -3804,6 +5362,15 @@ __metadata: languageName: node linkType: hard +"figures@npm:^3.2.0": + version: 3.2.0 + resolution: "figures@npm:3.2.0" + dependencies: + escape-string-regexp: ^1.0.5 + checksum: 85a6ad29e9aca80b49b817e7c89ecc4716ff14e3779d9835af554db91bac41c0f289c418923519392a1e582b4d10482ad282021330cd045bb7b80c84152f2a2b + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -3813,6 +5380,13 @@ __metadata: languageName: node linkType: hard +"file-uri-to-path@npm:2": + version: 2.0.0 + resolution: "file-uri-to-path@npm:2.0.0" + checksum: 4a71a99ddaa6ae7ae7bffe2948c34da59982ed465d930a0af9cb59fcc10fcd93366cc356ec3337c18373fde5df7ac52afda4558f155febd1799d135552207edb + languageName: node + linkType: hard + "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -3822,6 +5396,45 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:1.1.2": + version: 1.1.2 + resolution: "finalhandler@npm:1.1.2" + dependencies: + debug: 2.6.9 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + on-finished: ~2.3.0 + parseurl: ~1.3.3 + statuses: ~1.5.0 + unpipe: ~1.0.0 + checksum: 617880460c5138dd7ccfd555cb5dde4d8f170f4b31b8bd51e4b646bb2946c30f7db716428a1f2882d730d2b72afb47d1f67cc487b874cb15426f95753a88965e + languageName: node + linkType: hard + +"finalhandler@npm:1.2.0": + version: 1.2.0 + resolution: "finalhandler@npm:1.2.0" + dependencies: + debug: 2.6.9 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + on-finished: 2.4.1 + parseurl: ~1.3.3 + statuses: 2.0.1 + unpipe: ~1.0.0 + checksum: 92effbfd32e22a7dff2994acedbd9bcc3aa646a3e919ea6a53238090e87097f8ef07cced90aa2cc421abdf993aefbdd5b00104d55c7c5479a8d00ed105b45716 + languageName: node + linkType: hard + +"find-up@npm:3.0.0, find-up@npm:^3.0.0": + version: 3.0.0 + resolution: "find-up@npm:3.0.0" + dependencies: + locate-path: ^3.0.0 + checksum: 38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 + languageName: node + linkType: hard + "find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -3852,9 +5465,20 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.1.0": - version: 3.2.5 - resolution: "flatted@npm:3.2.5" +"flat@npm:^4.1.0": + version: 4.1.1 + resolution: "flat@npm:4.1.1" + dependencies: + is-buffer: ~2.0.3 + bin: + flat: cli.js + checksum: 398be12185eb0f3c59797c3670a8c35d07020b673363175676afbaf53d6b213660e060488554cf82c25504986e1a6059bdbcc5d562e87ca3e972e8a33148e3ae + languageName: node + linkType: hard + +"flatted@npm:^3.1.0": + version: 3.2.5 + resolution: "flatted@npm:3.2.5" checksum: 3c436e9695ccca29620b4be5671dd72e5dd0a7500e0856611b7ca9bd8169f177f408c3b9abfa78dfe1493ee2d873e2c119080a8a9bee4e1a186a9e60ca6c89f1 languageName: node linkType: hard @@ -3868,6 +5492,13 @@ __metadata: languageName: node linkType: hard +"forever-agent@npm:~0.6.1": + version: 0.6.1 + resolution: "forever-agent@npm:0.6.1" + checksum: 766ae6e220f5fe23676bb4c6a99387cec5b7b62ceb99e10923376e27bfea72f3c3aeec2ba5f45f3f7ba65d6616965aa7c20b15002b6860833bb6e394dea546a8 + languageName: node + linkType: hard + "form-data@npm:^3.0.0": version: 3.0.1 resolution: "form-data@npm:3.0.1" @@ -3879,6 +5510,54 @@ __metadata: languageName: node linkType: hard +"form-data@npm:~2.3.2": + version: 2.3.3 + resolution: "form-data@npm:2.3.3" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.6 + mime-types: ^2.1.12 + checksum: 10c1780fa13dbe1ff3100114c2ce1f9307f8be10b14bf16e103815356ff567b6be39d70fc4a40f8990b9660012dc24b0f5e1dde1b6426166eb23a445ba068ca3 + languageName: node + linkType: hard + +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: fd27e2394d8887ebd16a66ffc889dc983fbbd797d5d3f01087c020283c0f019a7d05ee85669383d8e0d216b116d720fc0cef2f6e9b7eb9f4c90c6e0bc7fd28e6 + languageName: node + linkType: hard + +"fresh@npm:0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 + languageName: node + linkType: hard + +"fs-extra@npm:^8.1.0": + version: 8.1.0 + resolution: "fs-extra@npm:8.1.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^4.0.0 + universalify: ^0.1.0 + checksum: bf44f0e6cea59d5ce071bba4c43ca76d216f89e402dc6285c128abc0902e9b8525135aa808adad72c9d5d218e9f4bcc63962815529ff2f684ad532172a284880 + languageName: node + linkType: hard + +"fs-extra@npm:^9.1.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: ^1.0.0 + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -3905,6 +5584,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:~2.1.1": + version: 2.1.3 + resolution: "fsevents@npm:2.1.3" + dependencies: + node-gyp: latest + checksum: b5ec0516b44d75b60af5c01ff80a80cd995d175e4640d2a92fbabd02991dd664d76b241b65feef0775c23d531c3c74742c0fbacd6205af812a9c3cef59f04292 + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" @@ -3914,6 +5603,25 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@~2.1.1#~builtin": + version: 2.1.3 + resolution: "fsevents@patch:fsevents@npm%3A2.1.3#~builtin::version=2.1.3&hash=31d12a" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + +"ftp@npm:^0.3.10": + version: 0.3.10 + resolution: "ftp@npm:0.3.10" + dependencies: + readable-stream: 1.1.x + xregexp: 2.0.0 + checksum: ddd313c1d44eb7429f3a7d77a0155dc8fe86a4c64dca58f395632333ce4b4e74c61413c6e0ef66ea3f3d32d905952fbb6d028c7117d522f793eb1fa282e17357 + languageName: node + linkType: hard + "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -3964,7 +5672,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -3989,6 +5697,15 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": + version: 5.2.0 + resolution: "get-stream@npm:5.2.0" + dependencies: + pump: ^3.0.0 + checksum: 8bc1a23174a06b2b4ce600df38d6c98d2ef6d84e020c1ddad632ad75bac4e092eeb40e4c09e0761c35fc2dbc5e7fff5dab5e763a383582c4a167dd69a905bd12 + languageName: node + linkType: hard + "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -4013,7 +5730,39 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": +"get-uri@npm:3": + version: 3.0.2 + resolution: "get-uri@npm:3.0.2" + dependencies: + "@tootallnate/once": 1 + data-uri-to-buffer: 3 + debug: 4 + file-uri-to-path: 2 + fs-extra: ^8.1.0 + ftp: ^0.3.10 + checksum: 5325b2906b08ca37529ca421cf52bc50376e75c6a945e0a8064e3f76b4bb67b8ab1e316a2fc7a307c8c606ab36d030720f39a57c97b027ff1134335e12102946 + languageName: node + linkType: hard + +"getos@npm:^3.2.1": + version: 3.2.1 + resolution: "getos@npm:3.2.1" + dependencies: + async: ^3.2.0 + checksum: 42fd78a66d47cebd3e09de5566cc0044e034b08f4a000a310dbd89a77b02c65d8f4002554bfa495ea5bdc4fa9d515f5ac785a7cc474ba45383cc697f865eeaf1 + languageName: node + linkType: hard + +"getpass@npm:^0.1.1": + version: 0.1.7 + resolution: "getpass@npm:0.1.7" + dependencies: + assert-plus: ^1.0.0 + checksum: ab18d55661db264e3eac6012c2d3daeafaab7a501c035ae0ccb193c3c23e9849c6e29b6ac762b9c2adae460266f925d55a3a2a3a3c8b94be2f222df94d70c046 + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -4031,13 +5780,27 @@ __metadata: languageName: node linkType: hard -"glob-to-regexp@npm:^0.4.0": +"glob-to-regexp@npm:^0.4.0, glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" checksum: e795f4e8f06d2a15e86f76e4d92751cf8bbfcf0157cea5c2f0f35678a8195a750b34096b1256e436f0cebc1883b5ff0888c47348443e69546a5a87f9e1eb1167 languageName: node linkType: hard +"glob@npm:7.1.3": + version: 7.1.3 + resolution: "glob@npm:7.1.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.0.4 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: d72a834a393948d6c4a5cacc6a29fe5fe190e1cd134e55dfba09aee0be6fe15be343e96d8ec43558ab67ff8af28e4420c7f63a4d4db1c779e515015e9c318616 + languageName: node + linkType: hard + "glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -4065,7 +5828,16 @@ __metadata: languageName: node linkType: hard -"globals@npm:^11.1.0": +"global-dirs@npm:^3.0.0": + version: 3.0.1 + resolution: "global-dirs@npm:3.0.1" + dependencies: + ini: 2.0.0 + checksum: 70147b80261601fd40ac02a104581432325c1c47329706acd773f3a6ce99bb36d1d996038c85ccacd482ad22258ec233c586b6a91535b1a116b89663d49d6438 + languageName: node + linkType: hard + +"globals@npm:^11.1.0, globals@npm:^11.12.0": version: 11.12.0 resolution: "globals@npm:11.12.0" checksum: 67051a45eca3db904aee189dfc7cd53c20c7d881679c93f6146ddd4c9f4ab2268e68a919df740d39c71f4445d2b38ee360fc234428baea1dbdfe68bbcb46979e @@ -4140,7 +5912,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 @@ -4154,6 +5926,42 @@ __metadata: languageName: node linkType: hard +"graphql-subscriptions@npm:^1.1.0": + version: 1.2.1 + resolution: "graphql-subscriptions@npm:1.2.1" + dependencies: + iterall: ^1.3.0 + peerDependencies: + graphql: ^0.10.5 || ^0.11.3 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 + checksum: 2b9533c6774e7be46acd6fbee528aab06429f15dc222eabd991e82c02bf74e390b638dffa1a3fd86c1e26212c40a42a0418d7f4a7c3a1edf0534978ef128e528 + languageName: node + linkType: hard + +"graphql-tag@npm:^2.12.6": + version: 2.12.6 + resolution: "graphql-tag@npm:2.12.6" + dependencies: + tslib: ^2.1.0 + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: b15162a3d62f17b9b79302445b9ee330e041582f1c7faca74b9dec5daa74272c906ec1c34e1c50592bb6215e5c3eba80a309103f6ba9e4c1cddc350c46f010df + languageName: node + linkType: hard + +"graphql@npm:^14.0.2 || ^15.5": + version: 15.8.0 + resolution: "graphql@npm:15.8.0" + checksum: 423325271db8858428641b9aca01699283d1fe5b40ef6d4ac622569ecca927019fce8196208b91dd1d8eb8114f00263fe661d241d0eb40c10e5bfd650f86ec5e + languageName: node + linkType: hard + +"growl@npm:1.10.5": + version: 1.10.5 + resolution: "growl@npm:1.10.5" + checksum: 4b86685de6831cebcbb19f93870bea624afee61124b0a20c49017013987cd129e73a8c4baeca295728f41d21265e1f859d25ef36731b142ca59c655fea94bb1a + languageName: node + linkType: hard + "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -4191,7 +5999,7 @@ __metadata: languageName: node linkType: hard -"has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": +"has-symbols@npm:^1.0.0, has-symbols@npm:^1.0.1, has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": version: 1.0.3 resolution: "has-symbols@npm:1.0.3" checksum: a054c40c631c0d5741a8285010a0777ea0c068f99ed43e5d6eb12972da223f8af553a455132fdb0801bdcfa0e0f443c0c03a68d8555aa529b3144b446c3f2410 @@ -4223,6 +6031,15 @@ __metadata: languageName: node linkType: hard +"he@npm:1.2.0": + version: 1.2.0 + resolution: "he@npm:1.2.0" + bin: + he: bin/he + checksum: 3d4d6babccccd79c5c5a3f929a68af33360d6445587d628087f39a965079d84f18ce9c3d3f917ee1e3978916fc833bb8b29377c3b403f919426f91bc6965e7a7 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -4237,6 +6054,54 @@ __metadata: languageName: node linkType: hard +"http-encoding@npm:^1.5.1": + version: 1.5.1 + resolution: "http-encoding@npm:1.5.1" + dependencies: + brotli-wasm: ^1.1.0 + pify: ^5.0.0 + zstd-codec: ^0.1.4 + checksum: 534aa2facb0ae529fa88b9778867472247711626b90030fd4351572c6147fb5e895d9d2e305e7dc5cc993345f2fbdb17ca99345651bf76dbac39a07f552af2ac + languageName: node + linkType: hard + +"http-errors@npm:1.8.0": + version: 1.8.0 + resolution: "http-errors@npm:1.8.0" + dependencies: + depd: ~1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: ">= 1.5.0 < 2" + toidentifier: 1.0.0 + checksum: 873d997bada0340b31cc69cbe8376e47ee142f60375b81447fa3ad7be512dd4026afb3b46ed2257ee59472d43782a34151994b34264b204bcaad02e67ad836cb + languageName: node + linkType: hard + +"http-errors@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + checksum: 9b0a3782665c52ce9dc658a0d1560bcb0214ba5699e4ea15aefb2a496e2ca83db03ebc42e1cce4ac1f413e4e0d2d736a3fd755772c556a9a06853ba2a0b7d920 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^4.0.1": + version: 4.0.1 + resolution: "http-proxy-agent@npm:4.0.1" + dependencies: + "@tootallnate/once": 1 + agent-base: 6 + debug: 4 + checksum: c6a5da5a1929416b6bbdf77b1aca13888013fe7eb9d59fc292e25d18e041bb154a8dfada58e223fc7b76b9b2d155a87e92e608235201f77d34aa258707963a82 + languageName: node + linkType: hard + "http-proxy-agent@npm:^5.0.0": version: 5.0.0 resolution: "http-proxy-agent@npm:5.0.0" @@ -4248,13 +6113,41 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "https-proxy-agent@npm:5.0.0" +"http-signature@npm:~1.3.6": + version: 1.3.6 + resolution: "http-signature@npm:1.3.6" + dependencies: + assert-plus: ^1.0.0 + jsprim: ^2.0.2 + sshpk: ^1.14.1 + checksum: 10be2af4764e71fee0281392937050201ee576ac755c543f570d6d87134ce5e858663fe999a7adb3e4e368e1e356d0d7fec6b9542295b875726ff615188e7a0c + languageName: node + linkType: hard + +"http2-wrapper@npm:^2.2.0": + version: 2.2.0 + resolution: "http2-wrapper@npm:2.2.0" + dependencies: + quick-lru: ^5.1.1 + resolve-alpn: ^1.2.0 + checksum: 6fd20e5cb6a58151715b3581e06a62a47df943187d2d1f69e538a50cccb7175dd334ecfde7900a37d18f3e13a1a199518a2c211f39860e81e9a16210c199cfaa + languageName: node + linkType: hard + +"https-proxy-agent@npm:5, https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" dependencies: agent-base: 6 debug: 4 - checksum: 165bfb090bd26d47693597661298006841ab733d0c7383a8cb2f17373387a94c903a3ac687090aa739de05e379ab6f868bae84ab4eac288ad85c328cd1ec9e53 + checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 + languageName: node + linkType: hard + +"human-signals@npm:^1.1.1": + version: 1.1.1 + resolution: "human-signals@npm:1.1.1" + checksum: d587647c9e8ec24e02821b6be7de5a0fc37f591f6c4e319b3054b43fd4c35a70a94c46fc74d8c1a43c47fde157d23acd7421f375e1c1365b09a16835b8300205 languageName: node linkType: hard @@ -4274,6 +6167,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: ">= 2.1.2 < 3" + checksum: bd9f120f5a5b306f0bc0b9ae1edeb1577161503f5f8252a20f1a9e56ef8775c9959fd01c55f2d3a39d9a8abaf3e30c1abeb1895f367dcbbe0a8fd1c9ca01c4f6 + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -4283,6 +6185,13 @@ __metadata: languageName: node linkType: hard +"ieee754@npm:^1.1.13": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e + languageName: node + linkType: hard + "ignore@npm:^5.2.0": version: 5.2.0 resolution: "ignore@npm:5.2.0" @@ -4343,13 +6252,20 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:~2.0.1, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 languageName: node linkType: hard +"ini@npm:2.0.0": + version: 2.0.0 + resolution: "ini@npm:2.0.0" + checksum: e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e + languageName: node + linkType: hard + "internal-slot@npm:^1.0.5": version: 1.0.5 resolution: "internal-slot@npm:1.0.5" @@ -4368,6 +6284,20 @@ __metadata: languageName: node linkType: hard +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349 + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: f88d3825981486f5a1942414c8d77dd6674dd71c065adcfa46f578d677edcb99fda25af42675cb59db492fdf427b34a5abfcde3982da11a8fd83a500b41cfe77 + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.0": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" @@ -4405,6 +6335,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: ^2.0.0 + checksum: 84192eb88cff70d320426f35ecd63c3d6d495da9d805b19bc65b518984b7c0760280e57dbf119b7e9be6b161784a5a673ab2c6abe83abb5198a432232ad5b35c + languageName: node + linkType: hard + "is-boolean-object@npm:^1.1.0": version: 1.1.2 resolution: "is-boolean-object@npm:1.1.2" @@ -4415,6 +6354,20 @@ __metadata: languageName: node linkType: hard +"is-buffer@npm:~1.1.6": + version: 1.1.6 + resolution: "is-buffer@npm:1.1.6" + checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 + languageName: node + linkType: hard + +"is-buffer@npm:~2.0.3": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 + languageName: node + linkType: hard + "is-builtin-module@npm:^3.2.1": version: 3.2.1 resolution: "is-builtin-module@npm:3.2.1" @@ -4431,6 +6384,17 @@ __metadata: languageName: node linkType: hard +"is-ci@npm:^3.0.0": + version: 3.0.1 + resolution: "is-ci@npm:3.0.1" + dependencies: + ci-info: ^3.2.0 + bin: + is-ci: bin.js + checksum: 192c66dc7826d58f803ecae624860dccf1899fc1f3ac5505284c0a5cf5f889046ffeb958fa651e5725d5705c5bcb14f055b79150ea5fcad7456a9569de60260e + languageName: node + linkType: hard + "is-core-module@npm:^2.11.0, is-core-module@npm:^2.9.0": version: 2.11.0 resolution: "is-core-module@npm:2.11.0" @@ -4465,6 +6429,13 @@ __metadata: languageName: node linkType: hard +"is-fullwidth-code-point@npm:^2.0.0": + version: 2.0.0 + resolution: "is-fullwidth-code-point@npm:2.0.0" + checksum: eef9c6e15f68085fec19ff6a978a6f1b8f48018fd1265035552078ee945573594933b09bbd6f562553e2a241561439f1ef5339276eba68d272001343084cfab8 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -4479,7 +6450,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -4488,6 +6459,16 @@ __metadata: languageName: node linkType: hard +"is-installed-globally@npm:~0.4.0": + version: 0.4.0 + resolution: "is-installed-globally@npm:0.4.0" + dependencies: + global-dirs: ^3.0.0 + is-path-inside: ^3.0.2 + checksum: 3359840d5982d22e9b350034237b2cda2a12bac1b48a721912e1ab8e0631dd07d45a2797a120b7b87552759a65ba03e819f1bd63f2d7ab8657ec0b44ee0bf399 + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -4532,7 +6513,7 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": +"is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 @@ -4619,6 +6600,20 @@ __metadata: languageName: node linkType: hard +"is-typedarray@npm:~1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 3508c6cd0a9ee2e0df2fa2e9baabcdc89e911c7bd5cf64604586697212feec525aa21050e48affb5ffc3df20f0f5d2e2cf79b08caa64e1ccc9578e251763aef7 + languageName: node + linkType: hard + +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: a2aab86ee7712f5c2f999180daaba5f361bdad1efadc9610ff5b8ab5495b86e4f627839d085c6530363c6d6d4ecbde340fb8e54bdb83da4ba8e0865ed5513c52 + languageName: node + linkType: hard + "is-weakmap@npm:^2.0.1": version: 2.0.1 resolution: "is-weakmap@npm:2.0.1" @@ -4654,6 +6649,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:0.0.1": + version: 0.0.1 + resolution: "isarray@npm:0.0.1" + checksum: 49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4 + languageName: node + linkType: hard + "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -4661,6 +6663,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -4668,6 +6677,22 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:^4.0.1": + version: 4.0.1 + resolution: "isomorphic-ws@npm:4.0.1" + peerDependencies: + ws: "*" + checksum: d7190eadefdc28bdb93d67b5f0c603385aaf87724fa2974abb382ac1ec9756ed2cfb27065cbe76122879c2d452e2982bc4314317f3d6c737ddda6c047328771a + languageName: node + linkType: hard + +"isstream@npm:~0.1.2": + version: 0.1.2 + resolution: "isstream@npm:0.1.2" + checksum: 1eb2fe63a729f7bdd8a559ab552c69055f4f48eb5c2f03724430587c6f450783c8f1cd936c1c952d0a927925180fcc892ebd5b174236cf1065d4bd5bdb37e963 + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-lib-coverage@npm:3.2.0" @@ -4720,6 +6745,13 @@ __metadata: languageName: node linkType: hard +"iterall@npm:^1.2.1, iterall@npm:^1.3.0": + version: 1.3.0 + resolution: "iterall@npm:1.3.0" + checksum: c78b99678f8c99be488cca7f33e4acca9b72c1326e050afbaf023f086e55619ee466af0464af94a0cb3f292e60cb5bac53a8fd86bd4249ecad26e09f17bb158b + languageName: node + linkType: hard + "jest-changed-files@npm:^29.5.0": version: 29.5.0 resolution: "jest-changed-files@npm:29.5.0" @@ -4758,7 +6790,7 @@ __metadata: languageName: node linkType: hard -"jest-cli@npm:^29.5.0": +"jest-cli@npm:25.1.0 - 29, jest-cli@npm:^29.5.0": version: 29.5.0 resolution: "jest-cli@npm:29.5.0" dependencies: @@ -4857,7 +6889,7 @@ __metadata: languageName: node linkType: hard -"jest-environment-node@npm:^29.5.0": +"jest-environment-node@npm:25.1.0 - 29, jest-environment-node@npm:^29.5.0": version: 29.5.0 resolution: "jest-environment-node@npm:29.5.0" dependencies: @@ -4871,6 +6903,13 @@ __metadata: languageName: node linkType: hard +"jest-expect-message@npm:^1.1.3": + version: 1.1.3 + resolution: "jest-expect-message@npm:1.1.3" + checksum: 995f02fec4ed23da4f538208bdce8515cc4564a55e1353665fc4704966cc79094bad335f3f8c5b60f197110cf22da14e52c0e05609a1e36a093a210def8c1b5f + languageName: node + linkType: hard + "jest-get-type@npm:^29.4.3": version: 29.4.3 resolution: "jest-get-type@npm:29.4.3" @@ -4909,6 +6948,7 @@ __metadata: "@unflakable/js-api": "workspace:^" jest: 25.1.0 - 29 jest-each: 25.1.0 - 29 + jest-environment-node: 25.1.0 - 29 languageName: unknown linkType: soft @@ -4923,6 +6963,8 @@ __metadata: deep-equal: ^2.0.5 fetch-mock-jest: ^1.5.1 jest: 25.1.0 - 29 + jest-cli: 25.1.0 - 29 + jest-environment-node: 25.1.0 - 29 temp: ^0.9.4 languageName: unknown linkType: soft @@ -5113,7 +7155,7 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:25.1.0 - 29, jest-util@npm:^29.5.0": +"jest-util@npm:25.1.0 - 29, jest-util@npm:^29.0.0, jest-util@npm:^29.5.0": version: 29.5.0 resolution: "jest-util@npm:29.5.0" dependencies: @@ -5157,6 +7199,17 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^27.4.5": + version: 27.5.1 + resolution: "jest-worker@npm:27.5.1" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 98cd68b696781caed61c983a3ee30bf880b5bd021c01d98f47b143d4362b85d0737f8523761e2713d45e18b4f9a2b98af1eaee77afade4111bb65c77d6f7c980 + languageName: node + linkType: hard + "jest-worker@npm:^29.5.0": version: 29.5.0 resolution: "jest-worker@npm:29.5.0" @@ -5169,7 +7222,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:25.1.0 - 29": +"jest@npm:25.1.0 - 29, jest@npm:^29.5.0": version: 29.5.0 resolution: "jest@npm:29.5.0" dependencies: @@ -5195,13 +7248,25 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^4.0.0": +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78 languageName: node linkType: hard +"js-yaml@npm:3.13.1": + version: 3.13.1 + resolution: "js-yaml@npm:3.13.1" + dependencies: + argparse: ^1.0.7 + esprima: ^4.0.0 + bin: + js-yaml: bin/js-yaml.js + checksum: 7511b764abb66d8aa963379f7d2a404f078457d106552d05a7b556d204f7932384e8477513c124749fa2de52eb328961834562bd09924902c6432e40daa408bc + languageName: node + linkType: hard + "js-yaml@npm:^3.13.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -5225,6 +7290,13 @@ __metadata: languageName: node linkType: hard +"jsbn@npm:~0.1.0": + version: 0.1.1 + resolution: "jsbn@npm:0.1.1" + checksum: e5ff29c1b8d965017ef3f9c219dacd6e40ad355c664e277d31246c90545a02e6047018c16c60a00f36d561b3647215c41894f5d869ada6908a2e0ce4200c88f2 + languageName: node + linkType: hard + "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -5243,7 +7315,7 @@ __metadata: languageName: node linkType: hard -"json-parse-even-better-errors@npm:^2.3.0": +"json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" checksum: 798ed4cf3354a2d9ccd78e86d2169515a0097a5c133337807cdf7f1fc32e1391d207ccfc276518cc1d7d8d4db93288b8a50ba4293d212ad1336e52a8ec0a941f @@ -5257,6 +7329,13 @@ __metadata: languageName: node linkType: hard +"json-schema@npm:0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 66389434c3469e698da0df2e7ac5a3281bcff75e797a5c127db7c5b56270e01ae13d9afa3c03344f76e32e81678337a8c912bdbb75101c62e487dc3778461d72 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -5264,6 +7343,13 @@ __metadata: languageName: node linkType: hard +"json-stringify-safe@npm:~5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee + languageName: node + linkType: hard + "json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" @@ -5275,7 +7361,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.2.2": +"json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -5284,6 +7370,43 @@ __metadata: languageName: node linkType: hard +"jsonfile@npm:^4.0.0": + version: 4.0.0 + resolution: "jsonfile@npm:4.0.0" + dependencies: + graceful-fs: ^4.1.6 + dependenciesMeta: + graceful-fs: + optional: true + checksum: 6447d6224f0d31623eef9b51185af03ac328a7553efcee30fa423d98a9e276ca08db87d71e17f2310b0263fd3ffa6c2a90a6308367f661dc21580f9469897c9e + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: ^4.1.6 + universalify: ^2.0.0 + dependenciesMeta: + graceful-fs: + optional: true + checksum: 7af3b8e1ac8fe7f1eccc6263c6ca14e1966fcbc74b618d3c78a0a2075579487547b94f72b7a1114e844a1e15bb00d440e5d1720bfc4612d790a6f285d5ea8354 + languageName: node + linkType: hard + +"jsprim@npm:^2.0.2": + version: 2.0.2 + resolution: "jsprim@npm:2.0.2" + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + checksum: d175f6b1991e160cb0aa39bc857da780e035611986b5492f32395411879fdaf4e513d98677f08f7352dac93a16b66b8361c674b86a3fa406e2e7af6b26321838 + languageName: node + linkType: hard + "kleur@npm:^3.0.3": version: 3.0.3 resolution: "kleur@npm:3.0.3" @@ -5291,6 +7414,13 @@ __metadata: languageName: node linkType: hard +"lazy-ass@npm:^1.6.0": + version: 1.6.0 + resolution: "lazy-ass@npm:1.6.0" + checksum: 5a3ebb17915b03452320804466345382a6c25ac782ec4874fecdb2385793896cd459be2f187dc7def8899180c32ee0ab9a1aa7fe52193ac3ff3fe29bb0591729 + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -5308,6 +7438,16 @@ __metadata: languageName: node linkType: hard +"levn@npm:~0.3.0": + version: 0.3.0 + resolution: "levn@npm:0.3.0" + dependencies: + prelude-ls: ~1.1.2 + type-check: ~0.3.2 + checksum: 0d084a524231a8246bb10fec48cdbb35282099f6954838604f3c7fc66f2e16fa66fd9cc2f3f20a541a113c4dafdf181e822c887c8a319c9195444e6c64ac395e + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -5315,6 +7455,44 @@ __metadata: languageName: node linkType: hard +"listr2@npm:^3.8.3": + version: 3.14.0 + resolution: "listr2@npm:3.14.0" + dependencies: + cli-truncate: ^2.1.0 + colorette: ^2.0.16 + log-update: ^4.0.0 + p-map: ^4.0.0 + rfdc: ^1.3.0 + rxjs: ^7.5.1 + through: ^2.3.8 + wrap-ansi: ^7.0.0 + peerDependencies: + enquirer: ">= 2.3.0 < 3" + peerDependenciesMeta: + enquirer: + optional: true + checksum: fdb8b2d6bdf5df9371ebd5082bee46c6d0ca3d1e5f2b11fbb5a127839855d5f3da9d4968fce94f0a5ec67cac2459766abbb1faeef621065ebb1829b11ef9476d + languageName: node + linkType: hard + +"loader-runner@npm:^4.2.0": + version: 4.3.0 + resolution: "loader-runner@npm:4.3.0" + checksum: a90e00dee9a16be118ea43fec3192d0b491fe03a32ed48a4132eb61d498f5536a03a1315531c19d284392a8726a4ecad71d82044c28d7f22ef62e029bf761569 + languageName: node + linkType: hard + +"locate-path@npm:^3.0.0": + version: 3.0.0 + resolution: "locate-path@npm:3.0.0" + dependencies: + p-locate: ^3.0.0 + path-exists: ^3.0.0 + checksum: 53db3996672f21f8b0bf2a2c645ae2c13ffdae1eeecfcd399a583bce8516c0b88dcb4222ca6efbbbeb6949df7e46860895be2c02e8d3219abd373ace3bfb4e11 + languageName: node + linkType: hard + "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -5347,6 +7525,13 @@ __metadata: languageName: node linkType: hard +"lodash.memoize@npm:4.x": + version: 4.1.2 + resolution: "lodash.memoize@npm:4.1.2" + checksum: 9ff3942feeccffa4f1fafa88d32f0d24fdc62fd15ded5a74a5f950ff5f0c6f61916157246744c620173dddf38d37095a92327d5fd3861e2063e736a5c207d089 + languageName: node + linkType: hard + "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -5354,6 +7539,13 @@ __metadata: languageName: node linkType: hard +"lodash.once@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245 + languageName: node + linkType: hard + "lodash.sortby@npm:^4.7.0": version: 4.7.0 resolution: "lodash.sortby@npm:4.7.0" @@ -5361,6 +7553,55 @@ __metadata: languageName: node linkType: hard +"lodash@npm:^4.16.4, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 + languageName: node + linkType: hard + +"log-symbols@npm:2.2.0": + version: 2.2.0 + resolution: "log-symbols@npm:2.2.0" + dependencies: + chalk: ^2.0.1 + checksum: 4c95e3b65f0352dbe91dc4989c10baf7a44e2ef5b0db7e6721e1476268e2b6f7090c3aa880d4f833a05c5c3ff18f4ec5215a09bd0099986d64a8186cfeb48ac8 + languageName: node + linkType: hard + +"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: ^4.1.0 + is-unicode-supported: ^0.1.0 + checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 + languageName: node + linkType: hard + +"log-update@npm:^4.0.0": + version: 4.0.0 + resolution: "log-update@npm:4.0.0" + dependencies: + ansi-escapes: ^4.3.0 + cli-cursor: ^3.1.0 + slice-ansi: ^4.0.0 + wrap-ansi: ^6.2.0 + checksum: ae2f85bbabc1906034154fb7d4c4477c79b3e703d22d78adee8b3862fa913942772e7fa11713e3d96fb46de4e3cabefbf5d0a544344f03b58d3c4bff52aa9eb2 + languageName: node + linkType: hard + +"loose-envify@npm:^1.1.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: ^3.0.0 || ^4.0.0 + bin: + loose-envify: cli.js + checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -5379,10 +7620,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.4.0": - version: 7.4.1 - resolution: "lru-cache@npm:7.4.1" - checksum: de6d8aa83b0a304943a1e1ec2a982d9b44ef36963c20bd01950f13dcb3eb482c57bfa15ab49b1baa15c682d6d33a10842bb7b33d8a449845009ffab6a2faec3c +"lru-cache@npm:^7.14.0, lru-cache@npm:^7.4.0": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 languageName: node linkType: hard @@ -5404,7 +7645,7 @@ __metadata: languageName: node linkType: hard -"make-error@npm:^1.1.1": +"make-error@npm:1.x, make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 @@ -5444,6 +7685,31 @@ __metadata: languageName: node linkType: hard +"md5@npm:^2.3.0": + version: 2.3.0 + resolution: "md5@npm:2.3.0" + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: ~1.1.6 + checksum: a63cacf4018dc9dee08c36e6f924a64ced735b37826116c905717c41cebeb41a522f7a526ba6ad578f9c80f02cb365033ccd67fe186ffbcc1a1faeb75daa9b6e + languageName: node + linkType: hard + +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 + languageName: node + linkType: hard + +"merge-descriptors@npm:1.0.1": + version: 1.0.1 + resolution: "merge-descriptors@npm:1.0.1" + checksum: 5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -5458,13 +7724,20 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4": - version: 4.0.4 - resolution: "micromatch@npm:4.0.4" +"methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a + languageName: node + linkType: hard + +"micromatch@npm:^4.0.0, micromatch@npm:^4.0.4": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" dependencies: - braces: ^3.0.1 - picomatch: ^2.2.3 - checksum: ef3d1c88e79e0a68b0e94a03137676f3324ac18a908c245a9e5936f838079fcc108ac7170a5fadc265a9c2596963462e402841406bda1a4bb7b68805601d631c + braces: ^3.0.2 + picomatch: ^2.3.1 + checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc languageName: node linkType: hard @@ -5475,7 +7748,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -5484,6 +7757,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: fef25e39263e6d207580bdc629f8872a3f9772c923c7f8c7e793175cee22777bbe8bba95e5d509a40aaa292d8974514ce634ae35769faa45f22d17edda5e8557 + languageName: node + linkType: hard + "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -5491,6 +7773,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:3.0.4": + version: 3.0.4 + resolution: "minimatch@npm:3.0.4" + dependencies: + brace-expansion: ^1.1.7 + checksum: 66ac295f8a7b59788000ea3749938b0970344c841750abd96694f80269b926ebcafad3deeb3f1da2522978b119e6ae3a5869b63b13a7859a456b3408bd18a078 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -5509,7 +7800,14 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": +"minimist@npm:0.0.8": + version: 0.0.8 + resolution: "minimist@npm:0.0.8" + checksum: 042f8b626b1fa44dffc23bac55771425ac4ee9d267b56f9064c07713e516e1799f3ba933bb628d2475a210caf7dcdb98161611baa1f0daf49309a944cb4bc48f + languageName: node + linkType: hard + +"minimist@npm:^1.2.0, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 @@ -5586,18 +7884,29 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1": - version: 0.5.5 - resolution: "mkdirp@npm:0.5.5" +"mkdirp@npm:0.5.1": + version: 0.5.1 + resolution: "mkdirp@npm:0.5.1" + dependencies: + minimist: 0.0.8 + bin: + mkdirp: bin/cmd.js + checksum: ed1ab49bb1d06c88dba7cfe930a3186f2605b5465aab7c8f24119baaba6e38f9ab4ac1695c68f476c65a48df2a69a8495049cd6e26c360ea082151a0771343d2 + languageName: node + linkType: hard + +"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.5": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" dependencies: - minimist: ^1.2.5 + minimist: ^1.2.6 bin: mkdirp: bin/cmd.js - checksum: 3bce20ea525f9477befe458ab85284b0b66c8dc3812f94155af07c827175948cdd8114852ac6c6d82009b13c1048c37f6d98743eb019651ee25c39acc8aabe7d + checksum: 0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 languageName: node linkType: hard -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": +"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4, mkdirp@npm:~1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" bin: @@ -5606,6 +7915,119 @@ __metadata: languageName: node linkType: hard +"mocha-junit-reporter@npm:^2.2.0": + version: 2.2.0 + resolution: "mocha-junit-reporter@npm:2.2.0" + dependencies: + debug: ^4.3.4 + md5: ^2.3.0 + mkdirp: ~1.0.4 + strip-ansi: ^6.0.1 + xml: ^1.0.1 + peerDependencies: + mocha: ">=2.2.5" + checksum: 476dc12b676b8313ddac8ed27c5b43c86d6d54770efa0081a35a58f3a6717a504cc871058850da0611e7fc42e17201332390dc516096d51a189f12ff2141d420 + languageName: node + linkType: hard + +"mocha@npm:=7.0.1": + version: 7.0.1 + resolution: "mocha@npm:7.0.1" + dependencies: + ansi-colors: 3.2.3 + browser-stdout: 1.3.1 + chokidar: 3.3.0 + debug: 3.2.6 + diff: 3.5.0 + escape-string-regexp: 1.0.5 + find-up: 3.0.0 + glob: 7.1.3 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 3.13.1 + log-symbols: 2.2.0 + minimatch: 3.0.4 + mkdirp: 0.5.1 + ms: 2.1.1 + node-environment-flags: 1.0.6 + object.assign: 4.1.0 + strip-json-comments: 2.0.1 + supports-color: 6.0.0 + which: 1.3.1 + wide-align: 1.1.3 + yargs: 13.3.0 + yargs-parser: 13.1.1 + yargs-unparser: 1.6.0 + bin: + _mocha: bin/_mocha + mocha: bin/mocha + checksum: df8d66a53412503ba0c6f9e0e843940603e9811c8c893cfed0c600c7c670b8f8f3f462c6828229adb77d34ea0e0c20cbe499afd283a0a801b7996cc1fc586b83 + languageName: node + linkType: hard + +"mockttp@npm:^3.7.5": + version: 3.7.5 + resolution: "mockttp@npm:3.7.5" + dependencies: + "@graphql-tools/schema": ^8.5.0 + "@graphql-tools/utils": ^8.8.0 + "@httptoolkit/httpolyglot": ^2.1.1 + "@httptoolkit/subscriptions-transport-ws": ^0.11.2 + "@httptoolkit/websocket-stream": ^6.0.1 + "@types/cors": ^2.8.6 + "@types/node": "*" + base64-arraybuffer: ^0.1.5 + body-parser: ^1.15.2 + cacheable-lookup: ^6.0.0 + common-tags: ^1.8.0 + connect: ^3.7.0 + cors: ^2.8.4 + cors-gate: ^1.1.3 + cross-fetch: ^3.1.5 + destroyable-server: ^1.0.0 + express: ^4.14.0 + express-graphql: ^0.11.0 + graphql: ^14.0.2 || ^15.5 + graphql-subscriptions: ^1.1.0 + graphql-tag: ^2.12.6 + http-encoding: ^1.5.1 + http2-wrapper: ^2.2.0 + https-proxy-agent: ^5.0.1 + isomorphic-ws: ^4.0.1 + lodash: ^4.16.4 + lru-cache: ^7.14.0 + native-duplexpair: ^1.0.0 + node-forge: ^1.2.1 + pac-proxy-agent: ^5.0.0 + parse-multipart-data: ^1.4.0 + performance-now: ^2.1.0 + portfinder: 1.0.28 + read-tls-client-hello: ^1.0.0 + semver: ^5.7.1 + socks-proxy-agent: ^7.0.0 + typed-error: ^3.0.2 + uuid: ^8.3.2 + ws: ^8.8.0 + bin: + mockttp: dist/admin/admin-bin.js + checksum: 0fe524add6bf879385584db3e98c9b3d6e16d7fc8ae175da23b2236ee61f0674f623099fa06c4cf5bd0008955445c34cb9520e639dfdcbbcf549b633445b56f9 + languageName: node + linkType: hard + +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 0e6a22b8b746d2e0b65a430519934fefd41b6db0682e3477c10f60c76e947c4c0ad06f63ffdf1d78d335f83edee8c0aa928aa66a36c7cd95b69b26f468d527f4 + languageName: node + linkType: hard + +"ms@npm:2.1.1": + version: 2.1.1 + resolution: "ms@npm:2.1.1" + checksum: 0078a23cd916a9a7435c413caa14c57d4b4f6e2470e0ab554b6964163c8a4436448ac7ae020e883685475da6b6796cc396b670f579cb275db288a21e3e57721e + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -5613,13 +8035,20 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d languageName: node linkType: hard +"native-duplexpair@npm:^1.0.0": + version: 1.0.0 + resolution: "native-duplexpair@npm:1.0.0" + checksum: d849a8cb78c59eb12326fde2a84fedc26568b4317da46d061e7110a35961230b674a04ec2496860c2eb5f05288176c7ce0eb3a51eb0ed6b76a4263f637461f9d + languageName: node + linkType: hard + "natural-compare-lite@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare-lite@npm:1.4.0" @@ -5634,16 +8063,40 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 languageName: node linkType: hard -"node-fetch@npm:^2.6.7": - version: 2.6.7 - resolution: "node-fetch@npm:2.6.7" +"neo-async@npm:^2.6.2": + version: 2.6.2 + resolution: "neo-async@npm:2.6.2" + checksum: deac9f8d00eda7b2e5cd1b2549e26e10a0faa70adaa6fdadca701cc55f49ee9018e427f424bac0c790b7c7e2d3068db97f3093f1093975f2acb8f8818b936ed9 + languageName: node + linkType: hard + +"netmask@npm:^2.0.2": + version: 2.0.2 + resolution: "netmask@npm:2.0.2" + checksum: c65cb8d3f7ea5669edddb3217e4c96910a60d0d9a4b52d9847ff6b28b2d0277cd8464eee0ef85133cdee32605c57940cacdd04a9a019079b091b6bba4cb0ec22 + languageName: node + linkType: hard + +"node-environment-flags@npm:1.0.6": + version: 1.0.6 + resolution: "node-environment-flags@npm:1.0.6" + dependencies: + object.getownpropertydescriptors: ^2.0.3 + semver: ^5.7.0 + checksum: 268139ed0f7fabdca346dcb26931300ec7a1dc54a58085a849e5c78a82b94967f55df40177a69d4e819da278d98686d5c4fd49ab0d7bcff16fda25b6fffc4ca3 + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7": + version: 2.6.11 + resolution: "node-fetch@npm:2.6.11" dependencies: whatwg-url: ^5.0.0 peerDependencies: @@ -5651,7 +8104,14 @@ __metadata: peerDependenciesMeta: encoding: optional: true - checksum: 8d816ffd1ee22cab8301c7756ef04f3437f18dace86a1dae22cf81db8ef29c0bf6655f3215cb0cdb22b420b6fe141e64b26905e7f33f9377a7fa59135ea3e10b + checksum: 249d0666a9497553384d46b5ab296ba223521ac88fed4d8a17d6ee6c2efb0fc890f3e8091cafe7f9fba8151a5b8d925db2671543b3409a56c3cd522b468b47b3 + languageName: node + linkType: hard + +"node-forge@npm:^1.2.1": + version: 1.3.1 + resolution: "node-forge@npm:1.3.1" + checksum: 08fb072d3d670599c89a1704b3e9c649ff1b998256737f0e06fbd1a5bf41cae4457ccaee32d95052d80bbafd9ffe01284e078c8071f0267dc9744e51c5ed42a9 languageName: node linkType: hard @@ -5700,14 +8160,14 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0": +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 languageName: node linkType: hard -"npm-run-path@npm:^4.0.1": +"npm-run-path@npm:^4.0.0, npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" dependencies: @@ -5728,6 +8188,13 @@ __metadata: languageName: node linkType: hard +"object-assign@npm:^4, object-assign@npm:^4.1.0": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + "object-inspect@npm:^1.12.3, object-inspect@npm:^1.9.0": version: 1.12.3 resolution: "object-inspect@npm:1.12.3" @@ -5745,13 +8212,25 @@ __metadata: languageName: node linkType: hard -"object-keys@npm:^1.1.1": +"object-keys@npm:^1.0.11, object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" checksum: b363c5e7644b1e1b04aa507e88dcb8e3a2f52b6ffd0ea801e4c7a62d5aa559affe21c55a07fd4b1fd55fc03a33c610d73426664b20032405d7b92a1414c34d6a languageName: node linkType: hard +"object.assign@npm:4.1.0": + version: 4.1.0 + resolution: "object.assign@npm:4.1.0" + dependencies: + define-properties: ^1.1.2 + function-bind: ^1.1.1 + has-symbols: ^1.0.0 + object-keys: ^1.0.11 + checksum: 648a9a463580bf48332d9a49a76fede2660ab1ee7104d9459b8a240562246da790b4151c3c073f28fda31c1fdc555d25a1d871e72be403e997e4468c91f4801f + languageName: node + linkType: hard + "object.assign@npm:^4.1.2, object.assign@npm:^4.1.4": version: 4.1.4 resolution: "object.assign@npm:4.1.4" @@ -5764,6 +8243,19 @@ __metadata: languageName: node linkType: hard +"object.getownpropertydescriptors@npm:^2.0.3": + version: 2.1.6 + resolution: "object.getownpropertydescriptors@npm:2.1.6" + dependencies: + array.prototype.reduce: ^1.0.5 + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.21.2 + safe-array-concat: ^1.0.0 + checksum: 7757ce0ef61c8bee7f8043f8980fd3d46fc1ab3faf0795bd1f9f836781143b4afc91f7219a3eed4675fbd0b562f3708f7e736d679ebfd43ea37ab6077d9f5004 + languageName: node + linkType: hard + "object.values@npm:^1.1.6": version: 1.1.6 resolution: "object.values@npm:1.1.6" @@ -5775,7 +8267,25 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0": +"on-finished@npm:2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: 1.1.1 + checksum: d20929a25e7f0bb62f937a425b5edeb4e4cde0540d77ba146ec9357f00b0d497cdb3b9b05b9c8e46222407d1548d08166bff69cc56dfa55ba0e4469228920ff0 + languageName: node + linkType: hard + +"on-finished@npm:~2.3.0": + version: 2.3.0 + resolution: "on-finished@npm:2.3.0" + dependencies: + ee-first: 1.1.1 + checksum: 1db595bd963b0124d6fa261d18320422407b8f01dc65863840f3ddaaf7bcad5b28ff6847286703ca53f4ec19595bd67a2f1253db79fc4094911ec6aa8df1671b + languageName: node + linkType: hard + +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -5784,7 +8294,7 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^5.1.2": +"onetime@npm:^5.1.0, onetime@npm:^5.1.2": version: 5.1.2 resolution: "onetime@npm:5.1.2" dependencies: @@ -5804,6 +8314,20 @@ __metadata: languageName: node linkType: hard +"optionator@npm:^0.8.1": + version: 0.8.3 + resolution: "optionator@npm:0.8.3" + dependencies: + deep-is: ~0.1.3 + fast-levenshtein: ~2.0.6 + levn: ~0.3.0 + prelude-ls: ~1.1.2 + type-check: ~0.3.2 + word-wrap: ~1.2.3 + checksum: b8695ddf3d593203e25ab0900e265d860038486c943ff8b774f596a310f8ceebdb30c6832407a8198ba3ec9debe1abe1f51d4aad94843612db3b76d690c61d34 + languageName: node + linkType: hard + "optionator@npm:^0.9.1": version: 0.9.1 resolution: "optionator@npm:0.9.1" @@ -5818,7 +8342,14 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^2.2.0": +"ospath@npm:^1.2.2": + version: 1.2.2 + resolution: "ospath@npm:1.2.2" + checksum: 505f48a4f4f1c557d6c656ec985707726e3714721680139be037613e903aa8c8fa4ddd8d1342006f9b2dc0065e6e20f8b7bea2ee05354f31257044790367b347 + languageName: node + linkType: hard + +"p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" dependencies: @@ -5836,6 +8367,15 @@ __metadata: languageName: node linkType: hard +"p-locate@npm:^3.0.0": + version: 3.0.0 + resolution: "p-locate@npm:3.0.0" + dependencies: + p-limit: ^2.0.0 + checksum: 83991734a9854a05fe9dbb29f707ea8a0599391f52daac32b86f08e21415e857ffa60f0e120bfe7ce0cc4faf9274a50239c7895fc0d0579d08411e513b83a4ae + languageName: node + linkType: hard + "p-locate@npm:^4.1.0": version: 4.1.0 resolution: "p-locate@npm:4.1.0" @@ -5870,6 +8410,34 @@ __metadata: languageName: node linkType: hard +"pac-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "pac-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": 1 + agent-base: 6 + debug: 4 + get-uri: 3 + http-proxy-agent: ^4.0.1 + https-proxy-agent: 5 + pac-resolver: ^5.0.0 + raw-body: ^2.2.0 + socks-proxy-agent: 5 + checksum: cfd26a0e2ebfea4ca6162465018ce093bf147d26cf6c8fb3e7155bc7c184370d80d4d09a1c097e3db7676d0e3f574ea1cb56a4aa7d1d2e5cca6238935fabf010 + languageName: node + linkType: hard + +"pac-resolver@npm:^5.0.0": + version: 5.0.1 + resolution: "pac-resolver@npm:5.0.1" + dependencies: + degenerator: ^3.0.2 + ip: ^1.1.5 + netmask: ^2.0.2 + checksum: e3bd8aada70d173cd4cec1ac810fb56161678b7a597060a740c4a31d9c5f8cd95687b2d0fd90b69c0cafe5ef787404074f38042ba08c8d378fed48973f58e493 + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -5891,6 +8459,34 @@ __metadata: languageName: node linkType: hard +"parse-multipart-data@npm:^1.4.0": + version: 1.5.0 + resolution: "parse-multipart-data@npm:1.5.0" + checksum: a385fb6609a7b393ee7e82042d5f923beaa7fb7d81d430db560869b719574f62f39a30e77fd711fbfa6fe3e212a8e6f81fd2126a80876a3c13dc1ae975eb5d91 + languageName: node + linkType: hard + +"parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 + languageName: node + linkType: hard + +"path-browserify@npm:^1.0.1": + version: 1.0.1 + resolution: "path-browserify@npm:1.0.1" + checksum: c6d7fa376423fe35b95b2d67990060c3ee304fc815ff0a2dc1c6c3cfaff2bd0d572ee67e18f19d0ea3bbe32e8add2a05021132ac40509416459fffee35200699 + languageName: node + linkType: hard + +"path-exists@npm:^3.0.0": + version: 3.0.0 + resolution: "path-exists@npm:3.0.0" + checksum: 96e92643aa34b4b28d0de1cd2eba52a1c5313a90c6542d03f62750d82480e20bfa62bc865d5cfc6165f5fcd5aeb0851043c40a39be5989646f223300021bae0a + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -5919,6 +8515,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:0.1.7": + version: 0.1.7 + resolution: "path-to-regexp@npm:0.1.7" + checksum: 69a14ea24db543e8b0f4353305c5eac6907917031340e5a8b37df688e52accd09e3cebfe1660b70d76b6bd89152f52183f28c74813dbf454ba1a01c82a38abce + languageName: node + linkType: hard + "path-to-regexp@npm:^2.2.1": version: 2.4.0 resolution: "path-to-regexp@npm:2.4.0" @@ -5933,6 +8536,20 @@ __metadata: languageName: node linkType: hard +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d + languageName: node + linkType: hard + +"performance-now@npm:^2.1.0": + version: 2.1.0 + resolution: "performance-now@npm:2.1.0" + checksum: 534e641aa8f7cba160f0afec0599b6cecefbb516a2e837b512be0adbe6c1da5550e89c78059c7fabc5c9ffdf6627edabe23eb7c518c4500067a898fa65c2b550 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -5947,6 +8564,20 @@ __metadata: languageName: node linkType: hard +"pify@npm:^2.2.0": + version: 2.3.0 + resolution: "pify@npm:2.3.0" + checksum: 9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba + languageName: node + linkType: hard + +"pify@npm:^5.0.0": + version: 5.0.0 + resolution: "pify@npm:5.0.0" + checksum: 443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -5963,6 +8594,17 @@ __metadata: languageName: node linkType: hard +"portfinder@npm:1.0.28": + version: 1.0.28 + resolution: "portfinder@npm:1.0.28" + dependencies: + async: ^2.6.2 + debug: ^3.1.1 + mkdirp: ^0.5.5 + checksum: 91fef602f13f8f4c64385d0ad2a36cc9dc6be0b8d10a2628ee2c3c7b9917ab4fefb458815b82cea2abf4b785cd11c9b4e2d917ac6fa06f14b6fa880ca8f8928c + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -5970,15 +8612,29 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.5.1": - version: 2.5.1 - resolution: "prettier@npm:2.5.1" +"prelude-ls@npm:~1.1.2": + version: 1.1.2 + resolution: "prelude-ls@npm:1.1.2" + checksum: c4867c87488e4a0c233e158e4d0d5565b609b105d75e4c05dc760840475f06b731332eb93cc8c9cecb840aa8ec323ca3c9a56ad7820ad2e63f0261dadcb154e4 + languageName: node + linkType: hard + +"prettier@npm:^2.5.1": + version: 2.5.1 + resolution: "prettier@npm:2.5.1" bin: prettier: bin-prettier.js checksum: 21b9408476ea1c544b0e45d51ceb94a84789ff92095abb710942d780c862d0daebdb29972d47f6b4d0f7ebbfb0ffbf56cc2cfa3e3e9d1cca54864af185b15b66 languageName: node linkType: hard +"pretty-bytes@npm:^5.6.0": + version: 5.6.0 + resolution: "pretty-bytes@npm:5.6.0" + checksum: 9c082500d1e93434b5b291bd651662936b8bd6204ec9fa17d563116a192d6d86b98f6d328526b4e8d783c07d5499e2614a807520249692da9ec81564b2f439cd + languageName: node + linkType: hard + "pretty-format@npm:^29.0.0, pretty-format@npm:^29.5.0": version: 29.5.0 resolution: "pretty-format@npm:29.5.0" @@ -5990,6 +8646,20 @@ __metadata: languageName: node linkType: hard +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: bfcce49814f7d172a6e6a14d5fa3ac92cc3d0c3b9feb1279774708a719e19acd673995226351a082a9ae99978254e320ccda4240ddc474ba31a76c79491ca7c3 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -6017,7 +8687,41 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0": +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + checksum: 29c6990ce9364648255454842f06f8c46fcd124d3e6d7c5066df44662de63cdc0bad032e9bf5a3d653ff72141cc7b6019873d685708ac8210c30458ad99f2b74 + languageName: node + linkType: hard + +"proxy-from-env@npm:1.0.0": + version: 1.0.0 + resolution: "proxy-from-env@npm:1.0.0" + checksum: 292e28d1de0c315958d71d8315eb546dd3cd8c8cbc2dab7c54eeb9f5c17f421771964ad0b5e1f77011bab2305bdae42e1757ce33bdb1ccc3e87732322a8efcf1 + languageName: node + linkType: hard + +"psl@npm:^1.1.28": + version: 1.9.0 + resolution: "psl@npm:1.9.0" + checksum: 20c4277f640c93d393130673f392618e9a8044c6c7bf61c53917a0fddb4952790f5f362c6c730a9c32b124813e173733f9895add8d26f566ed0ea0654b2e711d + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: ^1.1.0 + once: ^1.3.1 + checksum: e42e9229fba14732593a718b04cb5e1cfef8254544870997e0ecd9732b189a48e1256e4e5478148ecb47c8511dca2b09eae56b4d0aad8009e6fac8072923cfc9 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.3.0 resolution: "punycode@npm:2.3.0" checksum: 39f760e09a2a3bbfe8f5287cf733ecdad69d6af2fe6f97ca95f24b8921858b91e9ea3c9eeec6e08cede96181b3bb33f95c6ffd8c77e63986508aa2e8159fa200 @@ -6031,6 +8735,24 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.11.0": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: ^1.0.4 + checksum: 6e1f29dd5385f7488ec74ac7b6c92f4d09a90408882d0c208414a34dd33badc1a621019d4c799a3df15ab9b1d0292f97c1dd71dc7c045e69f81a8064e5af7297 + languageName: node + linkType: hard + +"qs@npm:~6.10.3": + version: 6.10.4 + resolution: "qs@npm:6.10.4" + dependencies: + side-channel: ^1.0.4 + checksum: 31e4fedd759d01eae52dde6692abab175f9af3e639993c5caaa513a2a3607b34d8058d3ae52ceeccf37c3025f22ed5e90e9ddd6c2537e19c0562ddd10dc5b1eb + languageName: node + linkType: hard + "querystring@npm:^0.2.0": version: 0.2.1 resolution: "querystring@npm:0.2.1" @@ -6045,6 +8767,65 @@ __metadata: languageName: node linkType: hard +"quick-lru@npm:^5.1.1": + version: 5.1.1 + resolution: "quick-lru@npm:5.1.1" + checksum: a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed + languageName: node + linkType: hard + +"randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: ^5.1.0 + checksum: d779499376bd4cbb435ef3ab9a957006c8682f343f14089ed5f27764e4645114196e75b7f6abf1cbd84fd247c0cb0651698444df8c9bf30e62120fbbc52269d6 + languageName: node + linkType: hard + +"range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 + languageName: node + linkType: hard + +"raw-body@npm:2.5.1": + version: 2.5.1 + resolution: "raw-body@npm:2.5.1" + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + checksum: 5362adff1575d691bb3f75998803a0ffed8c64eabeaa06e54b4ada25a0cd1b2ae7f4f5ec46565d1bec337e08b5ac90c76eaa0758de6f72a633f025d754dec29e + languageName: node + linkType: hard + +"raw-body@npm:2.5.2, raw-body@npm:^2.2.0, raw-body@npm:^2.4.1": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + checksum: ba1583c8d8a48e8fbb7a873fdbb2df66ea4ff83775421bfe21ee120140949ab048200668c47d9ae3880012f6e217052690628cf679ddfbd82c9fc9358d574676 + languageName: node + linkType: hard + +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc + languageName: node + linkType: hard + "react-is@npm:^18.0.0": version: 18.2.0 resolution: "react-is@npm:18.2.0" @@ -6052,6 +8833,51 @@ __metadata: languageName: node linkType: hard +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b + languageName: node + linkType: hard + +"read-tls-client-hello@npm:^1.0.0": + version: 1.0.1 + resolution: "read-tls-client-hello@npm:1.0.1" + dependencies: + "@types/node": "*" + checksum: 532c1c32ef049c245b59473ad7a06ad5db61bd22258ccfb54923be24173e8cafbb1a6a17bcc783884dce9b98db15db76a9569ea9c95b2b9b729be990439b931b + languageName: node + linkType: hard + +"readable-stream@npm:1.1.x": + version: 1.1.14 + resolution: "readable-stream@npm:1.1.14" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.1 + isarray: 0.0.1 + string_decoder: ~0.10.x + checksum: 17dfeae3e909945a4a1abc5613ea92d03269ef54c49288599507fc98ff4615988a1c39a999dcf9aacba70233d9b7040bc11a5f2bfc947e262dedcc0a8b32b5a0 + languageName: node + linkType: hard + +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.3.3": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.3 + isarray: ~1.0.0 + process-nextick-args: ~2.0.0 + safe-buffer: ~5.1.1 + string_decoder: ~1.1.1 + util-deprecate: ~1.0.1 + checksum: 65645467038704f0c8aaf026a72fbb588a9e2ef7a75cd57a01702ee9db1c4a1e4b03aaad36861a6a0926546a74d174149c8c207527963e0c2d3eee2f37678a42 + languageName: node + linkType: hard + "readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" @@ -6063,6 +8889,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.2.0": + version: 3.2.0 + resolution: "readdirp@npm:3.2.0" + dependencies: + picomatch: ^2.0.4 + checksum: 0456a4465a13eb5eaf40f0e0836b1bc6b9ebe479b48ba6f63a738b127a1990fb7b38f3ec4b4b6052f9230f976bc0558f12812347dc6b42ce4d548cfe82a9b6f3 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.0 resolution: "regenerate-unicode-properties@npm:10.1.0" @@ -6131,6 +8966,15 @@ __metadata: languageName: node linkType: hard +"request-progress@npm:^3.0.0": + version: 3.0.0 + resolution: "request-progress@npm:3.0.0" + dependencies: + throttleit: ^1.0.0 + checksum: 6ea1761dcc8a8b7b5894afd478c0286aa31bd69438d7050294bd4fd0d0b3e09b5cde417d38deef9c49809039c337d8744e4bb49d8632b0c3e4ffa5e8a687e0fd + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -6138,6 +8982,20 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7 + languageName: node + linkType: hard + +"resolve-alpn@npm:^1.2.0": + version: 1.2.1 + resolution: "resolve-alpn@npm:1.2.1" + checksum: f558071fcb2c60b04054c99aebd572a2af97ef64128d59bef7ab73bd50d896a222a056de40ffc545b633d99b304c259ea9d0c06830d5c867c34f0bfa60b8eae0 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -6194,6 +9052,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: ^5.1.0 + signal-exit: ^3.0.2 + checksum: f877dd8741796b909f2a82454ec111afb84eb45890eb49ac947d87991379406b3b83ff9673a46012fca0d7844bb989f45cc5b788254cf1a39b6b5a9659de0630 + languageName: node + linkType: hard + "retry@npm:0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" @@ -6215,7 +9083,14 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.2": +"rfdc@npm:^1.3.0": + version: 1.3.0 + resolution: "rfdc@npm:1.3.0" + checksum: fb2ba8512e43519983b4c61bd3fa77c0f410eff6bae68b08614437bc3f35f91362215f7b4a73cbda6f67330b5746ce07db5dd9850ad3edc91271ad6deea0df32 + languageName: node + linkType: hard + +"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" dependencies: @@ -6258,8 +9133,6 @@ __metadata: "@babel/core": ^7.21.5 "@babel/preset-env": ^7.21.5 "@babel/preset-typescript": ^7.21.5 - "@rollup/plugin-node-resolve": ^15.0.2 - "@rollup/plugin-typescript": ^11.1.0 "@types/debug": ^4.1.7 "@types/js-yaml": ^4.0.5 "@types/node": ^14.18.43 @@ -6269,11 +9142,11 @@ __metadata: debug: ^4.3.3 eslint: ^8.39.0 eslint-import-resolver-typescript: ^3.5.5 + eslint-plugin-cypress: ^2.13.3 eslint-plugin-import: ^2.27.5 eslint-plugin-jest: ^27.2.1 js-yaml: ^4.1.0 prettier: ^2.5.1 - rollup: ^3.21.1 semver: ^7.5.1 ts-node: ^10.7.0 typescript: ^4.9.5 @@ -6289,13 +9162,41 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.2.0": +"rxjs@npm:^7.5.1": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: ^2.1.0 + checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 + languageName: node + linkType: hard + +"safe-array-concat@npm:^1.0.0": + version: 1.0.0 + resolution: "safe-array-concat@npm:1.0.0" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.2.0 + has-symbols: ^1.0.3 + isarray: ^2.0.5 + checksum: f43cb98fe3b566327d0c09284de2b15fb85ae964a89495c1b1a5d50c7c8ed484190f4e5e71aacc167e16231940079b326f2c0807aea633d47cc7322f40a6b57f + languageName: node + linkType: hard + +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.2, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 languageName: node linkType: hard +"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: f2f1f7943ca44a594893a852894055cf619c1fbcb611237fc39e461ae751187e7baf4dc391a72125e0ac4fb2d8c5c0b3c71529622e6a58f46b960211e704903c + languageName: node + linkType: hard + "safe-regex-test@npm:^1.0.0": version: 1.0.0 resolution: "safe-regex-test@npm:1.0.0" @@ -6307,23 +9208,34 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.3.0": - version: 6.3.0 - resolution: "semver@npm:6.3.0" - bin: - semver: ./bin/semver.js - checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: ^1.1.0 + checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a + languageName: node + linkType: hard + +"schema-utils@npm:^3.1.1, schema-utils@npm:^3.1.2": + version: 3.1.2 + resolution: "schema-utils@npm:3.1.2" + dependencies: + "@types/json-schema": ^7.0.8 + ajv: ^6.12.5 + ajv-keywords: ^3.5.2 + checksum: 39683edfe3beff018cdb1ae4fa296fc55cea13a080aa2b4d9351895cd64b22ba4d87e2e548c2a2ac1bc76e60980670adb0f413a58104479f1a0c12e5663cb8ca languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.1": +"semver@npm:7.x, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.1": version: 7.5.1 resolution: "semver@npm:7.5.1" dependencies: @@ -6334,6 +9246,66 @@ __metadata: languageName: node linkType: hard +"semver@npm:^5.7.0, semver@npm:^5.7.1": + version: 5.7.1 + resolution: "semver@npm:5.7.1" + bin: + semver: ./bin/semver + checksum: 57fd0acfd0bac382ee87cd52cd0aaa5af086a7dc8d60379dfe65fea491fb2489b6016400813930ecd61fd0952dae75c115287a1b16c234b1550887117744dfaf + languageName: node + linkType: hard + +"semver@npm:^6.0.0, semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.3.0": + version: 6.3.0 + resolution: "semver@npm:6.3.0" + bin: + semver: ./bin/semver.js + checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 + languageName: node + linkType: hard + +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: ~1.2.1 + statuses: 2.0.1 + checksum: 74fc07ebb58566b87b078ec63e5a3e41ecd987e4272ba67b7467e86c6ad51bc6b0b0154133b6d8b08a2ddda360464f71382f7ef864700f34844a76c8027817a8 + languageName: node + linkType: hard + +"serialize-javascript@npm:^6.0.1": + version: 6.0.1 + resolution: "serialize-javascript@npm:6.0.1" + dependencies: + randombytes: ^2.1.0 + checksum: 3c4f4cb61d0893b988415bdb67243637333f3f574e9e9cc9a006a2ced0b390b0b3b44aef8d51c951272a9002ec50885eefdc0298891bc27eb2fe7510ea87dc4f + languageName: node + linkType: hard + +"serve-static@npm:1.15.0": + version: 1.15.0 + resolution: "serve-static@npm:1.15.0" + dependencies: + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + parseurl: ~1.3.3 + send: 0.18.0 + checksum: af57fc13be40d90a12562e98c0b7855cf6e8bd4c107fe9a45c212bf023058d54a1871b1c89511c3958f70626fff47faeb795f5d83f8cf88514dbaeb2b724464d + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -6341,6 +9313,13 @@ __metadata: languageName: node linkType: hard +"setprototypeof@npm:1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: be18cbbf70e7d8097c97f713a2e76edf84e87299b40d085c6bf8b65314e994cc15e2e317727342fa6996e38e1f52c59720b53fe621e2eb593a6847bf0356db89 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -6368,7 +9347,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -6407,6 +9386,28 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "slice-ansi@npm:3.0.0" + dependencies: + ansi-styles: ^4.0.0 + astral-regex: ^2.0.0 + is-fullwidth-code-point: ^3.0.0 + checksum: 5ec6d022d12e016347e9e3e98a7eb2a592213a43a65f1b61b74d2c78288da0aded781f665807a9f3876b9daa9ad94f64f77d7633a0458876c3a4fdc4eb223f24 + languageName: node + linkType: hard + +"slice-ansi@npm:^4.0.0": + version: 4.0.0 + resolution: "slice-ansi@npm:4.0.0" + dependencies: + ansi-styles: ^4.0.0 + astral-regex: ^2.0.0 + is-fullwidth-code-point: ^3.0.0 + checksum: 4a82d7f085b0e1b070e004941ada3c40d3818563ac44766cca4ceadd2080427d337554f9f99a13aaeb3b4a94d9964d9466c807b3d7b7541d1ec37ee32d308756 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -6414,6 +9415,17 @@ __metadata: languageName: node linkType: hard +"socks-proxy-agent@npm:5": + version: 5.0.1 + resolution: "socks-proxy-agent@npm:5.0.1" + dependencies: + agent-base: ^6.0.2 + debug: 4 + socks: ^2.3.3 + checksum: 1b60c4977b2fef783f0fc4dc619cd2758aafdb43f3cf679f1e3627cb6c6e752811cee5513ebb4157ad26786033d2f85029440f197d321e8293b38cc5aab01e06 + languageName: node + linkType: hard + "socks-proxy-agent@npm:^6.1.1": version: 6.1.1 resolution: "socks-proxy-agent@npm:6.1.1" @@ -6425,13 +9437,24 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.6.1": - version: 2.6.2 - resolution: "socks@npm:2.6.2" +"socks-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "socks-proxy-agent@npm:7.0.0" dependencies: - ip: ^1.1.5 + agent-base: ^6.0.2 + debug: ^4.3.3 + socks: ^2.6.2 + checksum: 720554370154cbc979e2e9ce6a6ec6ced205d02757d8f5d93fe95adae454fc187a5cbfc6b022afab850a5ce9b4c7d73e0f98e381879cf45f66317a4895953846 + languageName: node + linkType: hard + +"socks@npm:^2.3.3, socks@npm:^2.6.1, socks@npm:^2.6.2": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: ^2.0.0 smart-buffer: ^4.2.0 - checksum: dd9194293059d737759d5c69273850ad4149f448426249325c4bea0e340d1cf3d266c3b022694b0dcf5d31f759de23657244c481fc1e8322add80b7985c36b5e + checksum: 259d9e3e8e1c9809a7f5c32238c3d4d2a36b39b83851d0f573bfde5f21c4b1288417ce1af06af1452569cd1eb0841169afd4998f0e04ba04656f6b7f0e46d748 languageName: node linkType: hard @@ -6445,7 +9468,17 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1": +"source-map-support@npm:~0.5.20": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 43e98d700d79af1d36f859bdb7318e601dfc918c7ba2e98456118ebc4c4872b327773e5a1df09b0524e9e5063bb18f0934538eace60cca2710d1fa687645d137 + languageName: node + linkType: hard + +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2 @@ -6459,6 +9492,27 @@ __metadata: languageName: node linkType: hard +"sshpk@npm:^1.14.1": + version: 1.17.0 + resolution: "sshpk@npm:1.17.0" + dependencies: + asn1: ~0.2.3 + assert-plus: ^1.0.0 + bcrypt-pbkdf: ^1.0.0 + dashdash: ^1.12.0 + ecc-jsbn: ~0.1.1 + getpass: ^0.1.1 + jsbn: ~0.1.0 + safer-buffer: ^2.0.2 + tweetnacl: ~0.14.0 + bin: + sshpk-conv: bin/sshpk-conv + sshpk-sign: bin/sshpk-sign + sshpk-verify: bin/sshpk-verify + checksum: ba109f65c8e6c35133b8e6ed5576abeff8aa8d614824b7275ec3ca308f081fef483607c28d97780c1e235818b0f93ed8c8b56d0a5968d5a23fd6af57718c7597 + languageName: node + linkType: hard + "ssri@npm:^8.0.1": version: 8.0.1 resolution: "ssri@npm:8.0.1" @@ -6477,6 +9531,27 @@ __metadata: languageName: node linkType: hard +"statuses@npm:2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb + languageName: node + linkType: hard + +"statuses@npm:>= 1.5.0 < 2, statuses@npm:~1.5.0": + version: 1.5.0 + resolution: "statuses@npm:1.5.0" + checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c + languageName: node + linkType: hard + +"stream-shift@npm:^1.0.0": + version: 1.0.1 + resolution: "stream-shift@npm:1.0.1" + checksum: 59b82b44b29ec3699b5519a49b3cedcc6db58c72fb40c04e005525dfdcab1c75c4e0c180b923c380f204bed78211b9bad8faecc7b93dece4d004c3f6ec75737b + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -6487,7 +9562,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -6498,6 +9573,27 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^1.0.2 || 2, string-width@npm:^2.1.1": + version: 2.1.1 + resolution: "string-width@npm:2.1.1" + dependencies: + is-fullwidth-code-point: ^2.0.0 + strip-ansi: ^4.0.0 + checksum: d6173abe088c615c8dffaf3861dc5d5906ed3dc2d6fd67ff2bd2e2b5dce7fd683c5240699cf0b1b8aa679a3b3bd6b28b5053c824cb89b813d7f6541d8f89064a + languageName: node + linkType: hard + +"string-width@npm:^3.0.0, string-width@npm:^3.1.0": + version: 3.1.0 + resolution: "string-width@npm:3.1.0" + dependencies: + emoji-regex: ^7.0.1 + is-fullwidth-code-point: ^2.0.0 + strip-ansi: ^5.1.0 + checksum: 57f7ca73d201682816d573dc68bd4bb8e1dff8dc9fcf10470fdfc3474135c97175fec12ea6a159e67339b41e86963112355b64529489af6e7e70f94a7caf08b2 + languageName: node + linkType: hard + "string.prototype.trim@npm:^1.2.7": version: 1.2.7 resolution: "string.prototype.trim@npm:1.2.7" @@ -6540,6 +9636,40 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~0.10.x": + version: 0.10.31 + resolution: "string_decoder@npm:0.10.31" + checksum: fe00f8e303647e5db919948ccb5ce0da7dea209ab54702894dd0c664edd98e5d4df4b80d6fabf7b9e92b237359d21136c95bf068b2f7760b772ca974ba970202 + languageName: node + linkType: hard + +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: ~5.1.0 + checksum: 9ab7e56f9d60a28f2be697419917c50cac19f3e8e6c28ef26ed5f4852289fe0de5d6997d29becf59028556f2c62983790c1d9ba1e2a3cc401768ca12d5183a5b + languageName: node + linkType: hard + +"strip-ansi@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-ansi@npm:4.0.0" + dependencies: + ansi-regex: ^3.0.0 + checksum: d9186e6c0cf78f25274f6750ee5e4a5725fb91b70fdd79aa5fe648eab092a0ec5b9621b22d69d4534a56319f75d8944efbd84e3afa8d4ad1b9a9491f12c84eca + languageName: node + linkType: hard + +"strip-ansi@npm:^5.0.0, strip-ansi@npm:^5.1.0, strip-ansi@npm:^5.2.0": + version: 5.2.0 + resolution: "strip-ansi@npm:5.2.0" + dependencies: + ansi-regex: ^4.1.0 + checksum: bdb5f76ade97062bd88e7723aa019adbfacdcba42223b19ccb528ffb9fb0b89a5be442c663c4a3fb25268eaa3f6ea19c7c3fbae830bd1562d55adccae1fcec46 + languageName: node + linkType: hard + "strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -6570,6 +9700,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -6577,6 +9714,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:6.0.0": + version: 6.0.0 + resolution: "supports-color@npm:6.0.0" + dependencies: + has-flag: ^3.0.0 + checksum: 005b4a7e5d78a9a703454f5b7da34336b82825747724d1f3eefea6c3956afcb33b79b31854a93cef0fc1f2449919ae952f79abbfd09a5b5b43ecd26407d3a3a1 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -6595,7 +9741,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0": +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -6611,6 +9757,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^1.0.4": + version: 1.2.0 + resolution: "symbol-observable@npm:1.2.0" + checksum: 48ffbc22e3d75f9853b3ff2ae94a44d84f386415110aea5effc24d84c502e03a4a6b7a8f75ebaf7b585780bda34eb5d6da3121f826a6f93398429d30032971b6 + languageName: node + linkType: hard + "synckit@npm:^0.8.5": version: 0.8.5 resolution: "synckit@npm:0.8.5" @@ -6621,7 +9774,7 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^2.2.0": +"tapable@npm:^2.1.1, tapable@npm:^2.2.0": version: 2.2.1 resolution: "tapable@npm:2.2.1" checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 @@ -6652,6 +9805,49 @@ __metadata: languageName: node linkType: hard +"term-size@npm:2.1.0": + version: 2.1.0 + resolution: "term-size@npm:2.1.0" + checksum: 57558bfc384b6daacfe1952f1cfb32722cd46efcc2aedbffeb62d58ade598c195271756e2047a8190a810e3f564c05f37f3d5ba5fae149f407405dda198ff4c9 + languageName: node + linkType: hard + +"terser-webpack-plugin@npm:^5.3.7": + version: 5.3.9 + resolution: "terser-webpack-plugin@npm:5.3.9" + dependencies: + "@jridgewell/trace-mapping": ^0.3.17 + jest-worker: ^27.4.5 + schema-utils: ^3.1.1 + serialize-javascript: ^6.0.1 + terser: ^5.16.8 + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: 41705713d6f9cb83287936b21e27c658891c78c4392159f5148b5623f0e8c48559869779619b058382a4c9758e7820ea034695e57dc7c474b4962b79f553bc5f + languageName: node + linkType: hard + +"terser@npm:^5.16.8": + version: 5.17.6 + resolution: "terser@npm:5.17.6" + dependencies: + "@jridgewell/source-map": ^0.3.2 + acorn: ^8.5.0 + commander: ^2.20.0 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: 9c0ab0261a99a61c5f53d05d4ecc7f68c552bae6af481464fdd596bc9d7e89ce8e21b1833cb3ce06ad5f658e2b226081d543e4fe6e324b2cdf03ee8b7eeec01a + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -6670,6 +9866,20 @@ __metadata: languageName: node linkType: hard +"throttleit@npm:^1.0.0": + version: 1.0.0 + resolution: "throttleit@npm:1.0.0" + checksum: 1b2db4d2454202d589e8236c07a69d2fab838876d370030ebea237c34c0a7d1d9cf11c29f994531ebb00efd31e9728291042b7754f2798a8352ec4463455b659 + languageName: node + linkType: hard + +"through@npm:^2.3.8": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd + languageName: node + linkType: hard + "tiny-glob@npm:^0.2.9": version: 0.2.9 resolution: "tiny-glob@npm:0.2.9" @@ -6680,6 +9890,15 @@ __metadata: languageName: node linkType: hard +"tmp@npm:~0.2.1": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: ^3.0.0 + checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -6703,6 +9922,30 @@ __metadata: languageName: node linkType: hard +"toidentifier@npm:1.0.0": + version: 1.0.0 + resolution: "toidentifier@npm:1.0.0" + checksum: 199e6bfca1531d49b3506cff02353d53ec987c9ee10ee272ca6484ed97f1fc10fb77c6c009079ca16d5c5be4a10378178c3cacdb41ce9ec954c3297c74c6053e + languageName: node + linkType: hard + +"toidentifier@npm:1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 + languageName: node + linkType: hard + +"tough-cookie@npm:~2.5.0": + version: 2.5.0 + resolution: "tough-cookie@npm:2.5.0" + dependencies: + psl: ^1.1.28 + punycode: ^2.1.1 + checksum: 16a8cd090224dd176eee23837cbe7573ca0fa297d7e468ab5e1c02d49a4e9a97bb05fef11320605eac516f91d54c57838a25864e8680e27b069a5231d8264977 + languageName: node + linkType: hard + "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -6719,6 +9962,63 @@ __metadata: languageName: node linkType: hard +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: 49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7 + languageName: node + linkType: hard + +"ts-jest@npm:^29.1.0": + version: 29.1.0 + resolution: "ts-jest@npm:29.1.0" + dependencies: + bs-logger: 0.x + fast-json-stable-stringify: 2.x + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: 4.x + make-error: 1.x + semver: 7.x + yargs-parser: ^21.0.1 + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: 535dc42ad523cbe1e387701fb2e448518419b515c082f09b25411f0b3dd0b854cf3e8141c316d6f4b99883aeb4a4f94159cbb1edfb06d7f77ea6229fadb2e1bf + languageName: node + linkType: hard + +"ts-loader@npm:^9.4.3": + version: 9.4.3 + resolution: "ts-loader@npm:9.4.3" + dependencies: + chalk: ^4.1.0 + enhanced-resolve: ^5.0.0 + micromatch: ^4.0.0 + semver: ^7.3.4 + peerDependencies: + typescript: "*" + webpack: ^5.0.0 + checksum: 139ed53bc60717d0ca231cdffbdef7566b9feda11c72fecc697983113f1266ccca2e1cdf191f841a43afa6b87d6afe57a0caf4feecf02f30845aa7ac6f2411a4 + languageName: node + linkType: hard + "ts-node@npm:^10.7.0": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -6776,10 +10076,10 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.4.0, tslib@npm:^2.5.0": - version: 2.5.0 - resolution: "tslib@npm:2.5.0" - checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 +"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.5.0": + version: 2.5.2 + resolution: "tslib@npm:2.5.2" + checksum: 4d3c1e238b94127ed0e88aa0380db3c2ddae581dc0f4bae5a982345e9f50ee5eda90835b8bfba99b02df10a5734470be197158c36f9129ac49fdc14a6a9da222 languageName: node linkType: hard @@ -6794,6 +10094,22 @@ __metadata: languageName: node linkType: hard +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: ^5.0.1 + checksum: 05f6510358f8afc62a057b8b692f05d70c1782b70db86d6a1e0d5e28a32389e52fa6e7707b6c5ecccacc031462e4bc35af85ecfe4bbc341767917b7cf6965711 + languageName: node + linkType: hard + +"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": + version: 0.14.5 + resolution: "tweetnacl@npm:0.14.5" + checksum: 6061daba1724f59473d99a7bb82e13f211cdf6e31315510ae9656fefd4779851cb927adad90f3b488c8ed77c106adc0421ea8055f6f976ff21b27c5c4e918487 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -6803,6 +10119,15 @@ __metadata: languageName: node linkType: hard +"type-check@npm:~0.3.2": + version: 0.3.2 + resolution: "type-check@npm:0.3.2" + dependencies: + prelude-ls: ~1.1.2 + checksum: dd3b1495642731bc0e1fc40abe5e977e0263005551ac83342ecb6f4f89551d106b368ec32ad3fb2da19b3bd7b2d1f64330da2ea9176d8ddbfe389fb286eb5124 + languageName: node + linkType: hard + "type-detect@npm:4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" @@ -6824,6 +10149,16 @@ __metadata: languageName: node linkType: hard +"type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: 0.3.0 + mime-types: ~2.1.24 + checksum: 2c8e47675d55f8b4e404bcf529abdf5036c537a04c2b20177bcf78c9e3c1da69da3942b1346e6edb09e823228c0ee656ef0e033765ec39a70d496ef601a0c657 + languageName: node + linkType: hard + "typed-array-length@npm:^1.0.4": version: 1.0.4 resolution: "typed-array-length@npm:1.0.4" @@ -6835,6 +10170,13 @@ __metadata: languageName: node linkType: hard +"typed-error@npm:^3.0.2": + version: 3.2.2 + resolution: "typed-error@npm:3.2.2" + checksum: 90d0d2ebef72a3655153d7d4ffe8607ebb38a39e38f9f19642a55542c0459afc887862ff5353d57ee77502c5c438341843b21309ecd0cf2b19a344034c9fedef + languageName: node + linkType: hard + "typescript@npm:^4.9.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" @@ -6916,6 +10258,34 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^0.1.0": + version: 0.1.2 + resolution: "universalify@npm:0.1.2" + checksum: 40cdc60f6e61070fe658ca36016a8f4ec216b29bf04a55dce14e3710cc84c7448538ef4dad3728d0bfe29975ccd7bfb5f414c45e7b78883567fb31b246f02dff + languageName: node + linkType: hard + +"universalify@npm:^2.0.0": + version: 2.0.0 + resolution: "universalify@npm:2.0.0" + checksum: 2406a4edf4a8830aa6813278bab1f953a8e40f2f63a37873ffa9a3bc8f9745d06cc8e88f3572cb899b7e509013f7f6fcc3e37e8a6d914167a5381d8440518c44 + languageName: node + linkType: hard + +"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 + languageName: node + linkType: hard + +"untildify@npm:^4.0.0": + version: 4.0.0 + resolution: "untildify@npm:4.0.0" + checksum: 39ced9c418a74f73f0a56e1ba4634b4d959422dff61f4c72a8e39f60b99380c1b45ed776fbaa0a4101b157e4310d873ad7d114e8534ca02609b4916bb4187fb9 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.0.10": version: 1.0.10 resolution: "update-browserslist-db@npm:1.0.10" @@ -6939,13 +10309,29 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 languageName: node linkType: hard +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: c81095493225ecfc28add49c106ca4f09cdf56bc66731aa8dabc2edbbccb1e1bfe2de6a115e5c6a380d3ea166d1636410b62ef216bb07b3feb1cfde1d95d5080 + languageName: node + linkType: hard + +"uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df + languageName: node + linkType: hard + "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -6964,6 +10350,43 @@ __metadata: languageName: node linkType: hard +"value-or-promise@npm:1.0.11": + version: 1.0.11 + resolution: "value-or-promise@npm:1.0.11" + checksum: 13f8f2ef620118c73b4d1beee8ce6045d7182bbf15090ecfbcafb677ec43698506a5e9ace6bea5ea35c32bc612c9b1f824bb59b6581cdfb5c919052745c277d5 + languageName: node + linkType: hard + +"vary@npm:^1, vary@npm:~1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b + languageName: node + linkType: hard + +"verror@npm:1.10.0": + version: 1.10.0 + resolution: "verror@npm:1.10.0" + dependencies: + assert-plus: ^1.0.0 + core-util-is: 1.0.2 + extsprintf: ^1.2.0 + checksum: c431df0bedf2088b227a4e051e0ff4ca54df2c114096b0c01e1cbaadb021c30a04d7dd5b41ab277bcd51246ca135bf931d4c4c796ecae7a4fef6d744ecef36ea + languageName: node + linkType: hard + +"vm2@npm:^3.9.17": + version: 3.9.19 + resolution: "vm2@npm:3.9.19" + dependencies: + acorn: ^8.7.0 + acorn-walk: ^8.2.0 + bin: + vm2: bin/vm2 + checksum: fc6cf553134145cd7bb5246985bf242b056e3fb5ea71e2eef6710b2a5d6c6119cc6bc960435ff62480ee82efb43369be8f4db07b6690916ae7d3b2e714f395d8 + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -6973,6 +10396,16 @@ __metadata: languageName: node linkType: hard +"watchpack@npm:^2.4.0": + version: 2.4.0 + resolution: "watchpack@npm:2.4.0" + dependencies: + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.1.2 + checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -6987,6 +10420,50 @@ __metadata: languageName: node linkType: hard +"webpack-sources@npm:^3.2.3": + version: 3.2.3 + resolution: "webpack-sources@npm:3.2.3" + checksum: 989e401b9fe3536529e2a99dac8c1bdc50e3a0a2c8669cbafad31271eadd994bc9405f88a3039cd2e29db5e6d9d0926ceb7a1a4e7409ece021fe79c37d9c4607 + languageName: node + linkType: hard + +"webpack@npm:^5.84.1": + version: 5.84.1 + resolution: "webpack@npm:5.84.1" + dependencies: + "@types/eslint-scope": ^3.7.3 + "@types/estree": ^1.0.0 + "@webassemblyjs/ast": ^1.11.5 + "@webassemblyjs/wasm-edit": ^1.11.5 + "@webassemblyjs/wasm-parser": ^1.11.5 + acorn: ^8.7.1 + acorn-import-assertions: ^1.9.0 + browserslist: ^4.14.5 + chrome-trace-event: ^1.0.2 + enhanced-resolve: ^5.14.1 + es-module-lexer: ^1.2.1 + eslint-scope: 5.1.1 + events: ^3.2.0 + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.2.9 + json-parse-even-better-errors: ^2.3.1 + loader-runner: ^4.2.0 + mime-types: ^2.1.27 + neo-async: ^2.6.2 + schema-utils: ^3.1.2 + tapable: ^2.1.1 + terser-webpack-plugin: ^5.3.7 + watchpack: ^2.4.0 + webpack-sources: ^3.2.3 + peerDependenciesMeta: + webpack-cli: + optional: true + bin: + webpack: bin/webpack.js + checksum: 646b645df5badf2dac2ddd0193c9e9a177d51283d18f918eead36a0cdf7b750c4111d9ac11d9825c1334cbd0a6fb8f82628fbfb90d0066f927265dd11b47b192 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -7033,6 +10510,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": version: 1.1.9 resolution: "which-typed-array@npm:1.1.9" @@ -7047,6 +10531,17 @@ __metadata: languageName: node linkType: hard +"which@npm:1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: ^2.0.0 + bin: + which: ./bin/which + checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 + languageName: node + linkType: hard + "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -7058,6 +10553,15 @@ __metadata: languageName: node linkType: hard +"wide-align@npm:1.1.3": + version: 1.1.3 + resolution: "wide-align@npm:1.1.3" + dependencies: + string-width: ^1.0.2 || 2 + checksum: d09c8012652a9e6cab3e82338d1874a4d7db2ad1bd19ab43eb744acf0b9b5632ec406bdbbbb970a8f4771a7d5ef49824d038ba70aa884e7723f5b090ab87134d + languageName: node + linkType: hard + "wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" @@ -7067,13 +10571,44 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:^1.2.3": +"widest-line@npm:3.1.0": + version: 3.1.0 + resolution: "widest-line@npm:3.1.0" + dependencies: + string-width: ^4.0.0 + checksum: 03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3": version: 1.2.3 resolution: "word-wrap@npm:1.2.3" checksum: 30b48f91fcf12106ed3186ae4fa86a6a1842416df425be7b60485de14bec665a54a68e4b5156647dec3a70f25e84d270ca8bc8cd23182ed095f5c7206a938c1f languageName: node linkType: hard +"wrap-ansi@npm:^5.1.0": + version: 5.1.0 + resolution: "wrap-ansi@npm:5.1.0" + dependencies: + ansi-styles: ^3.2.0 + string-width: ^3.0.0 + strip-ansi: ^5.0.0 + checksum: 9b48c862220e541eb0daa22661b38b947973fc57054e91be5b0f2dcc77741a6875ccab4ebe970a394b4682c8dfc17e888266a105fb8b0a9b23c19245e781ceae + languageName: node + linkType: hard + +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: 6cd96a410161ff617b63581a08376f0cb9162375adeb7956e10c8cd397821f7eb2a6de24eb22a0b28401300bf228c86e50617cd568209b5f6775b93c97d2fe3a + languageName: node + linkType: hard + "wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -7102,6 +10637,49 @@ __metadata: languageName: node linkType: hard +"ws@npm:*, ws@npm:^8.8.0": + version: 8.13.0 + resolution: "ws@npm:8.13.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + languageName: node + linkType: hard + +"xml@npm:^1.0.1": + version: 1.0.1 + resolution: "xml@npm:1.0.1" + checksum: 11b5545ef3f8fec3fa29ce251f50ad7b6c97c103ed4d851306ec23366f5fa4699dd6a942262df52313a0cd1840ab26256da253c023bad3309d8ce46fe6020ca0 + languageName: node + linkType: hard + +"xregexp@npm:2.0.0": + version: 2.0.0 + resolution: "xregexp@npm:2.0.0" + checksum: de62d1f01c9f1a67c80cafe48a3dc081b324249a0e88e65dc9acae9cce6d8e63c9d91c0f97e2ad2d8c5351c856c139c04dc55ebd941e59b7d1d5c1169e164cff + languageName: node + linkType: hard + +"xtend@npm:^4.0.0": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a + languageName: node + linkType: hard + +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4 + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -7130,14 +10708,81 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^21.1.1": +"yargs-parser@npm:13.1.1": + version: 13.1.1 + resolution: "yargs-parser@npm:13.1.1" + dependencies: + camelcase: ^5.0.0 + decamelize: ^1.2.0 + checksum: fa5fd27736aa423dc9a114d160dae94625f7faf19c252b8c91ac0197be9715d1dbc9b98fda893f75f182111fb6c3c0ce60c631b73859dd1a06bec07cddfb98f4 + languageName: node + linkType: hard + +"yargs-parser@npm:^13.1.1, yargs-parser@npm:^13.1.2": + version: 13.1.2 + resolution: "yargs-parser@npm:13.1.2" + dependencies: + camelcase: ^5.0.0 + decamelize: ^1.2.0 + checksum: c8bb6f44d39a4acd94462e96d4e85469df865de6f4326e0ab1ac23ae4a835e5dd2ddfe588317ebf80c3a7e37e741bd5cb0dc8d92bcc5812baefb7df7c885e86b + languageName: node + linkType: hard + +"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c languageName: node linkType: hard -"yargs@npm:^17.3.1": +"yargs-unparser@npm:1.6.0": + version: 1.6.0 + resolution: "yargs-unparser@npm:1.6.0" + dependencies: + flat: ^4.1.0 + lodash: ^4.17.15 + yargs: ^13.3.0 + checksum: ca662bb94af53d816d47f2162f0a1d135783f09de9fd47645a5cb18dd25532b0b710432b680d2c065ff45de122ba4a96433c41595fa7bfcc08eb12e889db95c1 + languageName: node + linkType: hard + +"yargs@npm:13.3.0": + version: 13.3.0 + resolution: "yargs@npm:13.3.0" + dependencies: + cliui: ^5.0.0 + find-up: ^3.0.0 + get-caller-file: ^2.0.1 + require-directory: ^2.1.1 + require-main-filename: ^2.0.0 + set-blocking: ^2.0.0 + string-width: ^3.0.0 + which-module: ^2.0.0 + y18n: ^4.0.0 + yargs-parser: ^13.1.1 + checksum: 50aac9a7248ecbd9b5a6dd93010696e4847a3c9e23ae162d6e0caf10b236a0a90b461abaeab7678ded83dbd118538a331b4ac6fc7f5d22ec650b2e77e6403d5c + languageName: node + linkType: hard + +"yargs@npm:^13.3.0": + version: 13.3.2 + resolution: "yargs@npm:13.3.2" + dependencies: + cliui: ^5.0.0 + find-up: ^3.0.0 + get-caller-file: ^2.0.1 + require-directory: ^2.1.1 + require-main-filename: ^2.0.0 + set-blocking: ^2.0.0 + string-width: ^3.0.0 + which-module: ^2.0.0 + y18n: ^4.0.0 + yargs-parser: ^13.1.2 + checksum: 75c13e837eb2bb25717957ba58d277e864efc0cca7f945c98bdf6477e6ec2f9be6afa9ed8a876b251a21423500c148d7b91e88dee7adea6029bdec97af1ef3e8 + languageName: node + linkType: hard + +"yargs@npm:^17.3.1, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -7152,6 +10797,16 @@ __metadata: languageName: node linkType: hard +"yauzl@npm:^2.10.0": + version: 2.10.0 + resolution: "yauzl@npm:2.10.0" + dependencies: + buffer-crc32: ~0.2.3 + fd-slicer: ~1.1.0 + checksum: 7f21fe0bbad6e2cb130044a5d1d0d5a0e5bf3d8d4f8c4e6ee12163ce798fee3de7388d22a7a0907f563ac5f9d40f8699a223d3d5c1718da90b0156da6904022b + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" @@ -7165,3 +10820,10 @@ __metadata: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"zstd-codec@npm:^0.1.4": + version: 0.1.4 + resolution: "zstd-codec@npm:0.1.4" + checksum: 8689bc0defc4f387d1be990b8b8ca8ca56690d17dfc8dd4703db798465b92a21e64e54e886acfaa376147d9d07d879a68627b09fddc34a0c93f0dc5c610a790c + languageName: node + linkType: hard