diff --git a/.eslintrc-ts.js b/.eslintrc-ts.js
index a8798c8..1f41f9c 100644
--- a/.eslintrc-ts.js
+++ b/.eslintrc-ts.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
module.exports = {
extends: [
diff --git a/.eslintrc.js b/.eslintrc.js
index 689ec41..fde84cc 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
module.exports = {
env: {
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index ddca3c6..840be35 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -8,19 +8,19 @@ on:
# Run the pipeline daily so that we get continuous dogfooding.
schedule:
- # Run at 6pm UTC/10am Pacific
- - cron: 0 18 * * *
+ # Run at 8am UTC/midnight Pacific
+ - cron: 0 8 * * *
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch: {}
jobs:
check:
- name: Typecheck, lint, and audit
+ name: Build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
@@ -28,6 +28,8 @@ jobs:
cache: yarn
- id: install
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
run: yarn install --immutable
- run: yarn build
@@ -50,6 +52,16 @@ jobs:
name: js-api
path: packages/js-api/package.tgz
+ - name: Run @unflakable/cypress-plugin unit tests
+ env:
+ FORCE_COLOR: "1"
+ run: yarn workspace @unflakable/cypress-plugin test
+
+ - name: Run @unflakable/jest-plugin unit tests
+ env:
+ FORCE_COLOR: "1"
+ run: yarn workspace @unflakable/jest-plugin test
+
- if: ${{ always() && steps.install.outcome == 'success' }}
run: yarn lint
@@ -88,17 +100,17 @@ jobs:
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
+ cypress_linux_integration_tests:
+ name: "Cypress ${{ matrix.cypress }} Linux Node ${{ matrix.node }} Integration Tests"
runs-on: ubuntu-latest
- timeout-minutes: 45
+ timeout-minutes: 80
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
+ max-parallel: 16
matrix:
node:
- 16
@@ -113,8 +125,21 @@ jobs:
- "12.0"
- "12.10"
- "12.14"
+ - "12.15"
+ - "12.16"
+ - "12.17"
+ - "13.0"
+ - "13.1"
+ - "13.2"
+ # 13.3.2 introduced a regression that made that version (and 13.3.3) too flaky to test.
+ # See:
+ # - https://github.com/cypress-io/cypress/issues/28141
+ # - https://github.com/cypress-io/cypress/issues/28148
+ #- "13.3"
+ - "13.4"
+ - "13.5"
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
@@ -125,17 +150,44 @@ jobs:
uses: actions/cache@v3
with:
path: ~/.cache/Cypress
- key: cypress-${{ runner.os }}-node${{ matrix.node }}-cypress${{ matrix.cypress }}-modules-${{ hashFiles('yarn.lock') }}
+ key: cypress-${{ runner.os }}-node${{ matrix.node }}-cypress${{ matrix.cypress }}
- id: install
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
run: yarn install --immutable
+ - uses: actions/download-artifact@v3
+ with:
+ path: .artifacts
+
+ - name: Install pre-built plugin packages
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
+ run: |
+ curl -Lo jq https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64
+ chmod +x jq
+ cat package.json \
+ | ./jq '. + {"resolutions": (.resolutions + {
+ "@unflakable/cypress-plugin": "file:./.artifacts/cypress-plugin/package.tgz",
+ "@unflakable/jest-plugin": "file:./.artifacts/jest-plugin/package.tgz",
+ "@unflakable/js-api": "file:./.artifacts/js-api/package.tgz"
+ })}' > package-new.json
+ mv package-new.json package.json
+ yarn install --no-immutable
+
+ - name: Build test dependencies
+ run: yarn build:plugins-common && yarn build:test-common && yarn build:cypress-tests
+
- name: Set Cypress version
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
run: |
- yarn set resolution "cypress@npm:10 - 12" ${{ matrix.cypress }}
+ yarn set resolution "cypress@npm:11.2 - 13" ${{ matrix.cypress }}
grep --after-context=1 "^\".*cypress.*" yarn.lock
- - run: yarn build:plugins && yarn build:cypress-tests
+ - name: Install Cypress binary
+ run: yarn workspace cypress-integration exec cypress install
- name: Test
env:
@@ -144,7 +196,7 @@ jobs:
# DEBUG: unflakable:*
# Enable debug logs within the Cypress plugin.
- # TEST_DEBUG: unflakable:*
+ TEST_DEBUG: "unflakable:*,cypress:server:*,cypress-verbose:server:browsers:cri-client:*"
# Enable terminal colors for debug() output.
DEBUG_COLORS: "1"
@@ -159,26 +211,142 @@ jobs:
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
+ --reporters @unflakable/jest-plugin/dist/reporter \
+ --runner @unflakable/jest-plugin/dist/runner \
+ --testRunner @unflakable/jest-plugin/dist/test-runner
+
+ cypress_windows_integration_tests:
+ name: "Cypress ${{ matrix.cypress }} Windows Node ${{ matrix.node }} Integration Tests"
+ runs-on: windows-2019
+ # Cypress on Windows is slowwww...
+ timeout-minutes: 180
+ 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
+ max-parallel: 16
+ matrix:
+ node:
+ - 16
+ - 18
+ - 20
+ cypress:
+ # FIXME: support earlier versions
+ #- "10.0"
+ #- "10.11"
+ #- "11.0"
+ - "11.2"
+ - "12.0"
+ - "12.10"
+ - "12.14"
+ - "12.15"
+ - "12.16"
+ - "12.17"
+ - "13.0"
+ - "13.1"
+ - "13.2"
+ #- "13.3"
+ - "13.4"
+ - "13.5"
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node }}
+ cache: yarn
- jest_integration_tests:
- name: "Jest Integration Tests: Jest ${{ matrix.jest }} + Node ${{ matrix.node }}"
- # FIXME: also test on Windows
+ - name: Get AppData directory
+ run: echo ("LOCALAPPDATA=" + $env:LOCALAPPDATA) >> $env:GITHUB_ENV
+
+ - name: Cache Cypress binary
+ uses: actions/cache@v3
+ with:
+ path: ${{ env.LOCALAPPDATA }}\Cypress\Cache
+ key: cypress-${{ runner.os }}-node${{ matrix.node }}-cypress${{ matrix.cypress }}
+
+ - id: install
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
+ run: |
+ yarn install --immutable
+
+ - uses: actions/download-artifact@v3
+ with:
+ path: .artifacts
+
+ - name: Install pre-built plugin packages
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
+ shell: bash
+ run: |
+ curl -Lo jq.exe https://github.com/jqlang/jq/releases/download/jq-1.6/jq-win64.exe
+ cat package.json \
+ | ./jq.exe '. + {"resolutions": (.resolutions + {
+ "@unflakable/cypress-plugin": "file:./.artifacts/cypress-plugin/package.tgz",
+ "@unflakable/jest-plugin": "file:./.artifacts/jest-plugin/package.tgz",
+ "@unflakable/js-api": "file:./.artifacts/js-api/package.tgz"
+ })}' > package-new.json
+ mv package-new.json package.json
+ yarn install --no-immutable
+
+ - name: Build test dependencies
+ run: yarn build:plugins-common && yarn build:test-common && yarn build:cypress-tests
+
+ - name: Set Cypress version
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
+ run: |
+ yarn set resolution "cypress@npm:11.2 - 13" ${{ matrix.cypress }}
+ Select-String -Pattern '^".*cypress.*' -Path yarn.lock -Context 0,1
+
+ - name: Install Cypress binary
+ run: yarn workspace cypress-integration exec cypress install
+
+ - 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:*,cypress:server:*,cypress-verbose:server:browsers:cri-client:*"
+
+ # Enable terminal colors for debug() output.
+ DEBUG_COLORS: "1"
+
+ # Make chalk emit TTY colors.
+ FORCE_COLOR: "1"
+
+ UNFLAKABLE_API_KEY: ${{ secrets.UNFLAKABLE_API_KEY }}
+ UNFLAKABLE_SUITE_ID: ${{ github.repository == 'unflakable/unflakable-javascript' && '2QwtGckRudLNUGBsdkVEoSknck1' || '2Qwt9RyPIbOI95C6qjXCzcTelni' }}
+ run: |
+ yarn workspace cypress-integration test `
+ --reporters @unflakable/jest-plugin/dist/reporter `
+ --runner @unflakable/jest-plugin/dist/runner `
+ --testRunner @unflakable/jest-plugin/dist/test-runner
+
+ jest_linux_integration_tests:
+ name: "Jest ${{ matrix.jest }} Linux Node ${{ matrix.node }} Integration Tests"
runs-on: ubuntu-latest
- timeout-minutes: 15
+ timeout-minutes: 20
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_jest == 'true'
strategy:
fail-fast: false
+ max-parallel: 12
matrix:
node:
- 16
- 18
- 20
jest:
+ - "29.7"
+ - "29.6"
- "29.5"
- "29.4"
- "29.3"
@@ -205,9 +373,11 @@ jobs:
- "25.3"
- "25.2"
- "25.1"
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
@@ -217,6 +387,22 @@ jobs:
- id: install
run: yarn install --immutable
+ - uses: actions/download-artifact@v3
+ with:
+ path: .artifacts
+
+ - name: Install pre-built plugin packages
+ run: |
+ curl -Lo jq https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64
+ chmod +x jq
+ cat package.json \
+ | ./jq '. + {"resolutions": (.resolutions + {
+ "@unflakable/jest-plugin": "file:./.artifacts/jest-plugin/package.tgz",
+ "@unflakable/js-api": "file:./.artifacts/js-api/package.tgz"
+ })}' > package-new.json
+ mv package-new.json package.json
+ yarn install --no-immutable
+
# Since Jest is composed of many packages, we need to make sure that they're all using the
# same major and minor versions. Otherwise, NPM will keep the newer minor versions since
# they're semver-compatible. This script iteratively installs the lowest compatible semver
@@ -231,11 +417,24 @@ jobs:
if: ${{ startsWith(matrix.jest, '25.') }}
run: yarn set resolution "chalk@npm:^3.0.0 || ^4.0.0" 3.0
- - run: yarn build:plugins
+ - name: Build test dependencies
+ run: yarn build:plugins-common && yarn build:test-common
- name: Test
env:
- DEBUG: unflakable:*
+ # Enable debug logs within the jest-plugin/test/integration Jest tests that invoke Jest
+ # on the jest-plugin/test/integration-input test cases. WARNING: these are very verbose
+ # but are useful for seeing the raw chalk terminal codes.
+ # DEBUG: unflakable:*
+
+ # Enable debug logs within the Jest 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=29KWCuK12VnU7pkpvWgrGS0woAX
@@ -246,3 +445,113 @@ jobs:
yarn workspace jest-integration test \
--reporters @unflakable/jest-plugin/dist/reporter \
--runner @unflakable/jest-plugin/dist/runner
+
+ jest_windows_integration_tests:
+ name: "Jest ${{ matrix.jest }} Windows Node ${{ matrix.node }} Integration Tests"
+ runs-on: windows-2019
+ timeout-minutes: 30
+ 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_jest == 'true'
+ strategy:
+ fail-fast: false
+ max-parallel: 12
+ matrix:
+ node:
+ - 16
+ - 18
+ - 20
+ jest:
+ - "29.7"
+ - "29.6"
+ - "29.5"
+ - "29.4"
+ - "29.3"
+ - "29.2"
+ - "29.1"
+ - "29.0"
+ - "28.1"
+ - "28.0"
+ - "27.5"
+ - "27.4"
+ - "27.3"
+ - "27.2"
+ - "27.1"
+ - "27.0"
+ - "26.6"
+ - "26.5"
+ - "26.4"
+ - "26.3"
+ - "26.2"
+ - "26.1"
+ - "26.0"
+ - "25.5"
+ - "25.4"
+ - "25.3"
+ - "25.2"
+ - "25.1"
+ env:
+ CYPRESS_INSTALL_BINARY: "0"
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node }}
+ cache: yarn
+
+ - id: install
+ run: yarn install --immutable
+
+ - uses: actions/download-artifact@v3
+ with:
+ path: .artifacts
+
+ - name: Install pre-built plugin packages
+ shell: bash
+ run: |
+ curl -Lo jq.exe https://github.com/jqlang/jq/releases/download/jq-1.6/jq-win64.exe
+ cat package.json \
+ | ./jq.exe '. + {"resolutions": (.resolutions + {
+ "@unflakable/jest-plugin": "file:./.artifacts/jest-plugin/package.tgz",
+ "@unflakable/js-api": "file:./.artifacts/js-api/package.tgz"
+ })}' > package-new.json
+ mv package-new.json package.json
+ yarn install --no-immutable
+
+ - name: Set Jest version
+ run: |
+ yarn set-jest-version ${{ matrix.jest }}
+ Select-String -Pattern '^".*jest.*' -Path yarn.lock -Context 0,1
+
+ - name: Set resolution for chalk to 3.0.0
+ if: ${{ startsWith(matrix.jest, '25.') }}
+ run: yarn set resolution "chalk@npm:^3.0.0 || ^4.0.0" 3.0
+
+ - name: Build test dependencies
+ run: yarn build:plugins-common && yarn build:test-common
+
+ - name: Test
+ env:
+ # Enable debug logs within the jest-plugin/test/integration Jest tests that invoke Jest
+ # on the jest-plugin/test/integration-input test cases. WARNING: these are very verbose
+ # but are useful for seeing the raw chalk terminal codes.
+ # DEBUG: unflakable:*
+
+ # Enable debug logs within the Jest plugin.
+ TEST_DEBUG: "unflakable:*"
+
+ # Enable terminal colors for debug() output.
+ DEBUG_COLORS: "1"
+
+ # Make chalk emit TTY colors.
+ FORCE_COLOR: "1"
+
+ UNFLAKABLE_API_KEY: ${{ secrets.UNFLAKABLE_API_KEY }}
+ UNFLAKABLE_SUITE_ID: ${{ github.repository == 'unflakable/unflakable-javascript' && '29KWCuK12VnU7pkpvWgrGS0woAX' || '28UidZ8cSKjRe4g1xkd9EE8noDF' }}
+ run: |
+ yarn workspace jest-integration test `
+ --reporters @unflakable/jest-plugin/dist/reporter `
+ --runner @unflakable/jest-plugin/dist/runner
diff --git a/.github/workflows/cypress-realworld-app.yaml b/.github/workflows/cypress-realworld-app.yaml
index 0bb2ac2..63c169c 100644
--- a/.github/workflows/cypress-realworld-app.yaml
+++ b/.github/workflows/cypress-realworld-app.yaml
@@ -16,11 +16,11 @@ jobs:
timeout-minutes: 15
runs-on: ubuntu-latest
container:
- image: cypress/browsers:node16.16.0-chrome105-ff104-edge
+ image: cypress/browsers:node-18.16.0-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1
options: --user 1001
steps:
- name: Check out latest cypress-io/cypress-realworld-app
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: cypress-io/cypress-realworld-app
@@ -62,7 +62,7 @@ jobs:
# 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
+ wget -O jq https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64
chmod +x jq
yarn cache clean @unflakable/js-api
diff --git a/LICENSE b/LICENSE
index 53bf01b..fc24ed8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022-2023 Developer Innovations, LLC
+Copyright (c) 2022-2024 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
diff --git a/README.md b/README.md
index 0b3915a..6ac8e99 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,6 @@
-[](https://twitter.com/unflakable)
-
# Official Unflakable Plugins for JavaScript
This repository contains the official [Unflakable](https://unflakable.com) plugins for JavaScript
@@ -28,7 +26,7 @@ complete usage instructions.
This plugin maintains compatibility with the Cypress and Node.js versions listed below:
-[](https://cypress.io)
+[](https://cypress.io)
[](https://nodejs.org)
## Jest Plugin
diff --git a/package.json b/package.json
index c01d2c5..7cba66c 100644
--- a/package.json
+++ b/package.json
@@ -2,9 +2,6 @@
"packageManager": "yarn@3.5.1",
"private": true,
"devDependencies": {
- "@babel/core": "^7.21.5",
- "@babel/preset-env": "^7.21.5",
- "@babel/preset-typescript": "^7.21.5",
"@types/debug": "^4.1.7",
"@types/js-yaml": "^4.0.5",
"@types/node": "^14.18.43",
@@ -32,8 +29,9 @@
"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",
- "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",
+ "build:test-common": "yarn workspace unflakable-test-common build",
+ "build:tests": "yarn build:test-common && yarn build:cypress-tests && yarn typecheck:jest-tests",
+ "build:cypress-tests": "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 .",
@@ -44,7 +42,6 @@
"workspaces": [
"packages/*",
"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/README.md b/packages/cypress-plugin/README.md
index 961ceac..3818fe5 100644
--- a/packages/cypress-plugin/README.md
+++ b/packages/cypress-plugin/README.md
@@ -5,7 +5,6 @@
[](https://www.npmjs.com/package/@unflakable/cypress-plugin)
-[](https://twitter.com/unflakable)
# Unflakable Plugin for Cypress
@@ -19,7 +18,7 @@ complete usage instructions.
This plugin maintains compatibility with the Cypress and Node.js versions listed below:
-[](https://cypress.io)
+[](https://cypress.io)
[](https://nodejs.org)
## Contributing
diff --git a/packages/cypress-plugin/cypress-on-fix.d.ts b/packages/cypress-plugin/cypress-on-fix.d.ts
new file mode 100644
index 0000000..7d24e1b
--- /dev/null
+++ b/packages/cypress-plugin/cypress-on-fix.d.ts
@@ -0,0 +1,8 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+declare module "cypress-on-fix" {
+ import * as Cypress from "cypress";
+ export default function onProxy(
+ on: Cypress.PluginEvents
+ ): Cypress.PluginEvents;
+}
diff --git a/packages/cypress-plugin/jest.config.js b/packages/cypress-plugin/jest.config.js
index d2d9b3c..5ed0700 100644
--- a/packages/cypress-plugin/jest.config.js
+++ b/packages/cypress-plugin/jest.config.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
diff --git a/packages/cypress-plugin/mocha.d.ts b/packages/cypress-plugin/mocha.d.ts
index d81ed0d..937533b 100644
--- a/packages/cypress-plugin/mocha.d.ts
+++ b/packages/cypress-plugin/mocha.d.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
type GlobalError = Error;
@@ -20,8 +20,6 @@ declare namespace Mocha {
// Additional fields that Cypress adds to Mocha.Test.
export type CypressTestProps = {
- id: string;
- order: number;
wallClockStartedAt: string;
};
diff --git a/packages/cypress-plugin/package.json b/packages/cypress-plugin/package.json
index 05ff780..0cd0dc7 100644
--- a/packages/cypress-plugin/package.json
+++ b/packages/cypress-plugin/package.json
@@ -8,7 +8,7 @@
"bugs": "https://github.com/unflakable/unflakable-javascript/issues",
"homepage": "https://unflakable.com",
"license": "MIT",
- "version": "0.1.0",
+ "version": "0.2.1",
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -18,6 +18,10 @@
"types": "./dist/config-wrapper.d.ts",
"default": "./dist/config-wrapper.mjs"
},
+ "./config-wrapper-sync": {
+ "types": "./dist/config-wrapper-sync.d.ts",
+ "default": "./dist/config-wrapper-sync.js"
+ },
"./reporter": {
"types": "./dist/reporter.d.ts",
"default": "./dist/reporter.js"
@@ -31,8 +35,9 @@
"README.md",
"dist/**/*.js",
"dist/**/*.mjs",
- "dist/index.d.ts",
+ "dist/config-wrapper-sync.d.ts",
"dist/config-wrapper.d.ts",
+ "dist/index.d.ts",
"dist/reporter.d.ts",
"dist/skip-tests.d.ts"
],
@@ -40,16 +45,19 @@
"cypress-unflakable": "./dist/main.js"
},
"dependencies": {
+ "@stdlib/utils-convert-path": "^0.0.8",
"@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",
+ "cypress-on-fix": "^1.0.2",
"dayjs": "^1.10.4",
"debug": "^4.3.3",
"deep-equal": "^2.0.5",
"es6-promisify": "^7.0.0",
+ "escape-string-regexp": "^4.0.0",
"lodash": "^4.17.21",
"log-symbols": "^4.1.0",
"mocha": "=7.0.1",
@@ -74,21 +82,22 @@
"@types/yargs": "^17.0.24",
"@unflakable/plugins-common": "workspace:",
"cross-env": "^7.0.3",
- "cypress": "10 - 12",
+ "cypress": "11.2 - 13",
"jest": "^29.5.0",
"jest-environment-node": "^29.5.0",
+ "rimraf": "^5.0.1",
"rollup": "^3.21.1",
+ "rollup-plugin-dts": "^5.3.0",
+ "ts-jest": "^29.1.0",
"typescript": "^4.9.5",
"widest-line": "3.1.0"
},
"peerDependencies": {
- "cypress": "10 - 12"
+ "cypress": "11.2 - 13"
},
"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/",
+ "build": "yarn clean && tsc --noEmit && tsc --noEmit -p src && rollup --config",
+ "clean": "rimraf 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
index 32bb23a..3b6678e 100644
--- a/packages/cypress-plugin/rollup.config.mjs
+++ b/packages/cypress-plugin/rollup.config.mjs
@@ -1,64 +1,116 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import pluginCommonJs from "@rollup/plugin-commonjs";
+import pluginDts from "rollup-plugin-dts";
import pluginJson from "@rollup/plugin-json";
import pluginNodeResolve from "@rollup/plugin-node-resolve";
import pluginTypescript from "@rollup/plugin-typescript";
+import path from "path";
+
+/**
+ * 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 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.
+ *
+ * @type {import("rollup").IsExternal}
+ */
+const isExternal = (id) =>
+ !id.startsWith(".") &&
+ !path.isAbsolute(id) &&
+ !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);
+
+const plugins = [
+ pluginCommonJs(),
+ pluginJson(),
+ pluginNodeResolve({ preferBuiltins: true }),
+ pluginTypescript({ tsconfig: "src/tsconfig.json" }),
+];
+
+const 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,
+};
/**
* @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" : ""),
+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/config-wrapper-sync.ts",
+ "src/index.ts",
+ "src/main.ts",
+ "src/reporter.ts",
+ "src/skip-tests.ts",
+ ],
+ output: {
+ format: "cjs",
+ dir: "dist",
+ /**
+ * @param {import("rollup").PreRenderedChunk} chunk
+ * @returns {string}
+ */
+ banner: (chunk) => (chunk.name === "main" ? "#!/usr/bin/env node" : ""),
+ },
+ external: isExternal,
+ plugins,
+ treeshake,
},
- // 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,
+ {
+ input: "src/config-wrapper.ts",
+ output: {
+ chunkFileNames: "[name]-[hash].mjs",
+ file: "dist/config-wrapper.mjs",
+ format: "es",
+ },
+ external: isExternal,
+ plugins,
+ treeshake,
},
-};
+ // Rollup types so that UnflakableConfig from @unflakable/plugins-common is bundled. This package
+ // doesn't get published separately, so our public types shouldn't reference it.
+ {
+ input: [
+ // NB: This should include every exported .d.ts from package.json.
+ "dist/config-wrapper-sync.d.ts",
+ "dist/config-wrapper.d.ts",
+ "dist/index.d.ts",
+ "dist/reporter.d.ts",
+ "dist/skip-tests.d.ts",
+ ],
+ output: {
+ dir: ".",
+ entryFileNames: "[name].d.ts",
+ format: "cjs",
+ },
+ external: isExternal,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ plugins: [
+ pluginNodeResolve({ preferBuiltins: true }),
+ pluginDts({
+ respectExternal: true,
+ }),
+ ],
+ },
+];
diff --git a/packages/cypress-plugin/src/.eslintrc.js b/packages/cypress-plugin/src/.eslintrc.js
index 62e8179..4070ee2 100644
--- a/packages/cypress-plugin/src/.eslintrc.js
+++ b/packages/cypress-plugin/src/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
extends: ["../../../.eslintrc-ts.js"],
diff --git a/packages/cypress-plugin/src/config-env-vars.ts b/packages/cypress-plugin/src/config-env-vars.ts
index d9623b1..721480f 100644
--- a/packages/cypress-plugin/src/config-env-vars.ts
+++ b/packages/cypress-plugin/src/config-env-vars.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { EnvVar } from "@unflakable/plugins-common";
diff --git a/packages/cypress-plugin/src/config-wrapper-sync.ts b/packages/cypress-plugin/src/config-wrapper-sync.ts
new file mode 100644
index 0000000..96b672f
--- /dev/null
+++ b/packages/cypress-plugin/src/config-wrapper-sync.ts
@@ -0,0 +1,12 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import { wrapCypressConfig } from "./index";
+import _debug from "debug";
+import { loadUserConfigSync } from "./load-user-config";
+
+const debug = _debug("unflakable:config-wrapper-sync");
+
+const userConfig = loadUserConfigSync();
+debug("Loaded user config %o", userConfig);
+
+export default wrapCypressConfig(userConfig);
diff --git a/packages/cypress-plugin/src/config-wrapper.ts b/packages/cypress-plugin/src/config-wrapper.ts
index ce904e0..8b01fca 100644
--- a/packages/cypress-plugin/src/config-wrapper.ts
+++ b/packages/cypress-plugin/src/config-wrapper.ts
@@ -1,58 +1,11 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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";
+import { loadUserConfig } from "./load-user-config";
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);
diff --git a/packages/cypress-plugin/src/cypress-env-vars.ts b/packages/cypress-plugin/src/cypress-env-vars.ts
index 14c03b0..52f34fd 100644
--- a/packages/cypress-plugin/src/cypress-env-vars.ts
+++ b/packages/cypress-plugin/src/cypress-env-vars.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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).
diff --git a/packages/cypress-plugin/src/index.ts b/packages/cypress-plugin/src/index.ts
index 90b54e5..9a7cbdc 100644
--- a/packages/cypress-plugin/src/index.ts
+++ b/packages/cypress-plugin/src/index.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import _debug from "debug";
import {
@@ -19,6 +19,7 @@ import {
ENV_VAR_AUTO_SUPPORT,
ENV_VAR_UNFLAKABLE_RESOLVED_CONFIG_JSON,
} from "./config-env-vars";
+import cypressOnFix from "cypress-on-fix";
export { PluginOptions };
@@ -100,9 +101,14 @@ const wrapSetupNodeEvents =
| undefined
) =>
async (
- on: Cypress.PluginEvents,
+ baseOn: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise => {
+ // Due to https://github.com/cypress-io/cypress/issues/22428, only the last event handler
+ // registered for each event type will be called. This means we'll clobber any event handlers
+ // the user registers. To avoid this, we use cypress-on-fix.
+ const on = cypressOnFix(baseOn);
+
const userModifiedConfig =
userSetupNodeEvents !== undefined
? await userSetupNodeEvents(on, config)
diff --git a/packages/cypress-plugin/src/load-user-config.ts b/packages/cypress-plugin/src/load-user-config.ts
new file mode 100644
index 0000000..d56ecc5
--- /dev/null
+++ b/packages/cypress-plugin/src/load-user-config.ts
@@ -0,0 +1,64 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+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";
+import { pathToFileURL } from "url";
+
+const debug = _debug("unflakable:load-user-config");
+
+export type LoadedConfig =
+ | {
+ default: Cypress.ConfigOptions;
+ }
+ | (Cypress.ConfigOptions & { default: undefined });
+
+export const loadUserConfigSync = (): Cypress.ConfigOptions => {
+ 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.
+ const config = require(ENV_VAR_USER_CONFIG_PATH.value) as LoadedConfig;
+ return config.default ?? config;
+};
+
+export const loadUserConfig = async (): Promise<
+ Cypress.ConfigOptions
+> => {
+ // 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 {
+ return loadUserConfigSync();
+ } 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(
+ // Windows paths don't work unless we convert them to file:// URLs.
+ pathToFileURL(ENV_VAR_USER_CONFIG_PATH.value as string).href
+ )) as LoadedConfig;
+ return config.default ?? config;
+ }
+};
diff --git a/packages/cypress-plugin/src/main.ts b/packages/cypress-plugin/src/main.ts
index 480a9cd..314509e 100755
--- a/packages/cypress-plugin/src/main.ts
+++ b/packages/cypress-plugin/src/main.ts
@@ -1,6 +1,6 @@
//#!/usr/bin/env node
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import * as cypress from "cypress";
import _debug from "debug";
@@ -27,6 +27,8 @@ import {
} from "./config-env-vars";
const CONFIG_WRAPPER_MODULE = "@unflakable/cypress-plugin/config-wrapper";
+const CONFIG_WRAPPER_SYNC_MODULE =
+ "@unflakable/cypress-plugin/config-wrapper-sync";
const debug = _debug("unflakable:main");
@@ -263,9 +265,15 @@ const main = async (): Promise => {
? path.resolve(process.cwd(), runOptions.project)
: process.cwd();
- const unflakableConfig = await loadConfig(projectRoot);
+ const unflakableConfig = await loadConfig(
+ projectRoot,
+ () => [{}, []],
+ args["test-suite-id"]
+ );
debug(`Unflakable plugin is ${unflakableConfig.enabled ? "en" : "dis"}abled`);
+ let configFile: string | undefined = undefined;
+
if (unflakableConfig.enabled) {
if (args.branch !== undefined) {
branchOverride.value = args.branch;
@@ -285,9 +293,6 @@ const main = async (): Promise => {
"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"];
}
@@ -296,43 +301,77 @@ const main = async (): Promise => {
JSON.stringify(unflakableConfig);
if (args["auto-config"]) {
+ let userConfigPath: string | undefined = undefined;
if (runOptions.config !== undefined) {
ENV_VAR_USER_CONFIG_JSON.value = JSON.stringify(runOptions.config);
} else {
- const userConfigPath = await resolveUserConfigPath(
- projectRoot,
- runOptions
- );
+ 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;
- }
+ // 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 ?? projectRoot)
+ );
+ if (userTsConfig !== null) {
+ const tsNodeProject = path.dirname(userTsConfig);
+ debug(`Setting TS_NODE_PROJECT to ${tsNodeProject}`);
+ process.env.TS_NODE_PROJECT = tsNodeProject;
+ }
+
+ // ESM configuration files can only be imported via dynamic import(), which is async.
+ // However,
+ // CommonJS files can't have top-level await, which means we can't have a CommonJS wrapper
+ // (config-wrapper-sync) import an ESM configuration file. For that, we use the ESM config
+ // wrapper. However, the ESM wrapper doesn't work for CommonJS TypeScript projects unless
+ // they explicitly set `moduleResolution: node16` (or `nodenext`), which we don't want to
+ // require from users. This is why we detect whether the project and/or config file are ESM
+ // when deciding which config wrapper to use. We assume that if the Cypress config file is
+ // .mjs/.mts, the TypeScript project (if any) is already using `node16`/`nodenext` (since
+ // otherwise, the project's own Cypress config file wouldn't work).
+ const userConfigIsEsm =
+ userConfigPath !== undefined
+ ? [".mjs", ".mts"].includes(path.extname(userConfigPath))
+ : false;
+
+ // Try to determine whether the project is using ESM, as Cypress does. See
+ // https://github.com/cypress-io/cypress/blob/62f58e00ec0e1f95bc0db3c644638e4882b91992/packages/data-context/src/data/ProjectConfigIpc.ts#L276-L285
+ try {
+ const pkgJson = JSON.parse(
+ (await fs.readFile(path.join(projectRoot, "package.json"))).toString(
+ "utf8"
+ )
+ ) as { type?: string };
+
+ configFile =
+ pkgJson.type === "module" || userConfigIsEsm
+ ? require.resolve(CONFIG_WRAPPER_MODULE)
+ : require.resolve(CONFIG_WRAPPER_SYNC_MODULE);
+ } catch (e) {
+ // Project does not have `package.json` or it was not found.
+ // Reasonable to assume not using es modules unless config is explicitly ESM.
+ configFile = userConfigIsEsm
+ ? require.resolve(CONFIG_WRAPPER_MODULE)
+ : require.resolve(CONFIG_WRAPPER_SYNC_MODULE);
}
}
@@ -347,14 +386,14 @@ const main = async (): Promise => {
...runOptions,
...(args["auto-config"]
? {
- configFile: require.resolve(CONFIG_WRAPPER_MODULE),
+ configFile,
}
: {}),
quiet: true,
}
: runOptions
);
- if (results.status === "finished") {
+ if (results.status !== "failed") {
exitDefault(results, unflakableConfig);
} else {
exitFailure(results);
diff --git a/packages/cypress-plugin/src/plugin.ts b/packages/cypress-plugin/src/plugin.ts
index d5beec6..9cf075f 100644
--- a/packages/cypress-plugin/src/plugin.ts
+++ b/packages/cypress-plugin/src/plugin.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
createTestSuiteRun,
@@ -15,10 +15,13 @@ import {
branchOverride,
commitOverride,
isTestQuarantined,
+ loadGitRepo,
normalizeTestName,
+ toPosix,
UnflakableConfig,
+ UnflakableConfigEnabled,
} from "@unflakable/plugins-common";
-import { require, userAgent } from "./utils";
+import { printWarning, require, userAgent } from "./utils";
import { configureMochaReporter } from "./reporter-config";
import {
color,
@@ -85,8 +88,12 @@ const marshalAttempt = (
return null;
}
+ // NB: These types are broken in 12.17+ due to https://github.com/cypress-io/cypress/issues/27390.
return {
- start_time: new Date(attempt.startedAt).toISOString(),
+ start_time:
+ attempt.startedAt !== undefined
+ ? new Date(attempt.startedAt).toISOString()
+ : undefined,
// NB: there's no explicit end time for each attempt, Cypress does set the duration.
duration_ms: attempt.duration,
result,
@@ -96,9 +103,7 @@ const marshalAttempt = (
// 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;
- },
+ relativeToCommonRoot: string,
results: CypressCommandLine.RunResult
): void => {
const reporterStats = reporterStatsOrDefault(results);
@@ -150,7 +155,7 @@ const displayResults = (
["Duration:", humanTime.long(results.stats.wallClockDuration ?? 0)],
[
"Spec Ran:",
- formatPath(spec.relativeToCommonRoot, getWidth(table, 1), resultColor),
+ formatPath(relativeToCommonRoot, getWidth(table, 1), resultColor),
],
] as (HorizontalTableRow | undefined)[]
)
@@ -219,136 +224,17 @@ const formatFooterSummary = (
];
};
-// 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 specs:
+ | (Cypress.Spec & {
+ relativeToCommonRoot: string;
+ })[]
+ | null = null;
private specIndex = 0;
private supportFilePath: string | null = null;
@@ -393,7 +279,7 @@ export class UnflakableCypressPlugin {
options.autoSupportFile !== false
) {
const updatedSupportFile = await this.generateSkipTestsSupportFile(
- config.supportFile
+ config
);
this.supportFilePath = updatedSupportFile;
config.supportFile = updatedSupportFile;
@@ -411,32 +297,37 @@ export class UnflakableCypressPlugin {
};
private generateSkipTestsSupportFile = async (
- configuredSupportFile: string | false
+ config: Cypress.PluginConfigOptions
): 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.
+ // node_modules. Cypress also requires the support file to be located within the project root
+ // due to https://github.com/cypress-io/cypress/issues/8599#issuecomment-1290526416.
+ const tmpdir = path.join(config.projectRoot, ".unflakable-tmp");
+ await fs.mkdir(tmpdir, { recursive: true });
+
const supportFilePath =
(await promisify(tmpName)({
- prefix: "unflakable-cypress-support-file",
- })) + ".js";
+ prefix: "cypress-support-file",
+ tmpdir,
+ })) + ".cjs";
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}`);
+ if (config.supportFile !== false) {
+ debug(`Will load existing support file from ${config.supportFile}`);
}
const supportFileContents = `
require(${JSON.stringify(skipTestsPath)}).registerMochaInstrumentation();
${
- configuredSupportFile !== false
- ? `require(${JSON.stringify(configuredSupportFile)});`
+ config.supportFile !== false
+ ? `require(${JSON.stringify(config.supportFile)});`
: ""
}
`;
@@ -471,15 +362,19 @@ ${
commit === undefined ||
commit.length === 0)
) {
- const { branch: gitBranch, commit: gitCommit } = await autoDetectGit(
- console.error.bind(console)
- );
+ const git = await loadGitRepo();
+ if (git !== null) {
+ const { branch: gitBranch, commit: gitCommit } = await autoDetectGit(
+ git,
+ console.error.bind(console)
+ );
- if (branch === undefined || branch.length === 0) {
- branch = gitBranch;
- }
- if (commit === undefined || commit.length === 0) {
- commit = gitCommit;
+ if (branch === undefined || branch.length === 0) {
+ branch = gitBranch;
+ }
+ if (commit === undefined || commit.length === 0) {
+ commit = gitCommit;
+ }
}
}
@@ -494,7 +389,8 @@ ${
end_time: new Date(results.endedTestsAt).toISOString(),
test_runs: testRuns,
},
- testSuiteId: this.unflakableConfig.testSuiteId,
+ testSuiteId: (this.unflakableConfig as UnflakableConfigEnabled)
+ .testSuiteId,
apiKey: this.apiKey,
baseUrl: this.unflakableConfig.apiBaseUrl,
clientDescription: userAgentStr,
@@ -531,7 +427,12 @@ ${
const debug = baseDebug.extend("beforeRun");
debug("Received beforeRun event");
- this.specs = specs ?? [];
+ this.specs =
+ (specs as
+ | (Cypress.Spec & {
+ relativeToCommonRoot: string;
+ })[]
+ | undefined) ?? [];
displayRunStarting({
// Some of these public types seem wrong since Cypress passes the same values to the
// `before:run` event as it does to displayRunStarting():
@@ -560,15 +461,28 @@ ${
const debug = baseDebug.extend("afterRun");
debug("Received afterRun event");
- if (results.status === "finished") {
- renderSummaryTable(results);
+ if (this.supportFilePath !== null) {
+ debug(`Deleting temp support file ${this.supportFilePath}`);
+ await fs.unlink(this.supportFilePath).catch((e) => {
+ printWarning(
+ `Failed to delete temp support file ${
+ this.supportFilePath as string
+ }: ${e as string}`
+ );
+ });
+ }
+
+ if (results.status !== "failed") {
+ this.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 filename = toPosix(
+ path.relative(this.repoRoot, spec.absolute)
+ );
const isQuarantined =
this.manifest !== null &&
this.unflakableConfig.quarantineMode !== "no_quarantine" &&
@@ -597,11 +511,6 @@ ${
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 => {
@@ -635,12 +544,136 @@ ${
// This can be set by Cypress Cloud.
if (!results.skippedSpec) {
- displayResults(
- spec as Cypress.Spec & {
- relativeToCommonRoot: string;
- },
- results
- );
+ displayResults(this.relativeToCommonRoot(spec), results);
+ }
+ };
+
+ private relativeToCommonRoot = (spec: Cypress.Spec): string =>
+ (this.specs ?? []).find((fullSpec) => fullSpec.absolute === spec.absolute)
+ ?.relativeToCommonRoot ?? spec.name;
+
+ // Adapted from:
+ // https://github.com/cypress-io/cypress/blob/19e091d0bc2d1f4e6a6e62d2f81ea6a2f60d531a/packages/server/lib/util/print-run.ts#L299-L395
+ private 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 ?? run.stats.duration ?? 0
+ );
+
+ const formattedSpec = formatPath(
+ this.relativeToCommonRoot(run.spec),
+ 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("");
}
};
}
diff --git a/packages/cypress-plugin/src/reporter-common.ts b/packages/cypress-plugin/src/reporter-common.ts
index 11d6a62..3b8bbbc 100644
--- a/packages/cypress-plugin/src/reporter-common.ts
+++ b/packages/cypress-plugin/src/reporter-common.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Reporter stats from running a single spec.
export type ReporterStats = Mocha.Stats & {
diff --git a/packages/cypress-plugin/src/reporter-config.test.ts b/packages/cypress-plugin/src/reporter-config.test.ts
index 55d40b6..9fe454d 100644
--- a/packages/cypress-plugin/src/reporter-config.test.ts
+++ b/packages/cypress-plugin/src/reporter-config.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { configureMochaReporter } from "./reporter-config";
import { QuarantineMode } from "@unflakable/plugins-common";
diff --git a/packages/cypress-plugin/src/reporter-config.ts b/packages/cypress-plugin/src/reporter-config.ts
index d8e4951..dfbfbe8 100644
--- a/packages/cypress-plugin/src/reporter-config.ts
+++ b/packages/cypress-plugin/src/reporter-config.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { ReporterConfig } from "./reporter";
import { printWarning, require } from "./utils";
diff --git a/packages/cypress-plugin/src/reporter.ts b/packages/cypress-plugin/src/reporter.ts
index 5e24966..c1fdddd 100644
--- a/packages/cypress-plugin/src/reporter.ts
+++ b/packages/cypress-plugin/src/reporter.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import _debug from "debug";
import {
@@ -13,6 +13,7 @@ import {
import { TestSuiteManifest } from "@unflakable/js-api";
import {
isTestQuarantined,
+ toPosix,
UnflakableConfig,
} from "@unflakable/plugins-common";
import path from "path";
@@ -21,6 +22,7 @@ import deepEqual from "deep-equal";
import { printWarning } from "./utils";
import { ReporterStats } from "./reporter-common";
import styles, { ForegroundColor } from "ansi-styles";
+import escapeStringRegexp from "escape-string-regexp";
const debug = _debug("unflakable:reporter");
@@ -79,6 +81,30 @@ const stringifyDiffObjs = (err: Mocha.Error): void => {
}
};
+// Mocha test fail events for hook failures have the hook name in the title. However, we need to
+// extract the title of the affected test, which we do using a regex below.
+const extractTestTitleFromFailedHookTitle = (
+ failedHookTitle: string,
+ currentHookName: string
+): string => {
+ // Mocha concatenates the hook name to the test title here:
+ // https://github.com/mochajs/mocha/blob/0be3f78491bbbcdc4dcea660ee7bfd557a225d9c/lib/runner.js#L331
+ const matches = RegExp(
+ `^${escapeStringRegexp(currentHookName)} for "(.*)"$`
+ ).exec(failedHookTitle);
+ if (matches !== null) {
+ debug(
+ `extracted test title "${matches[1]}" from failed hook title "${failedHookTitle}"`
+ );
+ return matches[1];
+ } else {
+ debug(
+ `could not extract test title from failed hook title "${failedHookTitle}" with current hook name "${currentHookName}"`
+ );
+ return failedHookTitle;
+ }
+};
+
const mergeTestFailure = (
src: { err?: Mocha.Error },
dest: { err?: Mocha.Error }
@@ -92,42 +118,45 @@ const mergeTestFailure = (
// 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 ?? []),
- ];
+ if (!(dest.err.multiple ?? []).some((err) => deepEqual(src.err, err))) {
+ dest.err.multiple = [
+ src.err,
+ ...(src.err.multiple ?? []),
+ ...(dest.err.multiple ?? []),
+ ];
+ }
} else {
dest.err = src.err;
}
}
};
+const addHookNameToError = (hookName: string, err: Mocha.Error): void => {
+ const hookMsg = `${hookName} failed:\n`;
+ if (err.message !== undefined) {
+ // This is consumed by formatErrorMsgAndStack().
+ if (err.stack !== undefined) {
+ const indexIntoStackOrMsg = err.stack.indexOf(err.message);
+ if (indexIntoStackOrMsg !== -1) {
+ err.stack =
+ err.stack.slice(0, indexIntoStackOrMsg) +
+ hookMsg +
+ err.stack.slice(indexIntoStackOrMsg);
+ }
+ }
+ err.message = hookMsg + err.message;
+ }
+};
+
// 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 }
+ test: T,
+ titlePath: TestTitle
): 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
@@ -138,14 +167,14 @@ const pushOrMergeFailedTest = (
// guaranteed (especially if tests run in parallel) due to the async nature of Cypress.
.find(
(existingTestFailure) =>
- existingTestFailure.test.id === test.id &&
+ deepEqual(existingTestFailure.titlePath, titlePath) &&
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 });
+ failedTests.push({ test, titlePath, err: test.err });
return true;
} else {
mergeTestFailure(test, existingTestFailure);
@@ -259,17 +288,18 @@ const reportFailedAttempt = ({
);
};
+type TestTitle = string[];
+
// 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;
+ titlePath: TestTitle;
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 {
@@ -277,9 +307,11 @@ export default class UnflakableSpecReporter extends reporters.Base {
private readonly config: UnflakableConfig;
private readonly manifest: TestSuiteManifest | null;
- private readonly testFilename: string | null;
+ private readonly posixTestFilename: string | null;
- private indents = 0;
+ private currentSuiteTitles: string[] = [];
+ private currentHookName: string | null = null;
+ private lastHookName: string | null = null;
// Used for computing the set of skipped tests from each suite.
private nonSkippedTestJsonTitlePaths: Set = new Set();
@@ -316,17 +348,19 @@ export default class UnflakableSpecReporter extends reporters.Base {
options.reporterOptions as ReporterConfig;
this.config = config;
this.manifest = manifest ?? null;
- this.testFilename =
+ this.posixTestFilename =
runner.suite.file !== undefined
- ? path.relative(
- repoRoot,
- // Absolute path of the spec file.
- path.join(projectRoot, runner.suite.file)
+ ? toPosix(
+ 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"}`);
+ debug(`Repo-relative test filename: ${this.posixTestFilename ?? "null"}`);
runner.on(Runner.constants.EVENT_RUN_BEGIN, this.onRunBegin.bind(this));
runner.once(Runner.constants.EVENT_RUN_END, this.onRunEnd.bind(this));
@@ -334,6 +368,10 @@ export default class UnflakableSpecReporter extends reporters.Base {
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_HOOK_BEGIN, this.onHookBegin.bind(this));
+ runner.on(Runner.constants.EVENT_HOOK_END, this.onHookEnd.bind(this));
+
+ runner.on(Runner.constants.EVENT_TEST_BEGIN, this.onTestBegin.bind(this));
runner.on(Runner.constants.EVENT_TEST_FAIL, this.onTestFail.bind(this));
runner.on(
Runner.constants.EVENT_TEST_PENDING,
@@ -356,7 +394,8 @@ export default class UnflakableSpecReporter extends reporters.Base {
? styles.color[c].open + str + styles.color[c].close
: str;
- private indent = (): string => Array(this.indents).join(" ");
+ private indent = (): string =>
+ Array(this.currentSuiteTitles.length).join(" ");
private isQuarantined = (titlePath: TestTitle): boolean => {
// For ignore_failure (and skip_tests if somehow the quarantined tests still executed), ignore
@@ -365,18 +404,18 @@ export default class UnflakableSpecReporter extends reporters.Base {
return false;
}
- if (this.testFilename === null) {
+ if (this.posixTestFilename === null) {
throw new Error("Suite has no `file` attribute");
}
const isQuarantined =
this.manifest !== null &&
- isTestQuarantined(this.manifest, this.testFilename, titlePath);
+ isTestQuarantined(this.manifest, this.posixTestFilename, titlePath);
debug(
`Test is ${isQuarantined ? "" : "NOT "}quarantined: ${JSON.stringify(
titlePath
- )} in file ${this.testFilename}`
+ )} in file ${this.posixTestFilename}`
);
return isQuarantined;
@@ -396,15 +435,16 @@ export default class UnflakableSpecReporter extends reporters.Base {
// 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
+ (suiteTest) =>
+ deepEqual(suiteTest.titlePath(), lastTestAttempt.titlePath)
) ?? lastTestAttempt.test;
const testTitle = formatFailedTestTitle(suiteTest.titlePath());
const failedAttempts: FailedTestAttempt[] = [
...this.retriedTests
- .filter(
- (retriedTest) => retriedTest.test.id === lastTestAttempt.test.id
+ .filter((retriedTest) =>
+ deepEqual(retriedTest.titlePath, lastTestAttempt.titlePath)
)
.map((retriedTest) => ({
currentRetry: currentTestRetry(retriedTest.test),
@@ -658,7 +698,7 @@ export default class UnflakableSpecReporter extends reporters.Base {
this.specTests.push(test.titlePath());
});
- this.indents++;
+ this.currentSuiteTitles.push(suite.title);
console.log(reporters.Base.color("suite", this.indent() + suite.title));
};
@@ -669,8 +709,8 @@ export default class UnflakableSpecReporter extends reporters.Base {
}\` root=${String(suite.root)}`
);
- this.indents--;
- if (this.indents === 1) {
+ this.currentSuiteTitles.pop();
+ if (this.currentSuiteTitles.length === 1) {
console.log();
}
};
@@ -683,6 +723,7 @@ export default class UnflakableSpecReporter extends reporters.Base {
// 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.
+ // For hook failures, this gets called with this.currentHookName still set.
private onTestFail = (test: CypressTest, _err?: Mocha.Error): void => {
debug(
`onTestFail [${test.state ?? "no state"}] ${test.title}: ${
@@ -690,23 +731,30 @@ export default class UnflakableSpecReporter extends reporters.Base {
}`
);
- // 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;
+ if (test.err !== undefined && this.currentHookName !== null) {
+ addHookNameToError(this.currentHookName, test.err);
+ }
+
+ const titlePath =
+ this.currentHookName !== null
+ ? [
+ ...test.titlePath().slice(0, -1),
+ extractTestTitleFromFailedHookTitle(
+ test.title,
+ this.currentHookName
+ ),
+ ]
+ : test.titlePath();
// 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);
+ pushOrMergeFailedTest(this.retriedTests, test, titlePath);
+ } else if (this.isQuarantined(titlePath)) {
+ pushOrMergeFailedTest(this.quarantinedFailures, test, titlePath);
} else {
- pushOrMergeFailedTest(this.unquarantinedFailures, test);
+ pushOrMergeFailedTest(this.unquarantinedFailures, test, titlePath);
}
// NB: We don't print any output in this function since, for non-final-attempts, it only gets
@@ -720,9 +768,17 @@ export default class UnflakableSpecReporter extends reporters.Base {
const isFlaky = currentRetry > 0;
const isQuarantined = isFlaky && this.isQuarantined(test.titlePath());
if (isFlaky && isQuarantined) {
- this.quarantinedFlakes.push({ test, err: test.err });
+ this.quarantinedFlakes.push({
+ test,
+ titlePath: test.titlePath(),
+ err: test.err,
+ });
} else if (isFlaky) {
- this.unquarantinedFlakes.push({ test, err: test.err });
+ this.unquarantinedFlakes.push({
+ test,
+ titlePath: test.titlePath(),
+ err: test.err,
+ });
}
// Cypress overrides the behavior of the Mocha spec reporter (see
@@ -756,9 +812,9 @@ export default class UnflakableSpecReporter extends reporters.Base {
const isQuarantined = this.isQuarantined(test.titlePath());
if (isQuarantined) {
- pushOrMergeFailedTest(this.quarantinedFailures, test);
+ pushOrMergeFailedTest(this.quarantinedFailures, test, test.titlePath());
} else {
- pushOrMergeFailedTest(this.unquarantinedFailures, test);
+ pushOrMergeFailedTest(this.unquarantinedFailures, test, test.titlePath());
}
const quarantinedFmt = ` ${reporters.Base.symbols.err} ${test.title} [failed, quarantined]`;
@@ -777,9 +833,45 @@ export default class UnflakableSpecReporter extends reporters.Base {
);
};
+ private onHookBegin = (hook: CypressTest & { hookName: string }): void => {
+ debug(
+ `onHookBegin [${hook.state ?? "no state"}] ${hook.title}: ${
+ hook.err?.message ?? ""
+ }`
+ );
+
+ this.currentHookName = hook.title;
+ };
+
+ private onHookEnd = (test: CypressTest): void => {
+ debug(
+ `onHookEnd [${test.state ?? "no state"}] ${test.title}: ${
+ test.err?.message ?? ""
+ }`
+ );
+
+ this.lastHookName = this.currentHookName;
+ this.currentHookName = null;
+ };
+
+ private onTestBegin = (test: CypressTest): void => {
+ debug(
+ `onTestBegin [${test.state ?? "no state"}] ${test.title}: ${
+ test.err?.message ?? ""
+ }`
+ );
+
+ // It's possible the previous hook failed without invoking onHookEnd(). Once a test starts
+ // running, we stop caring about previous hooks.
+ this.lastHookName = null;
+ this.currentHookName = null;
+ };
+
// 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.
+ // For hook failures, this gets called with this.currentHookName still set. The test title never
+ // has the hook name in it here.
private onTestEnd = (test: CypressTest): void => {
debug(
`onTestEnd [${test.state ?? "no state"}] ${test.title}: ${
@@ -792,6 +884,10 @@ export default class UnflakableSpecReporter extends reporters.Base {
if (test.state === "passed") {
this.reportTestPassed(test);
} else if (test.state === "failed") {
+ if (test.err !== undefined && this.currentHookName !== null) {
+ addHookNameToError(this.currentHookName, test.err);
+ }
+
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
@@ -801,7 +897,7 @@ export default class UnflakableSpecReporter extends reporters.Base {
if (currentRetry < testRetries(test)) {
const existingTestFailureIdx = this.retriedTests.findIndex(
(existingTestFailure) =>
- existingTestFailure.test.id === test.id &&
+ deepEqual(existingTestFailure.titlePath, test.titlePath()) &&
currentTestRetry(existingTestFailure.test) === currentRetry
);
if (existingTestFailureIdx !== -1) {
@@ -845,12 +941,12 @@ export default class UnflakableSpecReporter extends reporters.Base {
// warning in this case rather than treating the test as flaky or potentially quarantining
// it.
if (currentTestRetry(test) > 0) {
- if (this.testFilename === null) {
+ if (this.posixTestFilename === null) {
throw new Error("Suite has no `file` attribute");
}
printWarning(
- `test ${titlePathJson} in file ${this.testFilename} was pending (skipped) during retry`
+ `test ${titlePathJson} in file ${this.posixTestFilename} was pending (skipped) during retry`
);
}
@@ -872,6 +968,10 @@ export default class UnflakableSpecReporter extends reporters.Base {
};
// NB: An `err` argument is never passed to this event handler.
+ // For hook failures, this gets called without this.currentHookName set because onHookEnd() gets
+ // called first. Instead, we use this.lastHookName. The test.hookName attribute is also set, which
+ // lets us determine if the error was caused by a hook. However, this doesn't include the full
+ // user-supplied hook title, so we instead rely on currentHookName/lastHookName.
private onTestRetry = (
test: MochaEventTest & { hookName?: string }
): void => {
@@ -881,11 +981,26 @@ export default class UnflakableSpecReporter extends reporters.Base {
}`
);
+ const hookName =
+ test.hookName !== undefined
+ ? this.currentHookName ?? this.lastHookName
+ : null;
+
+ const titlePath = [
+ // The root suite has an empty title that needs to be skipped.
+ ...this.currentSuiteTitles.slice(1),
+ test.title,
+ ];
+
+ if (test.err !== undefined && hookName !== null) {
+ addHookNameToError(hookName, test.err);
+ }
+
// 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
+ pushOrMergeFailedTest(this.retriedTests, test, titlePath) ||
+ hookName === null
) {
console.log(
this.indent() +
diff --git a/packages/cypress-plugin/src/skip-tests.ts b/packages/cypress-plugin/src/skip-tests.ts
index 01ac7f5..deabbc8 100644
--- a/packages/cypress-plugin/src/skip-tests.ts
+++ b/packages/cypress-plugin/src/skip-tests.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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`.
@@ -27,6 +27,7 @@ import {
CYPRESS_ENV_VAR_MANIFEST,
CYPRESS_ENV_VAR_REPO_ROOT,
} from "./cypress-env-vars";
+import convertPath from "@stdlib/utils-convert-path";
const debug = _debug("unflakable:skip-tests");
@@ -110,7 +111,15 @@ const instrumentTestFn = (
config: Cypress.TestConfigOverrides | undefined,
fn: Mocha.Func | undefined
): Mocha.Test => {
- const testFilename = path.relative(repoRoot, Cypress.spec.absolute);
+ // NB: On Windows, Cypress.spec.absolute uses a mixed path convention (e.g., C:/foo/bar),
+ // while repoRoot uses a Windows path convention (e.g., C:\foo\bar). In order for
+ // path.relative to work correctly, we have to convert both to POSIX conventions (e.g.,
+ // /c/foo/bar). This is because path-browserify doesn't have win32 support:
+ // https://github.com/browserify/path-browserify/issues/1.
+ const testFilename = path.relative(
+ convertPath(repoRoot, "posix"),
+ convertPath(Cypress.spec.absolute, "posix")
+ );
const titlePath = [...suiteStack, title];
const isQuarantined =
manifest !== null &&
diff --git a/packages/cypress-plugin/src/tsconfig.json b/packages/cypress-plugin/src/tsconfig.json
index 44badea..80d8585 100644
--- a/packages/cypress-plugin/src/tsconfig.json
+++ b/packages/cypress-plugin/src/tsconfig.json
@@ -1,4 +1,4 @@
{
"extends": "../tsconfig.json",
- "include": ["../mocha.d.ts", ".", ".eslintrc.js"]
+ "include": ["../cypress-on-fix.d.ts", "../mocha.d.ts", ".", ".eslintrc.js"]
}
diff --git a/packages/cypress-plugin/src/utils.ts b/packages/cypress-plugin/src/utils.ts
index 95d5fa7..f2f0bad 100644
--- a/packages/cypress-plugin/src/utils.ts
+++ b/packages/cypress-plugin/src/utils.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import chalk from "chalk";
import { readFileSync } from "fs";
diff --git a/packages/cypress-plugin/src/vendored/cli-table3.d.ts b/packages/cypress-plugin/src/vendored/cli-table3.d.ts
index 14c7749..f7b1b70 100644
--- a/packages/cypress-plugin/src/vendored/cli-table3.d.ts
+++ b/packages/cypress-plugin/src/vendored/cli-table3.d.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Cypress imports this even though it's not part of the cli-table3 exports, so we add some types
// here.
diff --git a/packages/cypress-plugin/test/.eslintrc.cjs b/packages/cypress-plugin/test/.eslintrc.cjs
index 7b41633..ea0aeee 100644
--- a/packages/cypress-plugin/test/.eslintrc.cjs
+++ b/packages/cypress-plugin/test/.eslintrc.cjs
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
env: {
diff --git a/packages/cypress-plugin/test/integration-common/.eslintrc.js b/packages/cypress-plugin/test/integration-common/.eslintrc.js
deleted file mode 100644
index b6febf5..0000000
--- a/packages/cypress-plugin/test/integration-common/.eslintrc.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (c) 2023 Developer Innovations, LLC
-
-module.exports = {
- extends: ["../../../../.eslintrc-ts.js"],
-};
diff --git a/packages/cypress-plugin/test/integration-common/rollup.config.mjs b/packages/cypress-plugin/test/integration-common/rollup.config.mjs
deleted file mode 100644
index fcf3630..0000000
--- a/packages/cypress-plugin/test/integration-common/rollup.config.mjs
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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
deleted file mode 100644
index 81ce31b..0000000
--- a/packages/cypress-plugin/test/integration-common/src/config.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// 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/tsconfig.json b/packages/cypress-plugin/test/integration-common/tsconfig.json
deleted file mode 100644
index 6008fb3..0000000
--- a/packages/cypress-plugin/test/integration-common/tsconfig.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "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
index b6febf5..0015a28 100644
--- a/packages/cypress-plugin/test/integration-input-esm/.eslintrc.cjs
+++ b/packages/cypress-plugin/test/integration-input-esm/.eslintrc.cjs
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index afbffdc..b21da85 100644
--- a/packages/cypress-plugin/test/integration-input-esm/config-js/devtools.js
+++ b/packages/cypress-plugin/test/integration-input-esm/config-js/devtools.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
export const openDevToolsOnLaunch =
/**
diff --git a/packages/cypress-plugin/test/integration-input-esm/config-js/headless.js b/packages/cypress-plugin/test/integration-input-esm/config-js/headless.js
new file mode 100644
index 0000000..82602cc
--- /dev/null
+++ b/packages/cypress-plugin/test/integration-input-esm/config-js/headless.js
@@ -0,0 +1,31 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+/**
+ * Workaround for https://github.com/cypress-io/cypress/issues/27804.
+ *
+ * @param {Cypress.PluginEvents} on
+ * @returns void
+ */
+export const fixHeadlessChrome = (on) => {
+ 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" &&
+ browser.isHeadless
+ ) {
+ launchOptions.args = launchOptions.args.map((arg) =>
+ arg === "--headless" ? "--headless=new" : arg
+ );
+ }
+
+ 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
index 4f0f5a3..6be9d7f 100644
--- a/packages/cypress-plugin/test/integration-input-esm/config-js/tasks.js
+++ b/packages/cypress-plugin/test/integration-input-esm/config-js/tasks.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
/**
* @param {Cypress.PluginEvents} on
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
index d159eac..60d0dad 100644
--- a/packages/cypress-plugin/test/integration-input-esm/config-js/webpack.js
+++ b/packages/cypress-plugin/test/integration-input-esm/config-js/webpack.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { default as webpack } from "webpack";
diff --git a/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts b/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts
index 58d80d5..f117e1a 100644
--- a/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/config/devtools.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
export const openDevToolsOnLaunch = (on: Cypress.PluginEvents): void => {
// Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or
diff --git a/packages/cypress-plugin/test/integration-input-esm/config/headless.ts b/packages/cypress-plugin/test/integration-input-esm/config/headless.ts
new file mode 100644
index 0000000..81a2718
--- /dev/null
+++ b/packages/cypress-plugin/test/integration-input-esm/config/headless.ts
@@ -0,0 +1,24 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+// Workaround for https://github.com/cypress-io/cypress/issues/27804.
+export const fixHeadlessChrome = (on: Cypress.PluginEvents): void => {
+ on(
+ "before:browser:launch",
+ (
+ browser: Cypress.Browser,
+ launchOptions: Cypress.BrowserLaunchOptions
+ ): void | Cypress.BrowserLaunchOptions => {
+ if (
+ browser.family === "chromium" &&
+ browser.name !== "electron" &&
+ browser.isHeadless
+ ) {
+ launchOptions.args = launchOptions.args.map((arg) =>
+ arg === "--headless" ? "--headless=new" : arg
+ );
+ }
+
+ 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
index 5d492df..0a5cb4f 100644
--- a/packages/cypress-plugin/test/integration-input-esm/config/tasks.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/config/tasks.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
diff --git a/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts b/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts
index ff784d0..ca31892 100644
--- a/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/config/webpack.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { default as webpack } from "webpack";
diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs b/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs
index 77e3bcf..d85ac80 100644
--- a/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs
+++ b/packages/cypress-plugin/test/integration-input-esm/cypress-config.cjs
@@ -1,9 +1,9 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
-const { registerSimpleGitMock } = require("cypress-integration-common/git");
+const { registerSimpleGitMock } = require("unflakable-test-common/dist/git");
const {
registerCosmiconfigMock,
-} = require("cypress-integration-common/config");
+} = require("unflakable-test-common/dist/config");
module.exports = {
/**
@@ -17,6 +17,9 @@ module.exports = {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
async setupNodeEvents(on, _config) {
+ const { fixHeadlessChrome } = await import("./config-js/headless.js");
+
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
@@ -38,6 +41,9 @@ module.exports = {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
async setupNodeEvents(on, _config) {
+ const { fixHeadlessChrome } = await import("./config-js/headless.js");
+
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress-config.js b/packages/cypress-plugin/test/integration-input-esm/cypress-config.js
index 2b14fc0..4cd3f6e 100644
--- a/packages/cypress-plugin/test/integration-input-esm/cypress-config.js
+++ b/packages/cypress-plugin/test/integration-input-esm/cypress-config.js
@@ -1,10 +1,11 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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";
+import { registerSimpleGitMock } from "unflakable-test-common/dist/git.js";
+import { registerCosmiconfigMock } from "unflakable-test-common/dist/config.js";
+import { fixHeadlessChrome } from "./config-js/headless.js";
/**
* @type {Cypress.ConfigOptions}
@@ -17,6 +18,7 @@ export default {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
setupNodeEvents(on, _config) {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
@@ -35,6 +37,7 @@ export default {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
setupNodeEvents(on, _config) {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
diff --git a/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts b/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts
index 9bd64c9..9b90f72 100644
--- a/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/cypress.config.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { defineConfig } from "cypress";
// Relative import paths require file extensions in ESM.
@@ -6,12 +6,14 @@ import { defineConfig } from "cypress";
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";
+import { registerSimpleGitMock } from "unflakable-test-common/dist/git.js";
+import { registerCosmiconfigMock } from "unflakable-test-common/dist/config.js";
+import { fixHeadlessChrome } from "./config/headless.js";
export default defineConfig({
component: {
setupNodeEvents(on: Cypress.PluginEvents, _config) {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
@@ -31,6 +33,7 @@ export default defineConfig({
| Promise
| Cypress.PluginConfigOptions
| void {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
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
index 1a7516c..5eb705e 100644
--- 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
@@ -1,3 +1,3 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index 0b677aa..b257777 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
require("./commands.js");
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
index 3c38662..e784a05 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Import commands.js using ES2015 syntax:
import "./commands.js";
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
index c254fe4..cd10e9e 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index 0447f29..1e2cc15 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index a6cb68a..876f349 100644
--- a/packages/cypress-plugin/test/integration-input-esm/cypress/support/commands.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/commands.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Webpack non-deterministically reports a TypeScript error here when run through Cypress. It seems
// to happen when `cypress-support-file` includes
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
index e39ba42..92d3f43 100644
--- 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
@@ -1,3 +1,5 @@
+
+
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
index 87db971..8225d55 100644
--- a/packages/cypress-plugin/test/integration-input-esm/cypress/support/component.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/component.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import "./commands.ts";
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
index 8fd06b4..9127c7b 100644
--- a/packages/cypress-plugin/test/integration-input-esm/cypress/support/e2e.ts
+++ b/packages/cypress-plugin/test/integration-input-esm/cypress/support/e2e.ts
@@ -1,3 +1,3 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index d591569..6a5dbfe 100644
--- a/packages/cypress-plugin/test/integration-input-esm/package.json
+++ b/packages/cypress-plugin/test/integration-input-esm/package.json
@@ -7,8 +7,7 @@
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@unflakable/cypress-plugin": "workspace:^",
- "cypress": "10 - 12",
- "cypress-integration-common": "workspace:^",
+ "cypress": "11.2 - 13",
"mocha": "=7.0.1",
"mocha-junit-reporter": "^2.2.0",
"process": "^0.11.10",
@@ -16,6 +15,7 @@
"react-dom": "^18.2.0",
"ts-loader": "^9.4.3",
"typescript": "^4.9.5",
+ "unflakable-test-common": "workspace:^",
"webpack": "^5.84.1"
},
"scripts": {
diff --git a/packages/cypress-plugin/test/integration-input-esm/tsconfig.json b/packages/cypress-plugin/test/integration-input-esm/tsconfig.json
index aff7c99..30ac11e 100644
--- a/packages/cypress-plugin/test/integration-input-esm/tsconfig.json
+++ b/packages/cypress-plugin/test/integration-input-esm/tsconfig.json
@@ -2,7 +2,9 @@
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
- "module": "ES2022",
+ // node16 adds ESM support.
+ "module": "node16",
+ "moduleResolution": "node16",
"preserveSymlinks": true,
// The ts-loader that Cypress uses doesn't seem to support the `??` operator.
"target": "ES2019",
diff --git a/packages/cypress-plugin/test/integration-input-manual/config/devtools.js b/packages/cypress-plugin/test/integration-input-manual/config/devtools.js
index c117765..e96f27e 100644
--- a/packages/cypress-plugin/test/integration-input-manual/config/devtools.js
+++ b/packages/cypress-plugin/test/integration-input-manual/config/devtools.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
/**
diff --git a/packages/cypress-plugin/test/integration-input-manual/config/headless.js b/packages/cypress-plugin/test/integration-input-manual/config/headless.js
new file mode 100644
index 0000000..f6540a0
--- /dev/null
+++ b/packages/cypress-plugin/test/integration-input-manual/config/headless.js
@@ -0,0 +1,33 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+module.exports = {
+ /**
+ * Workaround for https://github.com/cypress-io/cypress/issues/27804.
+ *
+ * @param {Cypress.PluginEvents} on
+ * @returns void
+ */
+ fixHeadlessChrome: (on) => {
+ 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" &&
+ browser.isHeadless
+ ) {
+ launchOptions.args = launchOptions.args.map((arg) =>
+ arg === "--headless" ? "--headless=new" : arg
+ );
+ }
+
+ 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
index 95cba3e..45c7d8a 100644
--- a/packages/cypress-plugin/test/integration-input-manual/config/tasks.js
+++ b/packages/cypress-plugin/test/integration-input-manual/config/tasks.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
/**
diff --git a/packages/cypress-plugin/test/integration-input-manual/config/webpack.js b/packages/cypress-plugin/test/integration-input-manual/config/webpack.js
index c6501b8..6e44c41 100644
--- a/packages/cypress-plugin/test/integration-input-manual/config/webpack.js
+++ b/packages/cypress-plugin/test/integration-input-manual/config/webpack.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
const webpack = require("webpack");
diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs b/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs
index 37d987c..e372c25 100644
--- a/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs
+++ b/packages/cypress-plugin/test/integration-input-manual/cypress-config.mjs
@@ -1,11 +1,15 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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 { registerSimpleGitMock } from "unflakable-test-common/dist/git.js";
+import { registerCosmiconfigMock } from "unflakable-test-common/dist/config.js";
import { registerUnflakable } from "@unflakable/cypress-plugin";
+import semverGte from "semver/functions/gte.js";
+import path from "path";
+import headless from "./config/headless.js";
+import cypressOnFix from "cypress-on-fix";
/**
* @type {Cypress.ConfigOptions}
@@ -13,11 +17,13 @@ import { registerUnflakable } from "@unflakable/cypress-plugin";
export default {
component: {
/**
- * @param {Cypress.PluginEvents} on
+ * @param {Cypress.PluginEvents} baseOn
* @param {Cypress.PluginConfigOptions} config
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
- setupNodeEvents(on, config) {
+ setupNodeEvents(baseOn, config) {
+ const on = cypressOnFix(baseOn);
+ headless.fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
tasks.registerTasks(on);
@@ -33,16 +39,26 @@ export default {
},
e2e: {
/**
- * @param {Cypress.PluginEvents} on
+ * @param {Cypress.PluginEvents} baseOn
* @param {Cypress.PluginConfigOptions} config
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
- setupNodeEvents(on, config) {
+ setupNodeEvents(baseOn, config) {
+ const on = cypressOnFix(baseOn);
+ headless.fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
tasks.registerTasks(on);
devtools.openDevToolsOnLaunch(on);
+ // Versions prior to 12.17.4 use Webpack 4, which doesn't support the package.json "exports"
+ // field (see https://github.com/cypress-io/cypress/issues/23826). Webpack 5 both supports and
+ // enforces this field, so we have to use a different require path to manually import the
+ // skip-tests module.
+ if (semverGte(config.version, "12.17.4")) {
+ config.supportFile = path.resolve("./cypress/support/e2e-webpack5.js");
+ }
+
return registerUnflakable(on, config);
},
// supportFile: "cypress/support/e2e.js",
diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress.config.js b/packages/cypress-plugin/test/integration-input-manual/cypress.config.js
index f11bae3..b45cbd1 100644
--- a/packages/cypress-plugin/test/integration-input-manual/cypress.config.js
+++ b/packages/cypress-plugin/test/integration-input-manual/cypress.config.js
@@ -1,14 +1,18 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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 { registerSimpleGitMock } = require("unflakable-test-common/dist/git");
const {
registerCosmiconfigMock,
-} = require("cypress-integration-common/config");
+} = require("unflakable-test-common/dist/config");
+const semverGte = require("semver/functions/gte");
const { registerUnflakable } = require("@unflakable/cypress-plugin");
+const path = require("path");
+const { fixHeadlessChrome } = require("./config/headless");
+const cypressOnFix = require("cypress-on-fix");
module.exports = {
/**
@@ -16,11 +20,13 @@ module.exports = {
*/
component: {
/**
- * @param {Cypress.PluginEvents} on
+ * @param {Cypress.PluginEvents} baseOn
* @param {Cypress.PluginConfigOptions} config
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
- setupNodeEvents(on, config) {
+ setupNodeEvents(baseOn, config) {
+ const on = cypressOnFix(baseOn);
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
@@ -36,16 +42,26 @@ module.exports = {
},
e2e: {
/**
- * @param {Cypress.PluginEvents} on
+ * @param {Cypress.PluginEvents} baseOn
* @param {Cypress.PluginConfigOptions} config
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
- setupNodeEvents(on, config) {
+ setupNodeEvents(baseOn, config) {
+ const on = cypressOnFix(baseOn);
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
openDevToolsOnLaunch(on);
+ // Versions prior to 12.17.4 use Webpack 4, which doesn't support the package.json "exports"
+ // field (see https://github.com/cypress-io/cypress/issues/23826). Webpack 5 both supports and
+ // enforces this field, so we have to use a different require path to manually import the
+ // skip-tests module.
+ if (semverGte(config.version, "12.17.4")) {
+ config.supportFile = path.resolve("./cypress/support/e2e-webpack5.js");
+ }
+
return registerUnflakable(on, config);
},
// supportFile: "cypress/support/e2e.js",
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
index c1bec18..cc6a872 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
const testFn = Cypress.env("SKIP_FAILURES") !== undefined ? it.skip : it;
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
index 1bab7bc..fef71a2 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
/*
let calls = 0;
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
index 3eca195..3478d79 100644
--- 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
@@ -1,8 +1,9 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
describe("describe block", () => {
if (Cypress.env("SKIP_BEFORE_HOOK") === undefined) {
before(
+ "before hook with title",
/**
* @param {Mocha.Done} done
*/
@@ -21,6 +22,7 @@ describe("describe block", () => {
if (Cypress.env("SKIP_BEFORE_EACH_HOOK") === undefined) {
beforeEach(
+ "beforeEach hook with title",
/**
* @param {Mocha.Done} done
*/
@@ -49,6 +51,7 @@ describe("describe block", () => {
if (Cypress.env("SKIP_AFTER_EACH_HOOK") === undefined) {
afterEach(
+ "afterEach hook with title",
/**
* @param {Mocha.Done} done
*/
@@ -67,6 +70,7 @@ describe("describe block", () => {
if (Cypress.env("SKIP_AFTER_HOOK") === undefined) {
after(
+ "after hook with title",
/**
* @param {Mocha.Done} done
*/
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
index aa580f4..a5af12b 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index e731026..3094edd 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
describe("spec with mixed test results", () => {
const quarantinedTestFn =
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
index 05d345a..5b22ca4 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
it("should pass", () => {
// Make sure the project's support file works even when skip_tests generates a temporary one on
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
index 2704e82..8f7488a 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
it("stub should be pending");
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
index f43ebc0..6682e4c 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
describe("describe block", () => {
(Cypress.env("SKIP_QUARANTINED") !== undefined ? it.skip : it)(
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
index 1a7516c..5eb705e 100644
--- a/packages/cypress-plugin/test/integration-input-manual/cypress/support/commands.js
+++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/commands.js
@@ -1,3 +1,3 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index e39ba42..92d3f43 100644
--- 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
@@ -1,3 +1,5 @@
+
+
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
index 5d0823f..d60f49e 100644
--- a/packages/cypress-plugin/test/integration-input-manual/cypress/support/component.js
+++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/component.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
require("./commands");
diff --git a/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e-webpack5.js b/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e-webpack5.js
new file mode 100644
index 0000000..8defad6
--- /dev/null
+++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e-webpack5.js
@@ -0,0 +1,10 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+// Import commands.js using CJS syntax:
+require("./commands.js");
+
+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
index 9033340..5ae257d 100644
--- a/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e.js
+++ b/packages/cypress-plugin/test/integration-input-manual/cypress/support/e2e.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Import commands.js using CJS syntax:
require("./commands.js");
diff --git a/packages/cypress-plugin/test/integration-input-manual/package.json b/packages/cypress-plugin/test/integration-input-manual/package.json
index f880b09..c67d68d 100644
--- a/packages/cypress-plugin/test/integration-input-manual/package.json
+++ b/packages/cypress-plugin/test/integration-input-manual/package.json
@@ -9,16 +9,18 @@
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@unflakable/cypress-plugin": "workspace:^",
- "cypress": "10 - 12",
- "cypress-integration-common": "workspace:^",
+ "cypress": "11.2 - 13",
"cypress-multi-reporters": "^1.6.3",
+ "cypress-on-fix": "^1.0.2",
"mocha": "=7.0.1",
"mocha-junit-reporter": "^2.2.0",
"process": "^0.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "semver": "^7.5.4",
"ts-loader": "^9.4.3",
"typescript": "^4.9.5",
+ "unflakable-test-common": "workspace:^",
"webpack": "^5.84.1"
}
}
diff --git a/packages/cypress-plugin/test/integration-input/.eslintrc.js b/packages/cypress-plugin/test/integration-input/.eslintrc.js
index b6febf5..0015a28 100644
--- a/packages/cypress-plugin/test/integration-input/.eslintrc.js
+++ b/packages/cypress-plugin/test/integration-input/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index c117765..e96f27e 100644
--- a/packages/cypress-plugin/test/integration-input/config-js/devtools.js
+++ b/packages/cypress-plugin/test/integration-input/config-js/devtools.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
/**
diff --git a/packages/cypress-plugin/test/integration-input/config-js/headless.js b/packages/cypress-plugin/test/integration-input/config-js/headless.js
new file mode 100644
index 0000000..f6540a0
--- /dev/null
+++ b/packages/cypress-plugin/test/integration-input/config-js/headless.js
@@ -0,0 +1,33 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+module.exports = {
+ /**
+ * Workaround for https://github.com/cypress-io/cypress/issues/27804.
+ *
+ * @param {Cypress.PluginEvents} on
+ * @returns void
+ */
+ fixHeadlessChrome: (on) => {
+ 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" &&
+ browser.isHeadless
+ ) {
+ launchOptions.args = launchOptions.args.map((arg) =>
+ arg === "--headless" ? "--headless=new" : arg
+ );
+ }
+
+ 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
index 95cba3e..45c7d8a 100644
--- a/packages/cypress-plugin/test/integration-input/config-js/tasks.js
+++ b/packages/cypress-plugin/test/integration-input/config-js/tasks.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
/**
diff --git a/packages/cypress-plugin/test/integration-input/config-js/webpack.js b/packages/cypress-plugin/test/integration-input/config-js/webpack.js
index c6501b8..6e44c41 100644
--- a/packages/cypress-plugin/test/integration-input/config-js/webpack.js
+++ b/packages/cypress-plugin/test/integration-input/config-js/webpack.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
const webpack = require("webpack");
diff --git a/packages/cypress-plugin/test/integration-input/config/devtools.ts b/packages/cypress-plugin/test/integration-input/config/devtools.ts
index 58d80d5..f117e1a 100644
--- a/packages/cypress-plugin/test/integration-input/config/devtools.ts
+++ b/packages/cypress-plugin/test/integration-input/config/devtools.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
export const openDevToolsOnLaunch = (on: Cypress.PluginEvents): void => {
// Open DevTools automatically. Only works for headed modes (i.e., not in screenshots or
diff --git a/packages/cypress-plugin/test/integration-input/config/headless.ts b/packages/cypress-plugin/test/integration-input/config/headless.ts
new file mode 100644
index 0000000..81a2718
--- /dev/null
+++ b/packages/cypress-plugin/test/integration-input/config/headless.ts
@@ -0,0 +1,24 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+// Workaround for https://github.com/cypress-io/cypress/issues/27804.
+export const fixHeadlessChrome = (on: Cypress.PluginEvents): void => {
+ on(
+ "before:browser:launch",
+ (
+ browser: Cypress.Browser,
+ launchOptions: Cypress.BrowserLaunchOptions
+ ): void | Cypress.BrowserLaunchOptions => {
+ if (
+ browser.family === "chromium" &&
+ browser.name !== "electron" &&
+ browser.isHeadless
+ ) {
+ launchOptions.args = launchOptions.args.map((arg) =>
+ arg === "--headless" ? "--headless=new" : arg
+ );
+ }
+
+ return launchOptions;
+ }
+ );
+};
diff --git a/packages/cypress-plugin/test/integration-input/config/tasks.ts b/packages/cypress-plugin/test/integration-input/config/tasks.ts
index 5d492df..0a5cb4f 100644
--- a/packages/cypress-plugin/test/integration-input/config/tasks.ts
+++ b/packages/cypress-plugin/test/integration-input/config/tasks.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
diff --git a/packages/cypress-plugin/test/integration-input/config/webpack.ts b/packages/cypress-plugin/test/integration-input/config/webpack.ts
index d159eac..60d0dad 100644
--- a/packages/cypress-plugin/test/integration-input/config/webpack.ts
+++ b/packages/cypress-plugin/test/integration-input/config/webpack.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { default as webpack } from "webpack";
diff --git a/packages/cypress-plugin/test/integration-input/cypress-config.js b/packages/cypress-plugin/test/integration-input/cypress-config.js
index e83d1a0..2131165 100644
--- a/packages/cypress-plugin/test/integration-input/cypress-config.js
+++ b/packages/cypress-plugin/test/integration-input/cypress-config.js
@@ -1,12 +1,13 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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 { registerSimpleGitMock } = require("unflakable-test-common/dist/git");
const {
registerCosmiconfigMock,
-} = require("cypress-integration-common/config");
+} = require("unflakable-test-common/dist/config");
+const { fixHeadlessChrome } = require("config-js/headless");
module.exports = {
/**
@@ -20,6 +21,7 @@ module.exports = {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
setupNodeEvents(on, _config) {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
@@ -38,6 +40,7 @@ module.exports = {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
setupNodeEvents(on, _config) {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
diff --git a/packages/cypress-plugin/test/integration-input/cypress-config.mjs b/packages/cypress-plugin/test/integration-input/cypress-config.mjs
index 74e8a64..caa4a1c 100644
--- a/packages/cypress-plugin/test/integration-input/cypress-config.mjs
+++ b/packages/cypress-plugin/test/integration-input/cypress-config.mjs
@@ -1,10 +1,11 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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";
+import { registerSimpleGitMock } from "unflakable-test-common/dist/git.js";
+import { registerCosmiconfigMock } from "unflakable-test-common/dist/config.js";
+import headless from "./config-js/headless.js";
/**
* @type {Cypress.ConfigOptions}
@@ -17,6 +18,7 @@ export default {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
setupNodeEvents(on, _config) {
+ headless.fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
tasks.registerTasks(on);
@@ -35,6 +37,7 @@ export default {
* @returns {Promise | Cypress.PluginConfigOptions | void}
*/
setupNodeEvents(on, _config) {
+ headless.fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
tasks.registerTasks(on);
diff --git a/packages/cypress-plugin/test/integration-input/cypress.config.ts b/packages/cypress-plugin/test/integration-input/cypress.config.ts
index 4e1efe8..2cc784d 100644
--- a/packages/cypress-plugin/test/integration-input/cypress.config.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress.config.ts
@@ -1,7 +1,7 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
-import { registerSimpleGitMock } from "cypress-integration-common/git";
-import { registerCosmiconfigMock } from "cypress-integration-common/config";
+import { registerSimpleGitMock } from "unflakable-test-common/dist/git";
+import { registerCosmiconfigMock } from "unflakable-test-common/dist/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
@@ -12,10 +12,14 @@ import { openDevToolsOnLaunch } from "config/devtools";
import webpackConfig from "config/webpack";
// eslint-disable-next-line import/no-unresolved
import { registerTasks } from "config/tasks";
+// eslint-disable-next-line import/no-unresolved
+import { fixHeadlessChrome } from "config/headless";
+import cypressOnFix from "cypress-on-fix";
export default defineConfig({
component: {
setupNodeEvents(on: Cypress.PluginEvents, _config) {
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
@@ -30,12 +34,20 @@ export default defineConfig({
},
e2e: {
setupNodeEvents(
- on: Cypress.PluginEvents,
+ baseOn: Cypress.PluginEvents,
_config
):
| Promise
| Cypress.PluginConfigOptions
| void {
+ // Due to https://github.com/cypress-io/cypress/issues/22428, only the last event handler
+ // registered for each event type will be called. This means we'll clobber any event handlers
+ // the user registers. To avoid this, we use cypress-on-fix.
+ // NB: Our plugin ordinarily does this for us, but we use this package to test what happens
+ // when the plugin is disabled.
+ const on = cypressOnFix(baseOn);
+
+ fixHeadlessChrome(on);
registerCosmiconfigMock();
registerSimpleGitMock();
registerTasks(on);
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
index 94b40a9..167c20f 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/component/fail.cy.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/component/fail.cy.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
const testFn = Cypress.env("SKIP_FAILURES") !== undefined ? it.skip : it;
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
index 6cda5a0..6011bf6 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/component/flake.cy.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/component/flake.cy.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
/*
let calls = 0;
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
index f37bae2..ba0a1c1 100644
--- 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
@@ -1,8 +1,8 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
describe("describe block", () => {
if (Cypress.env("SKIP_BEFORE_HOOK") === undefined) {
- before((done: Mocha.Done) => {
+ before("before hook with title", (done: Mocha.Done) => {
process.nextTick(() => {
throw new Error("before Error #1");
});
@@ -15,7 +15,7 @@ describe("describe block", () => {
}
if (Cypress.env("SKIP_BEFORE_EACH_HOOK") === undefined) {
- beforeEach((done: Mocha.Done) => {
+ beforeEach("beforeEach hook with title", (done: Mocha.Done) => {
process.nextTick(() => {
throw new Error("beforeEach Error #1");
});
@@ -37,7 +37,7 @@ describe("describe block", () => {
it("should be skipped", () => undefined);
if (Cypress.env("SKIP_AFTER_EACH_HOOK") === undefined) {
- afterEach((done: Mocha.Done) => {
+ afterEach("afterEach hook with title", (done: Mocha.Done) => {
process.nextTick(() => {
throw new Error("afterEach Error #1");
});
@@ -50,7 +50,7 @@ describe("describe block", () => {
}
if (Cypress.env("SKIP_AFTER_HOOK") === undefined) {
- after((done: Mocha.Done) => {
+ after("after hook with title", (done: Mocha.Done) => {
process.nextTick(() => {
throw new Error("after Error #1");
});
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
index aa580f4..a5af12b 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/component/invalid.cy.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/component/invalid.cy.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index b0ee438..5fd29be 100644
--- 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
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
describe("spec with mixed test results", () => {
const quarantinedTestFn =
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
index 05d345a..5b22ca4 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/component/pass.cy.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/component/pass.cy.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
it("should pass", () => {
// Make sure the project's support file works even when skip_tests generates a temporary one on
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
index 2704e82..8f7488a 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/component/pending.cy.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/component/pending.cy.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
it("stub should be pending");
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
index f43ebc0..6682e4c 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/component/quarantined.cy.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/component/quarantined.cy.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
describe("describe block", () => {
(Cypress.env("SKIP_QUARANTINED") !== undefined ? it.skip : it)(
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
index 1a7516c..5eb705e 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support-js/commands.js
+++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/commands.js
@@ -1,3 +1,3 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index fea2a9b..04783d5 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support-js/component.js
+++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/component.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
require("./commands");
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
index 3c38662..e784a05 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support-js/component.mjs
+++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/component.mjs
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Import commands.js using ES2015 syntax:
import "./commands.js";
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
index c254fe4..cd10e9e 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.js
+++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index 0447f29..1e2cc15 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.mjs
+++ b/packages/cypress-plugin/test/integration-input/cypress/support-js/e2e.mjs
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index 966021a..ad471e1 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support/commands.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/support/commands.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
///
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
index e39ba42..b2019a7 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support/component-index.html
+++ b/packages/cypress-plugin/test/integration-input/cypress/support/component-index.html
@@ -1,3 +1,5 @@
+
+
diff --git a/packages/cypress-plugin/test/integration-input/cypress/support/component.ts b/packages/cypress-plugin/test/integration-input/cypress/support/component.ts
index 5fbee34..08f39a2 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support/component.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/support/component.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Import commands.js using ES2015 syntax:
import "./commands";
diff --git a/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts b/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts
index 5aa0b7c..4284eff 100644
--- a/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts
+++ b/packages/cypress-plugin/test/integration-input/cypress/support/e2e.ts
@@ -1,3 +1,3 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index 1f5bc39..6c81b9e 100644
--- a/packages/cypress-plugin/test/integration-input/package.json
+++ b/packages/cypress-plugin/test/integration-input/package.json
@@ -6,8 +6,7 @@
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@unflakable/cypress-plugin": "workspace:^",
- "cypress": "10 - 12",
- "cypress-integration-common": "workspace:^",
+ "cypress": "11.2 - 13",
"mocha": "=7.0.1",
"mocha-junit-reporter": "^2.2.0",
"process": "^0.11.10",
@@ -15,6 +14,7 @@
"react-dom": "^18.2.0",
"ts-loader": "^9.4.3",
"typescript": "^4.9.5",
+ "unflakable-test-common": "workspace:^",
"webpack": "^5.84.1"
},
"scripts": {
diff --git a/packages/cypress-plugin/test/integration-input/tsconfig.json b/packages/cypress-plugin/test/integration-input/tsconfig.json
index 1243db8..e7f8717 100644
--- a/packages/cypress-plugin/test/integration-input/tsconfig.json
+++ b/packages/cypress-plugin/test/integration-input/tsconfig.json
@@ -2,6 +2,8 @@
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
+ // Make sure the plugin works without requiring `moduleResolution: nodenext`.
+ "moduleResolution": "node",
"preserveSymlinks": true,
// The ts-loader that Cypress uses doesn't seem to support the `??` operator.
"target": "ES2019",
@@ -9,6 +11,7 @@
"types": ["cypress", "node"]
},
"include": [
+ "../../cypress-on-fix.d.ts",
".eslintrc.js",
"config",
"config-js",
diff --git a/packages/cypress-plugin/test/integration/.eslintrc.js b/packages/cypress-plugin/test/integration/.eslintrc.js
index b6febf5..0015a28 100644
--- a/packages/cypress-plugin/test/integration/.eslintrc.js
+++ b/packages/cypress-plugin/test/integration/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
index fba616d..db3dac5 100644
--- a/packages/cypress-plugin/test/integration/jest.config.js
+++ b/packages/cypress-plugin/test/integration/jest.config.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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
@@ -13,6 +13,6 @@ module.exports = {
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,
+ testTimeout: 240000,
verbose: true,
};
diff --git a/packages/cypress-plugin/test/integration/package.json b/packages/cypress-plugin/test/integration/package.json
index 43c3ea0..36a0a97 100644
--- a/packages/cypress-plugin/test/integration/package.json
+++ b/packages/cypress-plugin/test/integration/package.json
@@ -3,21 +3,20 @@
"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:^",
+ "cypress": "11.2 - 13",
"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",
+ "mockttp": "^3.9.2",
+ "semver": "^7.5.4",
"ts-jest": "^29.1.0",
- "typescript": "^4.9.5"
+ "typescript": "^4.9.5",
+ "unflakable-test-common": "workspace:^"
},
"scripts": {
"test": "jest --useStderr --verbose --runInBand",
diff --git a/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts b/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts
index 3d5aec9..29cd967 100644
--- a/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts
+++ b/packages/cypress-plugin/test/integration/src/basic-matrix.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -10,7 +10,7 @@ import { QuarantineMode } from "@unflakable/plugins-common";
import { afterEach, beforeEach } from "@jest/globals";
import * as fs from "fs/promises";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
Object.entries(TEST_PROJECTS).forEach(([projectName, project]) => {
describe(
projectName === "integration-input"
@@ -60,6 +60,16 @@ integrationTestSuite(() => {
}
: null,
configFile,
+ envVars:
+ // moduleResolution must be `node16` or `nodenext` in order to load a
+ // .mjs ESM config file.
+ projectName === "integration-input" &&
+ configFile === "cypress-config.mjs"
+ ? {
+ TS_NODE_COMPILER_OPTIONS:
+ '{"moduleResolution":"node16"}',
+ }
+ : {},
project: projectName as TestProjectName,
testMode,
expectQuarantinedTestsToBeSkipped:
@@ -74,6 +84,7 @@ integrationTestSuite(() => {
}
: defaultSummaryTotals,
},
+ mockBackend,
done
)
);
diff --git a/packages/cypress-plugin/test/integration/src/basic.test.ts b/packages/cypress-plugin/test/integration/src/basic.test.ts
index 7809d01..d785029 100644
--- a/packages/cypress-plugin/test/integration/src/basic.test.ts
+++ b/packages/cypress-plugin/test/integration/src/basic.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
integrationTestSuite,
@@ -7,7 +7,7 @@ import {
} from "./test-wrappers";
import { QuarantineMode } from "@unflakable/plugins-common";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it("quarantine flaky test", (done) =>
integrationTest(
{
@@ -21,6 +21,7 @@ integrationTestSuite(() => {
numQuarantined: 5,
},
},
+ mockBackend,
done
));
@@ -43,6 +44,7 @@ integrationTestSuite(() => {
numTests: 19,
},
},
+ mockBackend,
done
));
@@ -76,6 +78,7 @@ integrationTestSuite(() => {
numTests: 19,
},
},
+ mockBackend,
done
)
);
diff --git a/packages/cypress-plugin/test/integration/src/config.test.ts b/packages/cypress-plugin/test/integration/src/config.test.ts
index c813e90..ffc364b 100644
--- a/packages/cypress-plugin/test/integration/src/config.test.ts
+++ b/packages/cypress-plugin/test/integration/src/config.test.ts
@@ -1,8 +1,8 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { integrationTestSuite, integrationTest } from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it("set test suite ID via config", (done) =>
integrationTest(
{
@@ -16,6 +16,7 @@ integrationTestSuite(() => {
expectedSuiteId: "MOCK_SUITE_ID_CONFIG",
},
},
+ mockBackend,
done
));
@@ -33,10 +34,26 @@ integrationTestSuite(() => {
expectedSuiteId: "MOCK_SUITE_ID_ENV",
},
},
+ mockBackend,
done
));
it("set test suite ID via CLI", (done) =>
+ integrationTest(
+ {
+ params: {
+ cliArgs: ["--test-suite-id", "MOCK_SUITE_ID_CLI"],
+ envVars: {
+ UNFLAKABLE_SUITE_ID: undefined,
+ },
+ expectedSuiteId: "MOCK_SUITE_ID_CLI",
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("set test suite ID via CLI override", (done) =>
integrationTest(
{
params: {
@@ -51,6 +68,7 @@ integrationTestSuite(() => {
expectedSuiteId: "MOCK_SUITE_ID_CLI",
},
},
+ mockBackend,
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
index 463c9dc..2539b47 100644
--- a/packages/cypress-plugin/test/integration/src/disable-plugin.test.ts
+++ b/packages/cypress-plugin/test/integration/src/disable-plugin.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -6,7 +6,7 @@ import {
integrationTestSuite,
} from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
const expectedExitCodeWithPluginDisabled = 11;
const summaryTotalsWithPluginDisabled = {
...defaultSummaryTotals,
@@ -27,6 +27,7 @@ integrationTestSuite(() => {
expectedExitCode: expectedExitCodeWithPluginDisabled,
summaryTotals: summaryTotalsWithPluginDisabled,
},
+ mockBackend,
done
));
@@ -42,6 +43,7 @@ integrationTestSuite(() => {
expectedExitCode: expectedExitCodeWithPluginDisabled,
summaryTotals: summaryTotalsWithPluginDisabled,
},
+ mockBackend,
done
));
@@ -57,6 +59,7 @@ integrationTestSuite(() => {
},
},
},
+ mockBackend,
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
index f9171e3..fe6f5b4 100644
--- a/packages/cypress-plugin/test/integration/src/disable-upload.test.ts
+++ b/packages/cypress-plugin/test/integration/src/disable-upload.test.ts
@@ -1,8 +1,8 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { integrationTest, integrationTestSuite } from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it("disable upload via config", (done) =>
integrationTest(
{
@@ -13,6 +13,7 @@ integrationTestSuite(() => {
expectResultsToBeUploaded: false,
},
},
+ mockBackend,
done
));
@@ -26,6 +27,7 @@ integrationTestSuite(() => {
expectResultsToBeUploaded: false,
},
},
+ mockBackend,
done
));
@@ -41,6 +43,7 @@ integrationTestSuite(() => {
expectResultsToBeUploaded: false,
},
},
+ mockBackend,
done
));
@@ -56,6 +59,7 @@ integrationTestSuite(() => {
},
},
},
+ mockBackend,
done
));
});
diff --git a/packages/cypress-plugin/test/integration/src/git.test.ts b/packages/cypress-plugin/test/integration/src/git.test.ts
index 26290db..1594a79 100644
--- a/packages/cypress-plugin/test/integration/src/git.test.ts
+++ b/packages/cypress-plugin/test/integration/src/git.test.ts
@@ -1,9 +1,9 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { integrationTest, integrationTestSuite } from "./test-wrappers";
import path from "path";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it("no git repo", (done) =>
integrationTest(
{
@@ -17,6 +17,7 @@ integrationTestSuite(() => {
},
},
},
+ mockBackend,
done
));
@@ -47,6 +48,7 @@ integrationTestSuite(() => {
expectedBranch: "pull/MOCK_PR_NUMBER/merge",
},
},
+ mockBackend,
done
));
@@ -62,6 +64,7 @@ integrationTestSuite(() => {
expectedCommit: "MOCK_COMMIT_ENV",
},
},
+ mockBackend,
done
));
@@ -84,6 +87,7 @@ integrationTestSuite(() => {
expectedCommit: "MOCK_COMMIT_CLI",
},
},
+ mockBackend,
done
));
@@ -100,6 +104,7 @@ integrationTestSuite(() => {
expectedRepoRelativePathPrefix: "",
},
},
+ mockBackend,
done
));
@@ -114,6 +119,7 @@ integrationTestSuite(() => {
expectedRepoRelativePathPrefix: "",
},
},
+ mockBackend,
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
index 224384e..68fedf1 100644
--- a/packages/cypress-plugin/test/integration/src/hook-failures.test.ts
+++ b/packages/cypress-plugin/test/integration/src/hook-failures.test.ts
@@ -1,9 +1,17 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { integrationTest, integrationTestSuite } from "./test-wrappers";
+import semverLt from "semver/functions/lt";
+import cypressPackage from "cypress/package.json";
-integrationTestSuite(() => {
- it("run should succeed when before() fails both tests are quarantined", (done) =>
+// Cypress 13.4 broke the handling of multiple hook failures. Prior to that version, Cypress/Mocha
+// reported both errors as failures of the first test in the suite, and then skipped all remaining
+// tests. Beginning in 13.4, Cypress skips all tests in the suite and never reports either error.
+// This was most likely introduced in https://github.com/cypress-io/cypress/pull/27930.
+const supportMultipleHookErrors = semverLt(cypressPackage.version, "13.4.0");
+
+integrationTestSuite((mockBackend) => {
+ it("run should succeed when before() fails and both tests are quarantined", (done) =>
integrationTest(
{
params: {
@@ -23,10 +31,11 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
- it("run should succeed when beforeEach() fails both tests are quarantined", (done) =>
+ it("run should succeed when beforeEach() fails and both tests are quarantined", (done) =>
integrationTest(
{
params: {
@@ -48,10 +57,11 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
- it("run should succeed when afterEach() fails both tests are quarantined", (done) =>
+ it("run should succeed when afterEach() fails and both tests are quarantined", (done) =>
integrationTest(
{
params: {
@@ -73,6 +83,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -95,6 +106,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -118,6 +130,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -141,6 +154,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -164,6 +178,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -188,6 +203,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -212,6 +228,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -238,6 +255,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -262,6 +280,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -288,30 +307,35 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
- it("multiple before() hook errors", (done) =>
- integrationTest(
- {
- params: {
- multipleHookErrors: true,
- specNameStubs: ["hook-fail"],
+ (supportMultipleHookErrors ? it : it.skip)(
+ "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,
+ },
},
- expectedExitCode: 1,
- summaryTotals: {
- icon: "fail",
- numFailing: 1,
- numFlaky: 0,
- numPassing: 0,
- numPending: 0,
- numQuarantined: 0,
- numSkipped: 1,
- numTests: 2,
- },
- },
- done
- ));
+ mockBackend,
+ done
+ )
+ );
it("test and afterEach() hook errors", (done) =>
integrationTest(
@@ -334,6 +358,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
done
));
@@ -358,6 +383,7 @@ integrationTestSuite(() => {
numTests: 2,
},
},
+ mockBackend,
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
index 1a10435..9b242f7 100644
--- a/packages/cypress-plugin/test/integration/src/long-names.test.ts
+++ b/packages/cypress-plugin/test/integration/src/long-names.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -7,7 +7,7 @@ import {
} from "./test-wrappers";
import { QuarantineMode } from "@unflakable/plugins-common";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it.each(["ignore_failures", "skip_tests"] as QuarantineMode[])(
"test names longer than 4096 chars should be truncated w/ quarantineMode = %s",
(quarantineMode, done) =>
@@ -35,6 +35,7 @@ integrationTestSuite(() => {
}
: defaultSummaryTotals,
},
+ mockBackend,
done
)
);
@@ -66,6 +67,7 @@ integrationTestSuite(() => {
numQuarantined: quarantineMode === "skip_tests" ? 6 : 5,
},
},
+ mockBackend,
done
)
);
diff --git a/packages/cypress-plugin/test/integration/src/matchers.ts b/packages/cypress-plugin/test/integration/src/matchers.ts
index 065a17f..b092d2c 100644
--- a/packages/cypress-plugin/test/integration/src/matchers.ts
+++ b/packages/cypress-plugin/test/integration/src/matchers.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { expect } from "@jest/globals";
import type { MatcherFunction } from "expect";
diff --git a/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts b/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts
index 6bd9b90..c31e12b 100644
--- a/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts
+++ b/packages/cypress-plugin/test/integration/src/no-quarantine.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -6,7 +6,7 @@ import {
integrationTestSuite,
} from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it.each(["CLI", "config"] as ("CLI" | "config")[])(
"set quarantineMode to no_quarantine via %s",
(mode, done) =>
@@ -35,6 +35,7 @@ integrationTestSuite(() => {
numFlaky: 3,
},
},
+ mockBackend,
done
)
);
diff --git a/packages/cypress-plugin/test/integration/src/parse-output.ts b/packages/cypress-plugin/test/integration/src/parse-output.ts
index d8e31f9..b82d778 100644
--- a/packages/cypress-plugin/test/integration/src/parse-output.ts
+++ b/packages/cypress-plugin/test/integration/src/parse-output.ts
@@ -1,12 +1,8 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
/* eslint-disable no-control-regex */
-import {
- specProjectPath,
- TEST_SPEC_STUBS,
- TestCaseParams,
-} from "./run-test-case";
+import { specPattern, TEST_SPEC_STUBS, TestCaseParams } from "./run-test-case";
export type RunStarting = {
specs: string[];
@@ -185,8 +181,6 @@ const parseRunStarting = (
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"
@@ -213,7 +207,12 @@ const parseRunStarting = (
expect(tableEntries["Cypress"]).toMatch(/^[0-9]+\.[0-9]+\.[0-9]+$/);
expect(tableEntries["Browser"]).toMatch(
- /^Chrome [0-9]+ \x1B\[90m\(headless\)\x1B\[39m$/
+ new RegExp(
+ `^${
+ // Chrome is slow to launch on Windows.
+ process.platform === "win32" ? "Edge" : "Chrome"
+ } [0-9]+ \x1B\\[90m\\(headless\\)\x1B\\[39m$`
+ )
);
expect(tableEntries["Node Version"]).toMatch(
new RegExp(
@@ -224,13 +223,7 @@ const parseRunStarting = (
)} \\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}`
- );
+ expect(tableEntries["Searched"]).toBe(specPattern(params));
const parsedSpecsLine = tableEntries["Specs"].match(
/^([0-9]+) found \((.*)\)$/
@@ -942,7 +935,7 @@ const parseSummaryTable = (
.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$/
+ /^\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(
@@ -991,7 +984,7 @@ const parseSummaryTable = (
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$/
+ /^\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,
@@ -1074,7 +1067,7 @@ const parsePluginDisabledSummaryTable = (
.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$/
+ /^\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\[(?:[39]4m(?[0-9]+)|90m-)\x1B\[39m \x1B\[90m│\x1B\[39m$/
);
expect(
@@ -1116,7 +1109,7 @@ const parsePluginDisabledSummaryTable = (
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$/
+ /^\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\[(?:[39]4m(?[0-9]+)|90m-)\x1B\[39m \x1B\[90m \x1B\[39m$/
);
expect(
parsedSummaryTotalsOrNull,
diff --git a/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts b/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts
index 823d9b6..09dd6ba 100644
--- a/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts
+++ b/packages/cypress-plugin/test/integration/src/plugin-failures.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -6,7 +6,7 @@ import {
integrationTestSuite,
} from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it("run should not fail due to error fetching manifest", (done) =>
integrationTest(
{
@@ -31,6 +31,7 @@ integrationTestSuite(() => {
numTests: 19,
},
},
+ mockBackend,
done
));
@@ -42,6 +43,7 @@ integrationTestSuite(() => {
},
expectedExitCode: 1,
},
+ mockBackend,
done
));
@@ -61,6 +63,7 @@ integrationTestSuite(() => {
numQuarantined: 0,
},
},
+ mockBackend,
done
));
});
diff --git a/packages/cypress-plugin/test/integration/src/retries.test.ts b/packages/cypress-plugin/test/integration/src/retries.test.ts
index 317a5a2..ab1acf6 100644
--- a/packages/cypress-plugin/test/integration/src/retries.test.ts
+++ b/packages/cypress-plugin/test/integration/src/retries.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -6,7 +6,7 @@ import {
integrationTestSuite,
} from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
describe.each(["CLI", "config"] as ("CLI" | "config")[])(
"set failureRetries via %s",
(mode) => {
@@ -32,6 +32,7 @@ integrationTestSuite(() => {
numFailing: 8,
},
},
+ mockBackend,
done
));
@@ -51,6 +52,7 @@ integrationTestSuite(() => {
expectedRetries: 1,
},
},
+ mockBackend,
done
));
@@ -70,6 +72,7 @@ integrationTestSuite(() => {
expectedRetries: 3,
},
},
+ mockBackend,
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
index d999de7..a463246 100644
--- a/packages/cypress-plugin/test/integration/src/run-test-case.ts
+++ b/packages/cypress-plugin/test/integration/src/run-test-case.ts
@@ -1,48 +1,44 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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 { CompletedRequest } 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 { execFile } from "child_process";
+import { promisify } from "util";
import {
CONFIG_MOCK_ENV_VAR,
CosmiconfigMockParams,
-} from "cypress-integration-common/config";
+} from "unflakable-test-common/dist/config";
import {
GIT_MOCK_ENV_VAR,
SimpleGitMockParams,
-} from "cypress-integration-common/git";
+} from "unflakable-test-common/dist/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");
+import {
+ MockBackend,
+ UnmatchedEndpoints,
+} from "unflakable-test-common/dist/mock-backend";
+import {
+ AsyncTestError,
+ spawnTestWithTimeout,
+} from "unflakable-test-common/dist/spawn";
+import cypressPackage from "cypress/package.json";
+import semverLt from "semver/functions/lt";
-// Jest times out after 70 seconds, so we bail early here to allow time to print the
+// Jest times out after 240 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 TEST_TIMEOUT_MS = 230000;
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]\\)"
@@ -132,6 +128,9 @@ export type TestCaseParams = {
testMode: TestMode;
};
+export const projectPath = (params: TestCaseParams): string =>
+ path.join("..", params.project);
+
export const specFilename = (
params: TestCaseParams,
specNameStub: string
@@ -145,18 +144,30 @@ export const specProjectPath = (
specNameStub: string
): string => `cypress/${params.testMode}/${specFilename(params, specNameStub)}`;
+export const specPattern = (params: TestCaseParams): string => {
+ const { specNameStubs, testMode } = params;
+ return specNameStubs !== undefined && specNameStubs.length > 0
+ ? // Cypress doesn't convert to relative path on Windows due to hard-coding a forward slash
+ // into the path. See:
+ // https://github.com/cypress-io/cypress/blob/3d0a2b406115db292130df774348c4f1fd4a3240/packages/server/lib/modes/run.ts#L52
+ specNameStubs
+ .map((stub) =>
+ process.platform === "win32"
+ ? `${path.resolve(projectPath(params))}\\${specProjectPath(
+ params,
+ stub
+ ).replaceAll("/", "\\")}`
+ : specProjectPath(params, stub)
+ )
+ .join(", ")
+ : testMode === "component"
+ ? "**/*.cy.{js,jsx,ts,tsx}"
+ : `cypress/e2e/**/*.cy.{js,jsx,ts,tsx}`;
+};
+
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$/;
@@ -166,6 +177,12 @@ type ExpectedRunRecord = {
attemptResults: TestAttemptResult[];
};
+// Cypress 13 removed per-attempt timing information. See:
+// - https://github.com/cypress-io/cypress/issues/27732
+// - https://github.com/cypress-io/cypress/pull/27230
+// - https://github.com/cypress-io/cypress/issues/27390
+const expectPerAttemptTiming = semverLt(cypressPackage.version, "13.0.0");
+
const expectSpecRuns = (
params: TestCaseParams,
specNameStub: string,
@@ -174,11 +191,15 @@ const expectSpecRuns = (
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,
- })),
+ attempts: attemptResults.map((result) =>
+ expectPerAttemptTiming
+ ? {
+ start_time: expectExt.stringMatching(TIMESTAMP_REGEX),
+ duration_ms: expectExt.toBeAnInteger(),
+ result,
+ }
+ : { result }
+ ),
filename: specRepoPath(params, specNameStub),
name,
}))
@@ -431,20 +452,19 @@ const verifyUploadResults = (
);
};
-const addFetchMockExpectations = async (
+const addBackendExpectations = async (
params: TestCaseParams,
summaryTotals: SummaryTotals,
+ mockBackend: MockBackend,
onError: (e: unknown) => void
-): Promise<{
- unmatchedApiRequestEndpoint: MockedEndpoint;
- unmatchedObjectStoreRequestEndpoint: MockedEndpoint;
-}> => {
+): Promise => {
const {
expectedApiKey,
expectedBranch,
expectedCommit,
expectedFlakeTestNameSuffix,
expectedSuiteId,
+ expectPluginToBeEnabled,
expectResultsToBeUploaded,
failToFetchManifest,
failToUploadResults,
@@ -453,258 +473,102 @@ const addFetchMockExpectations = async (
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"],
- },
+ const manifest = {
+ 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_MIXED_QUARANTINED_FAIL",
- filename: specRepoPath(params, "mixed/mixed"),
+ test_id: "TEST_FLAKE",
+ filename: specRepoPath(params, "flake"),
name: [
- "spec with mixed test results",
- "mixed: failure should be quarantined",
+ `should be flaky${expectedFlakeTestNameSuffix}`.substring(
+ 0,
+ TEST_NAME_ENTRY_MAX_LENGTH
+ ),
],
},
{
- test_id: "TEST_MIXED_QUARANTINED_FLAKE",
+ test_id: "TEST_MIXED_FLAKE",
filename: specRepoPath(params, "mixed/mixed"),
- name: [
- "spec with mixed test results",
- "mixed: flake should be quarantined",
- ],
+ name: ["spec with mixed test results", "mixed: should be flaky"],
},
+ ]
+ : []),
+ ...(quarantineHookFail
+ ? [
{
- 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}`,
+ test_id: "TEST_HOOK_FAIL",
+ filename: specRepoPath(params, "hook-fail"),
+ name: ["describe block", "should fail due to hook"],
},
- json: {
- upload_id: "MOCK_UPLOAD_ID",
+ ]
+ : []),
+ ...(quarantineHookSkip
+ ? [
+ {
+ test_id: "TEST_HOOK_SKIP",
+ filename: specRepoPath(params, "hook-fail"),
+ name: ["describe block", "should be skipped"],
},
- };
- } 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,
+ ]
+ : []),
+ ],
};
+
+ return mockBackend.addExpectations(
+ onError,
+ failToFetchManifest ? null : manifest,
+ (request) => verifyUploadResults(params, summaryTotals, request),
+ failToUploadResults
+ ? null
+ : {
+ run_id: MOCK_RUN_ID,
+ suite_id: expectedSuiteId,
+ ...(expectedBranch !== undefined
+ ? {
+ branch: expectedBranch,
+ }
+ : {}),
+ ...(expectedCommit !== undefined
+ ? {
+ commit: expectedCommit,
+ }
+ : {}),
+ },
+ userAgentRegex,
+ {
+ expectPluginToBeEnabled,
+ expectResultsToBeUploaded,
+ expectedApiKey,
+ expectedSuiteId,
+ }
+ );
};
// Similar to debug's default formatter, but with timestamps instead of the +Nms at the end of each
@@ -732,7 +596,8 @@ _debug.formatArgs = formatDebugArgsWithTimestamp;
export const runTestCase = async (
params: TestCaseParams,
expectedExitCode: number,
- summaryTotals: SummaryTotals
+ summaryTotals: SummaryTotals,
+ mockBackend: MockBackend
): Promise => {
const {
skipFailures,
@@ -746,20 +611,23 @@ export const runTestCase = async (
multipleHookErrors,
} = params;
- const fetchMismatch = { error: undefined as unknown | undefined };
+ const asyncTestError: AsyncTestError = { error: undefined };
- const { unmatchedApiRequestEndpoint, unmatchedObjectStoreRequestEndpoint } =
- await addFetchMockExpectations(params, summaryTotals, (error) => {
- if (fetchMismatch.error === undefined) {
- fetchMismatch.error = error ?? new Error("undefined error");
+ const unmatchedRequestEndpoints = await addBackendExpectations(
+ params,
+ summaryTotals,
+ mockBackend,
+ (error) => {
+ if (asyncTestError.error === undefined) {
+ asyncTestError.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),
+ expectedSearchFrom: path.resolve(projectPath(params)),
searchResult:
params.config !== null
? {
@@ -773,7 +641,11 @@ export const runTestCase = async (
// 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"])
+ await promisify(execFile)("yarn", ["bin", "cypress-unflakable"], {
+ cwd: projectPath(params),
+ // yarn.CMD isn't executable without a shell on Windows.
+ shell: process.platform === "win32",
+ })
).stdout.trimEnd();
Object.entries(params.testEnvVars).forEach(([key, value]) => {
@@ -799,7 +671,7 @@ export const runTestCase = async (
const args = [
"--require",
- require.resolve("cypress-integration-common/mock-cosmiconfig"),
+ require.resolve("unflakable-test-common/dist/mock-cosmiconfig"),
cypressPluginBin,
...(params.project === "integration-input-manual"
? ["--no-auto-config", "--no-auto-support"]
@@ -808,9 +680,9 @@ export const runTestCase = async (
"--",
// e2e/component
`--${params.testMode}`,
- // Chrome is faster than Electron, at least on Mac.
+ // Chrome is faster than Electron, at least on Mac. However, it's much slower on Windows.
"--browser",
- "chrome",
+ process.platform === "win32" ? "edge" : "chrome",
...(params.specNameStubs !== undefined
? [
"--spec",
@@ -828,146 +700,41 @@ export const runTestCase = async (
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}`,
+ UNFLAKABLE_API_BASE_URL: `http://localhost:${mockBackend.apiServerPort}`,
[CONFIG_MOCK_ENV_VAR]: JSON.stringify(configMockParams),
[GIT_MOCK_ENV_VAR]: JSON.stringify(params.git),
+ // Windows requires these environment variables to be propagated.
+ ...(process.platform === "win32"
+ ? {
+ APPDATA: process.env.APPDATA,
+ LOCALAPPDATA: process.env.LOCALAPPDATA,
+ TMP: process.env.TMP,
+ TEMP: process.env.TEMP,
+ }
+ : {}),
};
- debug(
- `Spawning test:\n args = %o\n environment = %o\n cwd = %s`,
+ await spawnTestWithTimeout(
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
- )
+ projectPath(params),
+ TEST_TIMEOUT_MS,
+ async (stdoutLines) => {
+ verifyOutput(
+ params,
+ stdoutLines,
+ summaryTotals,
+ mockBackend.apiServerPort
+ );
+ await mockBackend.checkExpectations(unmatchedRequestEndpoints);
+ },
+ expectedExitCode,
+ false,
+ asyncTestError
);
- 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
index 7f94fc2..278ccb8 100644
--- a/packages/cypress-plugin/test/integration/src/test-wrappers.ts
+++ b/packages/cypress-plugin/test/integration/src/test-wrappers.ts
@@ -1,15 +1,12 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
-import {
- apiServer,
- objectStoreServer,
- runTestCase,
- TestCaseParams,
-} from "./run-test-case";
+import { 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";
+import * as os from "os";
+import * as util from "util";
+import { MockBackend } from "unflakable-test-common/dist/mock-backend";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@@ -23,8 +20,6 @@ declare global {
}
}
-const debug = _debug("unflakable:integration-test:test-wrappers");
-
export type TestCase = {
params: Partial;
expectedExitCode?: number;
@@ -45,6 +40,7 @@ export const defaultSummaryTotals: SummaryTotals = {
export const integrationTest = (
testCase: TestCase,
+ mockBackend: MockBackend,
done: jest.DoneCallback
): void => {
void runTestCase(
@@ -105,34 +101,23 @@ export const integrationTest = (
},
},
testCase.expectedExitCode ?? defaultExitCode,
- testCase.summaryTotals ?? defaultSummaryTotals
+ testCase.summaryTotals ?? defaultSummaryTotals,
+ mockBackend
)
.then(done)
.catch((e) => {
- done(e as string | { message: string });
+ // Ensures any chained `cause` gets printed.
+ done(util.inspect(e, { colors: true, depth: 5 }));
});
};
-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();
+export const integrationTestSuite = (
+ runTests: (mockBackend: MockBackend) => void
+): void => {
+ const mockBackend = new MockBackend();
- debug(`Stopping mock S3 server`);
- return objectStoreServer.stop();
- });
+ beforeEach(() => mockBackend.start());
+ afterEach(() => mockBackend.stop());
const cypressMinorVersion = cypressPackage.version.match(/^[^.]+\.[^.]+/);
const nodeMajorVersion = process.version.match(/^[^.]+/);
@@ -142,11 +127,23 @@ export const integrationTestSuite = (runTests: () => void): void => {
? cypressMinorVersion[0]
: cypressPackage.version
}`, () => {
- // Only use Node major version for test name.
- describe(`Node ${
- nodeMajorVersion !== null ? nodeMajorVersion[0] : process.version
- }`, () => {
- runTests();
- });
+ const platform = os.platform();
+ describe(
+ platform === "darwin"
+ ? `MacOS`
+ : platform === "linux"
+ ? "Linux"
+ : platform === "win32"
+ ? "Windows"
+ : platform,
+ () => {
+ // Only use Node major version for test name.
+ describe(`Node ${
+ nodeMajorVersion !== null ? nodeMajorVersion[0] : process.version
+ }`, () => {
+ runTests(mockBackend);
+ });
+ }
+ );
});
};
diff --git a/packages/cypress-plugin/test/integration/src/unicode.test.ts b/packages/cypress-plugin/test/integration/src/unicode.test.ts
index be9a10a..d2bdd3e 100644
--- a/packages/cypress-plugin/test/integration/src/unicode.test.ts
+++ b/packages/cypress-plugin/test/integration/src/unicode.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import {
defaultSummaryTotals,
@@ -6,7 +6,7 @@ import {
integrationTestSuite,
} from "./test-wrappers";
-integrationTestSuite(() => {
+integrationTestSuite((mockBackend) => {
it("emoji test names should be allowed", (done) =>
integrationTest(
{
@@ -17,6 +17,7 @@ integrationTestSuite(() => {
},
},
},
+ mockBackend,
done
));
@@ -37,6 +38,7 @@ integrationTestSuite(() => {
numQuarantined: 5,
},
},
+ mockBackend,
done
));
});
diff --git a/packages/cypress-plugin/test/integration/src/verify-output.ts b/packages/cypress-plugin/test/integration/src/verify-output.ts
index ec5b440..33ebb92 100644
--- a/packages/cypress-plugin/test/integration/src/verify-output.ts
+++ b/packages/cypress-plugin/test/integration/src/verify-output.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Make sure expected output is present and chalk-formatted correctly.
import {
@@ -22,8 +22,17 @@ import {
import { expect as expectExt } from "@jest/globals";
import escapeStringRegexp from "escape-string-regexp";
import { TestAttemptResult } from "@unflakable/js-api";
+import semverGte from "semver/functions/gte";
+import cypressPackage from "cypress/package.json";
const THROWN_ERROR = "\x1B[0m\x1B[31m Error\x1B[0m\x1B[90m";
+const FAIL_SYMBOL = process.platform === "win32" ? "×" : "✖";
+const PASS_SYMBOL = process.platform === "win32" ? "√" : "✓";
+
+// Cypress 13.4 changed the number of retries returned after a retry passes (i.e., when a test is
+// flaky). See:
+// https://github.com/cypress-io/cypress/blob/5a95541c3c4e48bfc67a54642abc949576fa6f05/packages/driver/patches/mocha%2B7.0.1.dev.patch
+const adjustRetriesOnPass = semverGte(cypressPackage.version, "13.4.0");
const verifySpecOutput = (
params: TestCaseParams,
@@ -119,11 +128,11 @@ const verifySpecOutputs = (
// Last retry has different output.
{ length: expectedRetries },
(_, idx) =>
- ` \x1B[31m ✖ mixed: failure should be quarantined\x1B[0m\x1B[33m (attempt ${
+ ` \x1B[31m ${FAIL_SYMBOL} 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${
+ ` \x1B[35m ${FAIL_SYMBOL} mixed: failure should be quarantined [failed, quarantined]\x1B[39m${
expectedRetries > 0
? `\x1B[33m (attempt ${expectedRetries + 1} of ${
expectedRetries + 1
@@ -132,27 +141,27 @@ const verifySpecOutputs = (
}`,
...(expectedRetries > 0
? [
- ` \x1B[31m ✖ mixed: flake should be quarantined\x1B[0m\x1B[33m (attempt 1 of ${
+ ` \x1B[31m ${FAIL_SYMBOL} 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$`
+ `^ {2}\x1B\\[35m {2}${PASS_SYMBOL}\x1B\\[39m\x1B\\[90m mixed: flake should be quarantined\x1B\\[0m\x1B\\[35m \\[flaky, quarantined]\x1B\\[39m\x1B\\[33m \\(attempt 2 of ${
+ adjustRetriesOnPass ? 2 : expectedRetries + 1
+ }\\)\x1B\\[0m\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\x1B\\[0m$`
)
),
]
: [
- " \x1B[35m ✖ mixed: flake should be quarantined [failed, quarantined]\x1B[39m",
+ ` \x1B[35m ${FAIL_SYMBOL} mixed: flake should be quarantined [failed, quarantined]\x1B[39m`,
]),
]
: [
...Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- ` \x1B[31m ✖ mixed: failure should be quarantined\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} mixed: failure should be quarantined\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -160,15 +169,15 @@ const verifySpecOutputs = (
: ""
}`
),
- ` \x1B[31m ✖ mixed: flake should be quarantined\x1B[0m\x1B[33m (attempt 1 of ${
+ ` \x1B[31m ${FAIL_SYMBOL} 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$`
+ `^ {2}\x1B\\[33m {2}${PASS_SYMBOL}\x1B\\[0m\x1B\\[90m mixed: flake should be quarantined\x1B\\[0m\x1B\\[33m \\[flaky]\x1B\\[0m\x1B\\[33m \\(attempt 2 of ${
+ adjustRetriesOnPass ? 2 : expectedRetries + 1
+ }\\)\x1B\\[0m\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\x1B\\[0m$`
)
),
]
@@ -180,7 +189,7 @@ const verifySpecOutputs = (
? Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- ` \x1B[31m ✖ mixed: should fail\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} mixed: should fail\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -198,32 +207,34 @@ const verifySpecOutputs = (
]
: expectedRetries > 0
? [
- ` \x1B[31m ✖ mixed: should be flaky\x1B[0m\x1B[33m (attempt 1 of ${
+ ` \x1B[31m ${FAIL_SYMBOL} 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$`
+ `^ {2}\x1B\\[35m {2}${PASS_SYMBOL}\x1B\\[39m\x1B\\[90m mixed: should be flaky\x1B\\[0m\x1B\\[35m \\[flaky, quarantined]\x1B\\[39m\x1B\\[33m \\(attempt 2 of ${
+ adjustRetriesOnPass ? 2 : expectedRetries + 1
+ }\\)\x1B\\[0m\x1B\\[(?:33|90)m \\([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$`
+ `^ {2}\x1B\\[33m {2}${PASS_SYMBOL}\x1B\\[0m\x1B\\[90m mixed: should be flaky\x1B\\[0m\x1B\\[33m \\[flaky]\x1B\\[0m\x1B\\[33m \\(attempt 2 of ${
+ adjustRetriesOnPass ? 2 : expectedRetries + 1
+ }\\)\x1B\\[0m\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\x1B\\[0m$`
)
),
]
- : [" \x1B[31m ✖ mixed: should be flaky\x1B[0m"]
+ : [` \x1B[31m ${FAIL_SYMBOL} 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$/
+ new RegExp(
+ `^ {2}\\x1B\\[32m {2}${PASS_SYMBOL}\\x1B\\[0m\\x1B\\[90m mixed: should pass\\x1B\\[0m\\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\\x1B\\[0m$`
+ )
),
" \x1B[36m - mixed: should be skipped\x1B[0m",
],
@@ -599,7 +610,7 @@ const verifySpecOutputs = (
? Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- ` \x1B[31m ✖ should fail\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} should fail\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -612,7 +623,7 @@ const verifySpecOutputs = (
? Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- ` \x1B[31m ✖ should fail with multiple exceptions\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} should fail with multiple exceptions\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -626,7 +637,7 @@ const verifySpecOutputs = (
? Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- ` \x1B[31m ✖ should showDiff\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} should showDiff\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -694,33 +705,33 @@ const verifySpecOutputs = (
!expectQuarantinedTestsToBeSkipped)
? expectedRetries > 0
? [
- `\x1B[31m ✖ should be flaky${expectedFlakeTestNameSuffix}\x1B[0m\x1B[33m (attempt 1 of ${
+ `\x1B[31m ${FAIL_SYMBOL} 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(
+ `^\x1B\\[35m {2}${PASS_SYMBOL}\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$`
+ adjustRetriesOnPass ? 2 : expectedRetries + 1
+ }\\)\x1B\\[0m\x1B\\[(?:33|90)m \\([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(
+ `^\x1B\\[33m {2}${PASS_SYMBOL}\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$`
+ adjustRetriesOnPass ? 2 : expectedRetries + 1
+ }\\)\x1B\\[0m\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\x1B\\[0m$`
)
),
]
: [
- `\x1B[31m ✖ should be flaky${expectedFlakeTestNameSuffix}\x1B[0m`,
+ `\x1B[31m ${FAIL_SYMBOL} should be flaky${expectedFlakeTestNameSuffix}\x1B[0m`,
]
: [
`\x1B[36m - should be flaky${expectedFlakeTestNameSuffix}\x1B[0m${
@@ -838,206 +849,231 @@ const verifySpecOutputs = (
: "fail"
: "skip";
- const hookFailTestFailure = {
- attempts: Array.from(
+ const verifyHookFailSpecOutput = (errorLines: string[]): void => {
+ 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,
+ })
+ ),
+ };
+ const hookSkipTestFailure = {
+ attempts: [
+ {
+ titlePath: ["describe block", "should be skipped"],
+ attempt: undefined,
+ errorLines: expectExt.arrayContaining([
+ ...(!skipAfterHook
+ ? [
+ '\x1B[0m\x1B[31m Error: "after all" hook: after hook with title failed:',
+ ]
+ : []),
+ `> after Error #1`,
+ ]),
+ },
+ ],
+ };
+
+ verifySpecOutput(
+ params,
+ specOutputs,
+ "hook-fail",
{
- 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
+ ...EMPTY_REPORTER_OUTPUT_MATCH,
+ suitesAndTestAttempts: [
+ "\x1B[0m describe block\x1B[0m",
+ ...(hookFailResult === "pass"
? [
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:$`
+ `^ {2}\\x1B\\[32m {2}${PASS_SYMBOL}\\x1B\\[0m\\x1B\\[90m should fail due to hook\\x1B\\[0m\\x1B\\[(?:33|90)m \\([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 ${FAIL_SYMBOL} should fail due to hook [failed, quarantined]\x1B[39m`
+ : ` \x1B[31m ${FAIL_SYMBOL} 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
new RegExp(
- `^ *> ${
- !skipBeforeHook
- ? "before"
- : !skipBeforeEachHook
- ? "beforeEach"
- : "afterEach"
- } Error #1$`
+ `^ {2}\\x1B\\[32m {2}${PASS_SYMBOL}\\x1B\\[0m\\x1B\\[90m should be skipped\\x1B\\[0m\\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\\x1B\\[0m$`
)
),
- ...(multipleHookErrors
- ? [
- expectExt.stringMatching(
- new RegExp(
- `^(?:\x1B\\[31m)?(?: {5})?(?:Error: )?${
- !skipBeforeHook
- ? "before"
- : !skipBeforeEachHook
- ? "beforeEach"
- : "afterEach"
- } Error #2 \\(and Mocha's done\\(\\) called multiple times\\)$`
- )
- ),
- ]
- : []),
+ ]
+ : hookSkipResult === "fail"
+ ? [` \x1B[31m ${FAIL_SYMBOL} should be skipped\x1B[0m`]
+ : hookSkipResult === "quarantined"
+ ? [
+ ` \x1B[35m ${FAIL_SYMBOL} should be skipped [failed, quarantined]\x1B[39m`,
]
: []),
- ]),
- })
- ),
- };
- 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`,
- ]),
+ ],
+ 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,
+ },
+ ]
+ : [],
+ },
},
- ],
- };
-
- 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:
+ {
+ ...EMPTY_RESULTS,
+ color:
+ hookFailResult !== "fail" &&
+ hookSkipResult !== "fail" &&
+ (hookSkipResult !== "skip" || quarantineHookSkip)
+ ? "pass"
+ : "fail",
+ numTests: 2,
+ numFailing:
(hookFailResult === "fail" ? 1 : 0) +
(hookSkipResult === "fail" ? 1 : 0),
- tests: [
- ...(hookFailResult === "fail" ? [hookFailTestFailure] : []),
- ...(hookSkipResult === "fail" ? [hookSkipTestFailure] : []),
- ],
- },
- quarantinedFailures: {
- count:
+ numPassing:
+ (hookFailResult === "pass" ? 1 : 0) +
+ (hookSkipResult === "pass" ? 1 : 0),
+ numQuarantined:
(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,
- }
+ numSkipped: hookSkipResult === "skip" ? 1 : 0,
+ }
+ );
+ };
+
+ verifyHookFailSpecOutput(
+ 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" hook: before hook with title'
+ : !skipBeforeEachHook
+ ? '"before each" hook: beforeEach hook with title'
+ : '"after each" hook: afterEach hook with title'
+ } 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\\)$`
+ )
+ ),
+ ]
+ : []),
+ ]
+ : []),
+ ])
);
+ if (!skipBeforeHook || !skipBeforeEachHook || !skipAfterEachHook) {
+ verifyHookFailSpecOutput(
+ // The error should contain the hook name and not the plain test name. This should catch bugs
+ // in which we fail to merge errors originating in hooks when we both onTestFail() and either
+ // onTestRetry()/onTestEnd() are called.
+ expectExt.not.arrayContaining([
+ expectExt.stringMatching("should fail due to hook failed:"),
+ ])
+ );
+ }
+
verifySpecOutput(
params,
specOutputs,
@@ -1048,7 +1084,7 @@ const verifySpecOutputs = (
? Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- `\x1B[31m ✖ An uncaught error was detected outside of a test\x1B[0m${
+ `\x1B[31m ${FAIL_SYMBOL} An uncaught error was detected outside of a test\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -1091,12 +1127,16 @@ const verifySpecOutputs = (
"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$/
+ new RegExp(
+ `^\\x1B\\[32m +${PASS_SYMBOL}\\x1B\\[0m\\x1B\\[90m should pass\\x1B\\[0m\\x1B\\[(?:33|90)m \\([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$/
+ new RegExp(
+ `^ {2}\\x1B\\[32m {2}${PASS_SYMBOL}\\x1B\\[0m\\x1B\\[90m suite test should pass\\x1B\\[0m\\x1B\\[(?:33|90)m \\([0-9]+.+?\\)\\x1B\\[0m$`
+ )
),
],
passing: 2,
@@ -1193,7 +1233,7 @@ const verifySpecOutputs = (
// Last retry has different output.
{ length: expectedRetries },
(_, idx) =>
- ` \x1B[31m ✖ should be quarantined\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} should be quarantined\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -1201,7 +1241,7 @@ const verifySpecOutputs = (
: ""
}`
),
- ` \x1B[35m ✖ should be quarantined [failed, quarantined]\x1B[39m${
+ ` \x1B[35m ${FAIL_SYMBOL} should be quarantined [failed, quarantined]\x1B[39m${
expectedRetries > 0
? `\x1B[33m (attempt ${expectedRetries + 1} of ${
expectedRetries + 1
@@ -1215,7 +1255,7 @@ const verifySpecOutputs = (
: Array.from(
{ length: expectedRetries + 1 },
(_, idx) =>
- ` \x1B[31m ✖ should be quarantined\x1B[0m${
+ ` \x1B[31m ${FAIL_SYMBOL} should be quarantined\x1B[0m${
expectedRetries > 0
? `\x1B[33m (attempt ${idx + 1} of ${
expectedRetries + 1
@@ -1331,9 +1371,9 @@ export const verifyOutput = (
).toStrictEqual(expectedSpecs);
// Make sure there are no unexpected specs.
- expect(expectedSpecs).toStrictEqual(
+ expect(
parsedOutput.specOutputs.map((spec) => spec.filename).sort()
- );
+ ).toStrictEqual(expectedSpecs);
verifySpecOutputs(params, parsedOutput.specOutputs);
}
diff --git a/packages/cypress-plugin/test/integration/tsconfig.json b/packages/cypress-plugin/test/integration/tsconfig.json
index 75be32b..e935350 100644
--- a/packages/cypress-plugin/test/integration/tsconfig.json
+++ b/packages/cypress-plugin/test/integration/tsconfig.json
@@ -7,5 +7,5 @@
// that involve lots of Node.JS processes from inside the browser.
"types": ["jest", "jest-expect-message", "node"]
},
- "include": [".eslintrc.js", "jest.config.js", "src"]
+ "include": [".eslintrc.js", "jest.config.js", "src", "unflakable.js"]
}
diff --git a/packages/cypress-plugin/test/integration/unflakable.js b/packages/cypress-plugin/test/integration/unflakable.js
new file mode 100644
index 0000000..f29e4fc
--- /dev/null
+++ b/packages/cypress-plugin/test/integration/unflakable.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+module.exports = {
+ __unstableIsFailureTestIndependent: [
+ // Cypress sometimes hangs waiting for Chrome tabs to close. See:
+ // https://github.com/cypress-io/cypress/issues/27360
+ // https://github.com/cypress-io/cypress/blob/fe54cf504aefcfa6b621a90baa57e345cfa09548/packages/server/lib/modes/run.ts#L676-L680
+ // NB: This requires DEBUG="cypress:server:run" (at a minimum).
+ /attempting to close the browser tab(?:(?!resetting server state).)*$/s,
+ /Still waiting to connect to Edge, retrying in 1 second.*(?:Error: Test timed out after|All promises were rejected)/s,
+ /There was an error reconnecting to the Chrome DevTools protocol\. Please restart the browser\./,
+ /Cypress failed to make a connection to the Chrome DevTools Protocol after retrying/,
+ // When this error occurs, Cypress ends up printing the "Running: " header multiple times,
+ // which the integration test parses as if that spec were in fact invoked multiple times. We
+ // don't want the test itself to ignore multiple spec invocations since that could indicate a
+ // bug. Instead, we treat it as a test independent failure iff this error message is in the
+ // output. Otherwise, we'll still treat it as a true failure.
+ /Timed out waiting for the browser to connect. Retrying\.\.\./,
+ /Cypress verification timed out\./,
+ ],
+};
diff --git a/packages/cypress-plugin/tsconfig.json b/packages/cypress-plugin/tsconfig.json
index 9e9a7c8..786cbe3 100644
--- a/packages/cypress-plugin/tsconfig.json
+++ b/packages/cypress-plugin/tsconfig.json
@@ -15,5 +15,5 @@
// Avoids conflicting global definitions from, e.g., jest.
"types": ["node"]
},
- "include": ["mocha.d.ts", "rollup.config.mjs"]
+ "include": ["cypress-on-fix.d.ts", "mocha.d.ts", "rollup.config.mjs"]
}
diff --git a/packages/jest-plugin/.eslintrc.js b/packages/jest-plugin/.eslintrc.js
index dcbec19..f943e67 100644
--- a/packages/jest-plugin/.eslintrc.js
+++ b/packages/jest-plugin/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
extends: ["../../.eslintrc-ts.js"],
diff --git a/packages/jest-plugin/LICENSE b/packages/jest-plugin/LICENSE
index 53bf01b..fc24ed8 100644
--- a/packages/jest-plugin/LICENSE
+++ b/packages/jest-plugin/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022-2023 Developer Innovations, LLC
+Copyright (c) 2022-2024 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
diff --git a/packages/jest-plugin/README.md b/packages/jest-plugin/README.md
index d8a7de2..38f4fd4 100644
--- a/packages/jest-plugin/README.md
+++ b/packages/jest-plugin/README.md
@@ -5,7 +5,6 @@
[](https://www.npmjs.com/package/@unflakable/jest-plugin)
-[](https://twitter.com/unflakable)
# Unflakable Plugin for Jest
diff --git a/packages/jest-plugin/jest-circus.d.ts b/packages/jest-plugin/jest-circus.d.ts
new file mode 100644
index 0000000..833746d
--- /dev/null
+++ b/packages/jest-plugin/jest-circus.d.ts
@@ -0,0 +1,41 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+// jest-circus doesn't export the types for runner. See:
+// https://github.com/jestjs/jest/blob/6d2632adae0f0fa1fe116d3b475fd9783d0de1b5/packages/jest-circus/runner.js#L10-L9
+// https://github.com/jestjs/jest/blob/6d2632adae0f0fa1fe116d3b475fd9783d0de1b5/packages/jest-runner/src/types.ts#L34
+declare module "jest-circus/runner" {
+ import { Config } from "@jest/types";
+ import { JestEnvironment } from "@jest/environment";
+ import {
+ TestResult,
+ AssertionResult,
+ SerializableError,
+ Test,
+ } from "@jest/test-result";
+
+ // Exported by newer Jest versions but not older ones prior to 26.2.0.
+ export declare type TestEvents = {
+ "test-file-start": [Test];
+ "test-file-success": [Test, TestResult];
+ "test-file-failure": [Test, SerializableError];
+ "test-case-result": [string, AssertionResult];
+ };
+
+ export declare type TestFileEvent<
+ T extends keyof TestEvents = keyof TestEvents
+ > = (eventName: T, args: TestEvents[T]) => unknown;
+
+ export declare type UnsubscribeFn = () => void;
+
+ export declare type TestFramework = (
+ globalConfig: Config.GlobalConfig,
+ config: Config.ProjectConfig,
+ environment: JestEnvironment,
+ runtime: unknown,
+ testPath: string,
+ sendMessageToJest?: TestFileEvent
+ ) => Promise;
+
+ const initialize: TestFramework;
+ export default initialize;
+}
diff --git a/packages/jest-plugin/jest.config.js b/packages/jest-plugin/jest.config.js
new file mode 100644
index 0000000..6cd3940
--- /dev/null
+++ b/packages/jest-plugin/jest.config.js
@@ -0,0 +1,18 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+module.exports = {
+ extensionsToTreatAsEsm: [".ts"],
+ roots: ["src"],
+ testEnvironment: "node",
+ testTimeout: 60000,
+ transform: {
+ "^.+\\.[tj]s$": [
+ "ts-jest",
+ {
+ tsconfig: "tsconfig.json",
+ },
+ ],
+ },
+ verbose: true,
+};
diff --git a/packages/jest-plugin/package.json b/packages/jest-plugin/package.json
index 0c25483..1174ab1 100644
--- a/packages/jest-plugin/package.json
+++ b/packages/jest-plugin/package.json
@@ -8,7 +8,7 @@
"bugs": "https://github.com/unflakable/unflakable-javascript/issues",
"homepage": "https://unflakable.com",
"license": "MIT",
- "version": "0.2.0",
+ "version": "0.3.0",
"exports": {
"./dist/reporter": {
"types": "./dist/reporter.d.ts",
@@ -17,13 +17,18 @@
"./dist/runner": {
"types": "./dist/runner.d.ts",
"default": "./dist/runner.js"
+ },
+ "./dist/test-runner": {
+ "types": "./dist/test-runner.d.ts",
+ "default": "./dist/test-runner.js"
}
},
"files": [
"README.md",
"dist/**/*.js",
"dist/reporter.d.ts",
- "dist/runner.d.ts"
+ "dist/runner.d.ts",
+ "dist/test-runner.d.ts"
],
"dependencies": {
"@unflakable/js-api": "workspace:^",
@@ -32,6 +37,7 @@
"debug": "^4.3.3",
"deep-equal": "^2.0.5",
"escape-string-regexp": "^4.0.0",
+ "semver": "^7.5.4",
"simple-git": "^3.16.0"
},
"devDependencies": {
@@ -50,19 +56,22 @@
"@types/jest": "25.1.0 - 29",
"@unflakable/plugins-common": "workspace:^",
"exit": "^0.1.2",
+ "jest": "25.1.0 - 29",
+ "jest-circus": "25.1.0 - 29",
+ "jest-environment-node": "25.1.0 - 29",
"jest-runner": "25.1.0 - 29",
"jest-util": "25.1.0 - 29",
+ "rimraf": "^5.0.1",
"rollup": "^3.21.1",
"typescript": "^4.9.5"
},
"peerDependencies": {
- "@jest/console": "25.1.0 - 29",
- "@jest/reporters": "25.1.0 - 29",
- "jest-runner": "25.1.0 - 29",
- "jest-util": "25.1.0 - 29"
+ "jest": "25.1.0 - 29"
},
"scripts": {
- "build": "tsc --noEmit && rollup --config",
- "build:watch": "rollup --config --watch"
+ "build": "yarn clean && tsc --noEmit && rollup --config",
+ "build:watch": "rollup --config --watch",
+ "clean": "rimraf dist/",
+ "test": "jest --useStderr --verbose"
}
}
diff --git a/packages/jest-plugin/rollup.config.mjs b/packages/jest-plugin/rollup.config.mjs
index d39da52..c57bf72 100644
--- a/packages/jest-plugin/rollup.config.mjs
+++ b/packages/jest-plugin/rollup.config.mjs
@@ -1,41 +1,76 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
-import pluginTypescript from "@rollup/plugin-typescript";
-import pluginNodeResolve from "@rollup/plugin-node-resolve";
+import path from "path";
import pluginCommonJs from "@rollup/plugin-commonjs";
+import pluginDts from "rollup-plugin-dts";
import pluginJson from "@rollup/plugin-json";
+import pluginNodeResolve from "@rollup/plugin-node-resolve";
+import pluginTypescript from "@rollup/plugin-typescript";
+
+/**
+ * Bundle the internal @unflakable/plugins-common package, but leave most other imported packages
+ * as external. Internal modules begin with `.` or `/`.
+ *
+ * @type {import("rollup").IsExternal}
+ */
+const isExternal = (id) =>
+ !id.startsWith(".") &&
+ !path.isAbsolute(id) &&
+ !id.startsWith("src/") &&
+ !id.startsWith("@unflakable/plugins-common/") &&
+ ![
+ // Support older versions of Jest that don't support sub-path externals in package.json.
+ "@unflakable/js-api/consts",
+ "@unflakable/plugins-common",
+ ].includes(id);
/**
* @type {import("rollup").NormalizedInputOptions}
*/
-export default {
- input: ["src/reporter.ts", "src/runner.ts"],
- output: {
- dir: "dist",
- format: "cjs",
- // Mimicks TypeScript `esModuleInterop` (see
- // https://rollupjs.org/configuration-options/#output-format).
- interop: "auto",
- sourcemap: true,
+export default [
+ {
+ input: ["src/reporter.ts", "src/runner.ts", "src/test-runner.ts"],
+ output: {
+ dir: "dist",
+ format: "cjs",
+ // Mimicks TypeScript `esModuleInterop` (see
+ // https://rollupjs.org/configuration-options/#output-format).
+ interop: "auto",
+ sourcemap: true,
+ },
+ // Bundle the internal @unflakable/plugins-common package in dist/, but leave most other
+ // imported packages as an external. Internal modules begin with `.` or `/`.
+ external: isExternal,
+ // Unclear why this is necessary.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ plugins: [
+ pluginCommonJs(),
+ pluginJson(),
+ pluginNodeResolve(),
+ pluginTypescript(),
+ ],
+ },
+ // Rollup types so that UnflakableConfig from @unflakable/plugins-common is bundled. This package
+ // doesn't get published separately, so our public types shouldn't reference it.
+ {
+ input: [
+ // NB: This should include every exported .d.ts from package.json.
+ "dist/reporter.d.ts",
+ "dist/runner.d.ts",
+ "dist/test-runner.d.ts",
+ ],
+ output: {
+ dir: ".",
+ entryFileNames: "[name].d.ts",
+ format: "cjs",
+ },
+ external: isExternal,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ plugins: [
+ pluginNodeResolve({ preferBuiltins: true }),
+ pluginDts({
+ respectExternal: true,
+ }),
+ ],
},
- // Bundle the internal @unflakable/plugins-common package in dist/, but leave most other
- // imported packages as an external. Internal modules begin with `.` or `/`.
- external: (id) =>
- !id.startsWith(".") &&
- !id.startsWith("/") &&
- !id.startsWith("src/") &&
- !id.startsWith("@unflakable/plugins-common/") &&
- ![
- // Support older versions of Jest that don't support sub-path externals in package.json.
- "@unflakable/js-api/consts",
- "@unflakable/plugins-common",
- ].includes(id),
- // Unclear why this is necessary.
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- plugins: [
- pluginCommonJs(),
- pluginJson(),
- pluginNodeResolve(),
- pluginTypescript(),
- ],
-};
+];
diff --git a/packages/jest-plugin/src/config.test.ts b/packages/jest-plugin/src/config.test.ts
new file mode 100644
index 0000000..b105138
--- /dev/null
+++ b/packages/jest-plugin/src/config.test.ts
@@ -0,0 +1,130 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import { loadConfig } from "./config";
+import { cosmiconfigSync, Options } from "cosmiconfig";
+import {
+ setCosmiconfigSync,
+ UnflakableConfigFile,
+} from "@unflakable/plugins-common";
+import { IsFailureTestIndependentFn } from "./types";
+
+const MOCK_SUITE_ID = "MOCK_SUITE_ID";
+const SEARCH_FROM = ".";
+
+const throwUnimplemented = (): never => {
+ throw new Error("unimplemented");
+};
+
+const setMockConfig = (
+ config: Partial<
+ UnflakableConfigFile & {
+ __unstableIsFailureTestIndependent:
+ | string
+ | RegExp
+ | (string | RegExp)[]
+ | IsFailureTestIndependentFn;
+ }
+ >
+): void => {
+ setCosmiconfigSync(
+ (
+ 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(SEARCH_FROM);
+ return {
+ config,
+ filepath: "unflakable.js",
+ isEmpty: false,
+ };
+ },
+ };
+ }
+ );
+};
+
+describe("__unstableIsFailureTestIndependent", () => {
+ it("should default to undefined", () => {
+ setMockConfig({ testSuiteId: MOCK_SUITE_ID });
+ const config = loadConfig(SEARCH_FROM);
+ expect(config.testSuiteId).toBe(MOCK_SUITE_ID);
+ expect(config.isFailureTestIndependent).toBeUndefined();
+ });
+
+ it("should accept a string regex", () => {
+ setMockConfig({
+ testSuiteId: MOCK_SUITE_ID,
+ __unstableIsFailureTestIndependent: ".*",
+ });
+ const config = loadConfig(SEARCH_FROM);
+ expect(config.testSuiteId).toBe(MOCK_SUITE_ID);
+ expect(Array.isArray(config.isFailureTestIndependent)).toBe(true);
+ expect(config.isFailureTestIndependent).toHaveLength(1);
+ expect((config.isFailureTestIndependent as RegExp[])[0]).toBeInstanceOf(
+ RegExp
+ );
+ expect((config.isFailureTestIndependent as RegExp[])[0].source).toBe(".*");
+ expect((config.isFailureTestIndependent as RegExp[])[0].flags).toBe("");
+ });
+
+ it("should accept a RegExp object", () => {
+ setMockConfig({
+ testSuiteId: MOCK_SUITE_ID,
+ __unstableIsFailureTestIndependent: /.*/gs,
+ });
+ const config = loadConfig(SEARCH_FROM);
+ expect(config.testSuiteId).toBe(MOCK_SUITE_ID);
+ expect(Array.isArray(config.isFailureTestIndependent)).toBe(true);
+ expect(config.isFailureTestIndependent).toHaveLength(1);
+ expect((config.isFailureTestIndependent as RegExp[])[0]).toBeInstanceOf(
+ RegExp
+ );
+ expect((config.isFailureTestIndependent as RegExp[])[0].source).toBe(".*");
+ expect((config.isFailureTestIndependent as RegExp[])[0].flags).toBe("gs");
+ });
+
+ it("should accept an array of strings/RegExps object", () => {
+ setMockConfig({
+ testSuiteId: MOCK_SUITE_ID,
+ __unstableIsFailureTestIndependent: [/foo/s, /bar/g, "baz", ".*"],
+ });
+ const config = loadConfig(SEARCH_FROM);
+ expect(config.testSuiteId).toBe(MOCK_SUITE_ID);
+ expect(Array.isArray(config.isFailureTestIndependent)).toBe(true);
+ expect(config.isFailureTestIndependent).toHaveLength(4);
+ expect((config.isFailureTestIndependent as RegExp[])[0]).toBeInstanceOf(
+ RegExp
+ );
+ expect((config.isFailureTestIndependent as RegExp[])[0].source).toBe("foo");
+ expect((config.isFailureTestIndependent as RegExp[])[0].flags).toBe("s");
+ expect((config.isFailureTestIndependent as RegExp[])[1]).toBeInstanceOf(
+ RegExp
+ );
+ expect((config.isFailureTestIndependent as RegExp[])[1].source).toBe("bar");
+ expect((config.isFailureTestIndependent as RegExp[])[1].flags).toBe("g");
+ expect((config.isFailureTestIndependent as RegExp[])[2]).toBeInstanceOf(
+ RegExp
+ );
+ expect((config.isFailureTestIndependent as RegExp[])[2].source).toBe("baz");
+ expect((config.isFailureTestIndependent as RegExp[])[2].flags).toBe("");
+ expect((config.isFailureTestIndependent as RegExp[])[3]).toBeInstanceOf(
+ RegExp
+ );
+ expect((config.isFailureTestIndependent as RegExp[])[3].source).toBe(".*");
+ expect((config.isFailureTestIndependent as RegExp[])[3].flags).toBe("");
+ });
+});
diff --git a/packages/jest-plugin/src/config.ts b/packages/jest-plugin/src/config.ts
new file mode 100644
index 0000000..97159dc
--- /dev/null
+++ b/packages/jest-plugin/src/config.ts
@@ -0,0 +1,92 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import {
+ IsFailureTestIndependentFn,
+ UnflakableJestConfig,
+ UnflakableJestConfigInner,
+} from "./types";
+import { loadConfigSync } from "@unflakable/plugins-common";
+import util from "util";
+import semverLt from "semver/functions/lt";
+import jestPackage from "jest/package.json";
+
+const parseIsFailureTestIndependent = (
+ isFailureTestIndependent: unknown,
+ filepath: string
+): RegExp[] | IsFailureTestIndependentFn => {
+ if (typeof isFailureTestIndependent === "function") {
+ return isFailureTestIndependent as IsFailureTestIndependentFn;
+ } else if (Array.isArray(isFailureTestIndependent)) {
+ return isFailureTestIndependent.map((entry) => {
+ if (typeof entry === "string") {
+ try {
+ return new RegExp(entry);
+ } catch (e: unknown) {
+ throw new Error(
+ `Invalid \`__unstableIsFailureTestIndependent\` regex \`${util.format(
+ entry
+ )}\` found in ${filepath}: ${util.inspect(e)}`
+ );
+ }
+ } else if (entry instanceof RegExp) {
+ return entry;
+ } else {
+ throw new Error(
+ `Unexpected \`__unstableIsFailureTestIndependent\` value \`${util.format(
+ entry
+ )}\` found in ${filepath}`
+ );
+ }
+ });
+ } else if (typeof isFailureTestIndependent === "string") {
+ try {
+ return [new RegExp(isFailureTestIndependent)];
+ } catch (e: unknown) {
+ throw new Error(
+ `Invalid \`__unstableIsFailureTestIndependent\` regex \`${util.format(
+ isFailureTestIndependent
+ )}\` found in ${filepath}: ${util.inspect(e)}`
+ );
+ }
+ } else if (isFailureTestIndependent instanceof RegExp) {
+ return [isFailureTestIndependent];
+ } else {
+ throw new Error(
+ `Unexpected \`__unstableIsFailureTestIndependent\` value \`${util.format(
+ isFailureTestIndependent
+ )}\` found in ${filepath}`
+ );
+ }
+};
+
+export const loadConfig = (searchFrom: string): UnflakableJestConfig =>
+ loadConfigSync(
+ searchFrom,
+ (configResult): [UnflakableJestConfigInner, string[]] => {
+ const config =
+ configResult !== null && typeof configResult.config === "object"
+ ? (configResult.config as { [s: string]: unknown })
+ : null;
+ if (
+ configResult !== null &&
+ config?.__unstableIsFailureTestIndependent !== undefined
+ ) {
+ if (semverLt(jestPackage.version, "28.0.0")) {
+ throw new Error(
+ "__unstableIsFailureTestIndependent requires Jest version 28+"
+ );
+ }
+ return [
+ {
+ isFailureTestIndependent: parseIsFailureTestIndependent(
+ config.__unstableIsFailureTestIndependent,
+ configResult.filepath
+ ),
+ },
+ ["__unstableIsFailureTestIndependent"],
+ ];
+ } else {
+ return [{}, []];
+ }
+ }
+ );
diff --git a/packages/jest-plugin/src/index.ts b/packages/jest-plugin/src/index.ts
index 0c72377..be0a8c0 100644
--- a/packages/jest-plugin/src/index.ts
+++ b/packages/jest-plugin/src/index.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import reporter from "./reporter";
import runner from "./runner";
diff --git a/packages/jest-plugin/src/reporter.ts b/packages/jest-plugin/src/reporter.ts
index bff6a34..6c1fe51 100644
--- a/packages/jest-plugin/src/reporter.ts
+++ b/packages/jest-plugin/src/reporter.ts
@@ -1,11 +1,10 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import * as path from "path";
import type {
AggregatedResult,
AssertionResult,
Status,
- TestResult,
} from "@jest/test-result";
import {
BaseReporter,
@@ -14,7 +13,7 @@ import {
Test,
VerboseReporter,
} from "@jest/reporters";
-import { FAILED, groupBy, testKey, USER_AGENT } from "./utils";
+import { groupBy, testKey, USER_AGENT } from "./utils";
import {
TestAttemptResult,
TestRunRecord,
@@ -23,8 +22,11 @@ import {
} from "@unflakable/js-api";
import {
UnflakableAggregatedResult,
+ UnflakableAggregatedResultWithCounts,
UnflakableAssertionResult,
+ UnflakableJestConfig,
UnflakableTestResult,
+ UnflakableTestResultWithCounts,
} from "./types";
import { specialChars } from "jest-util";
import type { Config } from "@jest/types";
@@ -38,10 +40,14 @@ import {
autoDetectGit,
branchOverride,
commitOverride,
+ getRepoRoot,
loadApiKey,
- loadConfigSync,
- UnflakableConfig,
+ loadGitRepo,
+ toPosix,
+ UnflakableConfigEnabled,
} from "@unflakable/plugins-common";
+import { loadConfig } from "./config";
+import { addResult, makeEmptyAggregatedTestResult } from "@jest/test-result";
const debug = _debug("unflakable:reporter");
@@ -93,123 +99,184 @@ const getIcon = (test: UnflakableAssertionResult): string => {
}
};
-// Removes stats attributed to retries (which shouldn't affect the overall stats) and counts flaky
-// tests.
-const processedResults = (
- aggregatedResults: AggregatedResult
-): UnflakableAggregatedResult => {
+// Recomputes the aggregated stats after taking into account retries, flakes, quarantine, and
+// test-independent failures. This function returns new objects and does NOT modify its input.
+const computeResultsForReporter = (
+ origAggregatedResults: UnflakableAggregatedResult
+): UnflakableAggregatedResultWithCounts => {
const resultsByFilenameAndName = Object.fromEntries(
Object.entries(
groupBy(
- aggregatedResults.testResults,
+ origAggregatedResults.testResults,
(testFileResult: UnflakableTestResult) => testFileResult.testFilePath
)
).map(([testFilePath, testFileResults]) => [
testFilePath,
groupBy(
testFileResults.flatMap((testFileResult) => testFileResult.testResults),
- (assertionResult) => JSON.stringify(testKey(assertionResult))
+ (assertionResult) => JSON.stringify(testKey(assertionResult, false))
),
])
);
- return aggregatedResults.testResults.reduce(
- (filteredResults, testResult: UnflakableTestResult) => {
- if ((testResult._unflakableAttempt ?? 0) > 0) {
- return {
- ...filteredResults,
- numFailedTestSuites:
- testResult.numFailingTests > 0 ||
- testResult.testExecError !== undefined
- ? filteredResults.numFailedTestSuites - 1
- : filteredResults.numFailedTestSuites,
- numPassedTestSuites:
- !testResult.skipped &&
- !(
- testResult.numFailingTests > 0 ||
- testResult.testExecError !== undefined
- )
- ? filteredResults.numPassedTestSuites - 1
- : filteredResults.numPassedTestSuites,
- numPendingTestSuites: testResult.skipped
- ? filteredResults.numPendingTestSuites - 1
- : filteredResults.numPendingTestSuites,
- snapshot: {
- ...filteredResults.snapshot,
- matched:
- filteredResults.snapshot.matched - testResult.snapshot.matched,
- total:
- filteredResults.snapshot.total -
- testResult.snapshot.added -
- testResult.snapshot.matched -
- testResult.snapshot.unmatched -
- testResult.snapshot.updated,
- // When we retry failed tests, Jest incorrectly counts named snapshots as obsolete.
- unchecked:
- filteredResults.snapshot.unchecked -
- testResult.snapshot.unchecked,
- uncheckedKeysByFile: filteredResults.snapshot.uncheckedKeysByFile
- .map((uncheckedSnapshot) => {
- uncheckedSnapshot.keys = uncheckedSnapshot.keys.filter((key) =>
- testResult.snapshot.uncheckedKeys.includes(key)
- );
- return uncheckedSnapshot;
- })
- .filter((uncheckedSnapshot) => uncheckedSnapshot.keys.length > 0),
- unmatched:
- filteredResults.snapshot.unmatched -
- testResult.snapshot.unmatched,
- filesUnmatched:
- filteredResults.snapshot.filesUnmatched -
- (testResult.snapshot.unmatched > 0 ? 1 : 0),
- },
- };
- } else {
+ // Only includes the first attempt of each test file, but the stats take into account subsequent
+ // attempts to determine flakiness and test-independence.
+ const updatedTestResults: UnflakableTestResultWithCounts[] =
+ origAggregatedResults.testResults
+ .filter((testResult) => (testResult._unflakableAttempt ?? 0) === 0)
+ .map((testResult): UnflakableTestResultWithCounts => {
const attemptsByTestName =
resultsByFilenameAndName[testResult.testFilePath];
- const numFlakyTests = testResult.testResults.reduce(
- (numFlakyTests, assertionResult) => {
+
+ const {
+ numFailingTests,
+ numFlakyTests,
+ numPassingTests,
+ numPassingTestsWithIndependentFailures,
+ numQuarantinedTests,
+ } = testResult.testResults.reduce(
+ (
+ {
+ numFailingTests,
+ numFlakyTests,
+ numPassingTests,
+ numPassingTestsWithIndependentFailures,
+ numQuarantinedTests,
+ },
+ assertionResult
+ ) => {
const attempts =
- attemptsByTestName[JSON.stringify(testKey(assertionResult))];
- return attempts.some((attempt) => attempt.status === "passed") &&
+ attemptsByTestName[
+ JSON.stringify(testKey(assertionResult, false))
+ ];
+ const isPassing =
+ attempts.some((attempt) => attempt.status === "passed") &&
+ attempts.every(
+ (attempt) =>
+ attempt.status === "passed" ||
+ attempt.status === "pending" ||
+ attempt._unflakableIsFailureTestIndependent === true
+ );
+ const isPassingWithTestIndependentFailures =
+ isPassing &&
+ attempts.some(
+ (attempt) =>
+ attempt.status === "failed" &&
+ attempt._unflakableIsFailureTestIndependent === true
+ );
+ const isQuarantined =
+ !isPassing &&
attempts.some(
- (attempt: UnflakableAssertionResult) =>
+ (attempt) =>
attempt.status === "failed" &&
+ attempt._unflakableIsQuarantined === true
+ );
+ const isFlaky =
+ !isQuarantined &&
+ attempts.some((attempt) => attempt.status === "passed") &&
+ attempts.some(
+ (attempt) =>
+ attempt.status === "failed" &&
+ attempt._unflakableIsFailureTestIndependent !== true &&
attempt._unflakableIsQuarantined !== true
- )
- ? numFlakyTests + 1
- : numFlakyTests;
+ );
+ const isFailing =
+ !isQuarantined &&
+ attempts.some((attempt) => attempt.status === "failed") &&
+ attempts.every(
+ (attempt) =>
+ attempt.status === "failed" || attempt.status === "pending"
+ );
+ return {
+ numFailingTests: numFailingTests + (isFailing ? 1 : 0),
+ numFlakyTests: numFlakyTests + (isFlaky ? 1 : 0),
+ numPassingTests: numPassingTests + (isPassing ? 1 : 0),
+ numPassingTestsWithIndependentFailures:
+ numPassingTestsWithIndependentFailures +
+ (isPassingWithTestIndependentFailures ? 1 : 0),
+ numQuarantinedTests:
+ numQuarantinedTests + (isQuarantined ? 1 : 0),
+ };
},
- 0
+ {
+ numFailingTests: 0,
+ numFlakyTests: 0,
+ numPassingTests: 0,
+ numPassingTestsWithIndependentFailures: 0,
+ numQuarantinedTests: 0,
+ }
);
+
return {
- ...filteredResults,
- testResults: [
- ...filteredResults.testResults,
- {
- ...testResult,
- numFailingTests: Math.max(
- testResult.numFailingTests - numFlakyTests,
- 0
- ),
- _unflakableNumFlakyTests: numFlakyTests,
- },
- ],
+ ...testResult,
+ numFailingTests,
+ numPassingTests,
+ _unflakableNumFlakyTests: numFlakyTests,
+ _unflakableNumQuarantinedTests: numQuarantinedTests,
+ _unflakableNumPassingTestsWithIndependentFailures:
+ numPassingTestsWithIndependentFailures,
};
+ });
+
+ const emptyAggregatedResults: UnflakableAggregatedResultWithCounts = {
+ ...makeEmptyAggregatedTestResult(),
+
+ // Jest sets these fields separately in its TestScheduler:
+ // https://github.com/jestjs/jest/blob/7cf50065ace0f0fffeb695a7980e404a17d3b761/packages/jest-core/src/TestScheduler.ts#L429
+ numTotalTestSuites: origAggregatedResults.numTotalTestSuites,
+ startTime: origAggregatedResults.startTime,
+ success: origAggregatedResults.success,
+
+ testResults: updatedTestResults,
+ _unflakableNumFlakyTests: 0,
+ _unflakableNumQuarantinedTests: 0,
+ _unflakableNumQuarantinedSuites: 0,
+ _unflakableNumPassedTestsWithIndependentFailures: 0,
+ _unflakableNumPassedTestSuitesWithIndependentFailures: 0,
+ };
+
+ return updatedTestResults.reduce((aggregatedResults, testResult) => {
+ const prevNumPassedTestSuites = aggregatedResults.numPassedTestSuites;
+ addResult(aggregatedResults, testResult);
+
+ aggregatedResults.numTotalTests +=
+ testResult._unflakableNumFlakyTests +
+ testResult._unflakableNumQuarantinedTests;
+
+ aggregatedResults._unflakableNumFlakyTests +=
+ testResult._unflakableNumFlakyTests;
+ aggregatedResults._unflakableNumQuarantinedTests +=
+ testResult._unflakableNumQuarantinedTests;
+
+ aggregatedResults._unflakableNumPassedTestsWithIndependentFailures +=
+ testResult._unflakableNumPassingTestsWithIndependentFailures;
+
+ if (aggregatedResults.numPassedTestSuites > prevNumPassedTestSuites) {
+ // Handle edge cases that onResult() considers a suite pass but that should be failed or
+ // quarantined:
+ // https://github.com/jestjs/jest/blob/6d2632adae0f0fa1fe116d3b475fd9783d0de1b5/packages/jest-test-result/src/helpers.ts#L110
+ if (testResult._unflakableNumFlakyTests > 0) {
+ aggregatedResults.numFailedTestSuites += 1;
+ aggregatedResults.numPassedTestSuites -= 1;
+ } else if (testResult._unflakableNumQuarantinedTests > 0) {
+ aggregatedResults._unflakableNumQuarantinedSuites += 1;
+ aggregatedResults.numPassedTestSuites -= 1;
+ } else if (
+ testResult._unflakableNumPassingTestsWithIndependentFailures > 0
+ ) {
+ aggregatedResults._unflakableNumPassedTestSuitesWithIndependentFailures++;
}
- },
- {
- ...aggregatedResults,
- testResults: [] as UnflakableTestResult[],
}
- );
+
+ return aggregatedResults;
+ }, emptyAggregatedResults);
};
export default class UnflakableReporter extends BaseReporter {
private readonly apiKey: string;
- private readonly unflakableConfig: UnflakableConfig;
+ private readonly unflakableConfig: UnflakableJestConfig;
- private readonly cwd: string;
+ private readonly rootDir: string;
private readonly defaultReporter: DefaultReporter & {
// Not defined in Jest < 26.2.
onTestCaseResult?: (test: Test, testCaseResult: AssertionResult) => void;
@@ -217,9 +284,10 @@ export default class UnflakableReporter extends BaseReporter {
private readonly summaryReporter: SummaryReporter;
constructor(globalConfig: Config.GlobalConfig) {
+ debug("constructor");
super();
- this.cwd = process.cwd();
- this.unflakableConfig = loadConfigSync(globalConfig.rootDir);
+ this.rootDir = globalConfig.rootDir;
+ this.unflakableConfig = loadConfig(globalConfig.rootDir);
this.apiKey = this.unflakableConfig.enabled ? loadApiKey() : "";
if (globalConfig.verbose === true) {
@@ -242,13 +310,14 @@ export default class UnflakableReporter extends BaseReporter {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: monkey patch to avoid full re-implementation of VerboseReporter.
verboseReporter._logTest = (
- test: UnflakableAssertionResult,
+ assertionResult: UnflakableAssertionResult,
indentLevel: number
): void => {
- const status = getIcon(test);
- const duration = test.duration ?? 0;
+ const status = getIcon(assertionResult);
+ const duration = assertionResult.duration ?? 0;
const time =
duration > 0 ? ` (${formatTime(Math.round(duration))})` : "";
+
// prettier-ignore
(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -261,8 +330,11 @@ export default class UnflakableReporter extends BaseReporter {
status +
" " +
chalk.dim(
- test.title +
- (test._unflakableIsQuarantined === true
+ assertionResult.title +
+ (assertionResult._unflakableIsFailureTestIndependent === true
+ ? chalk.red(" [test independent]")
+ : "") +
+ (assertionResult._unflakableIsQuarantined === true
? chalk.yellow(" [quarantined]")
: "") +
time
@@ -278,7 +350,7 @@ export default class UnflakableReporter extends BaseReporter {
this.defaultReporter.printTestFileHeader = (
_testPath: unknown,
config: Config.ProjectConfig,
- result: UnflakableTestResult
+ result: UnflakableTestResultWithCounts
): void => {
const resultHeader = getResultHeader(result, globalConfig, config);
@@ -342,50 +414,77 @@ export default class UnflakableReporter extends BaseReporter {
this.defaultReporter.onTestStart(test);
}
- onTestCaseResult(test: Test, testCaseResult: AssertionResult): void {
- debug("onTestCaseResult");
+ onTestCaseResult(
+ test: Test,
+ assertionResult: UnflakableAssertionResult
+ ): void {
+ debug(
+ `onTestCaseResult path=\`${test.path}\` title=%o status=%o`,
+ [...assertionResult.ancestorTitles, assertionResult.title],
+ assertionResult.status
+ );
+
// Not defined in Jest < 26.2.
if (this.defaultReporter.onTestCaseResult !== undefined) {
- this.defaultReporter.onTestCaseResult(test, testCaseResult);
+ this.defaultReporter.onTestCaseResult(test, assertionResult);
}
}
+ // NB: This is called once per test *file* attempt, but the aggregatedResults include every test
+ // attempt so far, including those from other files.
onTestResult(
test: Test,
- testResult: TestResult,
- aggregatedResult: AggregatedResult
+ testResult: UnflakableTestResult,
+ aggregatedResults: UnflakableAggregatedResult
): void {
- debug("onTestResult");
+ debug(`onTestResult path=\`${test.path}\``);
+
+ const testResultForReporter: UnflakableTestResultWithCounts = {
+ ...testResult,
+ // Undo the sanitization of quarantined tests that the runner performs in order to keep
+ // Jest from exiting with non-zero status if all the failed tests are quarantined. This needs
+ // to be non-zero if there are any quarantined tests so that
+ // DefaultReporter.printTestFileHeader() prints FAIL for this file (preceded by QUARANTINED
+ // if all of the failures are quarantined).
+ numFailingTests: testResult.testResults.filter(
+ (assertionResult) => assertionResult.status === "failed"
+ ).length,
+ // We don't know when tests are flaky until the end.
+ _unflakableNumFlakyTests: 0,
+ _unflakableNumPassingTestsWithIndependentFailures: 0,
+ _unflakableNumQuarantinedTests: testResult.testResults.filter(
+ (attempt) =>
+ attempt.status === "failed" &&
+ attempt._unflakableIsQuarantined === true
+ ).length,
+ snapshot: {
+ ...testResult.snapshot,
+ // When we retry failed tests, Jest incorrectly counts named snapshots as obsolete. Filter
+ // out obsolete tests during retries.
+ unchecked:
+ (testResult._unflakableAttempt ?? 0) > 0
+ ? 0
+ : testResult.snapshot.unchecked,
+ uncheckedKeys:
+ (testResult._unflakableAttempt ?? 0) > 0
+ ? []
+ : testResult.snapshot.uncheckedKeys,
+ },
+ };
+
this.defaultReporter.onTestResult(
test,
- {
- ...testResult,
- // Undo the sanitization of quarantined tests that the runner performs in order to keep
- // Jest from exiting with non-zero status if all the failed tests are quarantined.
- numFailingTests: testResult.testResults.filter(
- (assertionResult) => assertionResult.status === FAILED
- ).length,
- snapshot: {
- ...testResult.snapshot,
- // When we retry failed tests, Jest incorrectly counts named snapshots as obsolete. Filter
- // out obsolete tests during retries.
- unchecked:
- ((testResult as UnflakableTestResult)._unflakableAttempt ?? 0) > 0
- ? 0
- : testResult.snapshot.unchecked,
- uncheckedKeys:
- ((testResult as UnflakableTestResult)._unflakableAttempt ?? 0) > 0
- ? []
- : testResult.snapshot.uncheckedKeys,
- },
- },
- processedResults(aggregatedResult)
+ testResultForReporter,
+ computeResultsForReporter(aggregatedResults)
);
}
+ // NB: This is called only once, after UnflakableRunner.runTests() returns (i.e., after all
+ // retries have been exhausted or all tests passed). The aggregatedResults include every test
+ // attempt.
async onRunComplete(
contexts: Set | undefined,
- aggregatedResults: AggregatedResult
+ aggregatedResults: UnflakableAggregatedResult
): Promise {
debug("onRunComplete");
this.defaultReporter.onRunComplete();
@@ -393,7 +492,7 @@ export default class UnflakableReporter extends BaseReporter {
// Don't double-count tests that were retried.
this.summaryReporter.onRunComplete(
contexts,
- processedResults(aggregatedResults)
+ computeResultsForReporter(aggregatedResults)
);
if (this.unflakableConfig.enabled && this.unflakableConfig.uploadResults) {
@@ -409,9 +508,13 @@ export default class UnflakableReporter extends BaseReporter {
private async uploadResults(
aggregatedResults: AggregatedResult,
- unflakableConfig: UnflakableConfig
+ unflakableConfig: UnflakableConfigEnabled
): Promise {
const testSuiteId = unflakableConfig.testSuiteId;
+
+ const git = unflakableConfig.gitAutoDetect ? await loadGitRepo() : null;
+ const repoRoot = git !== null ? await getRepoRoot(git) : this.rootDir;
+
const results = Object.entries(
groupBy(
aggregatedResults.testResults,
@@ -423,13 +526,13 @@ export default class UnflakableReporter extends BaseReporter {
testFileResults.flatMap(
(testFileResult) => testFileResult.testResults
),
- (assertionResult) => JSON.stringify(testKey(assertionResult))
+ (assertionResult) => JSON.stringify(testKey(assertionResult, true))
)
)
.map(
([, assertionResults]): TestRunRecord => ({
- filename: path.relative(this.cwd, testFilePath),
- name: testKey(assertionResults[0]),
+ filename: toPosix(path.relative(repoRoot, testFilePath)),
+ name: testKey(assertionResults[0], true),
attempts: assertionResults
.map((testResult: UnflakableAssertionResult) => ({
testResult,
@@ -446,6 +549,12 @@ export default class UnflakableReporter extends BaseReporter {
? Math.floor(testResult.duration)
: undefined,
result: result as NonNullable,
+ ...((result === "fail" || result === "quarantined") &&
+ testResult._unflakableIsFailureTestIndependent === true
+ ? {
+ failure_reason: "independent",
+ }
+ : {}),
})),
})
)
@@ -462,13 +571,14 @@ export default class UnflakableReporter extends BaseReporter {
commit = commitOverride.value;
if (
- unflakableConfig.gitAutoDetect &&
+ git !== null &&
(branch === undefined ||
branch.length === 0 ||
commit === undefined ||
commit.length === 0)
) {
const { branch: gitBranch, commit: gitCommit } = await autoDetectGit(
+ git,
this.log.bind(this)
);
diff --git a/packages/jest-plugin/src/runner.ts b/packages/jest-plugin/src/runner.ts
index ecc7abd..17dbcda 100644
--- a/packages/jest-plugin/src/runner.ts
+++ b/packages/jest-plugin/src/runner.ts
@@ -1,11 +1,7 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import * as path from "path";
-import type {
- AssertionResult,
- SerializableError,
- TestResult,
-} from "@jest/test-result";
+import type { SerializableError, TestResult } from "@jest/test-result";
import { FAILED, groupBy, testKey, USER_AGENT } from "./utils";
import TestRunner, {
OnTestFailure,
@@ -20,121 +16,55 @@ import {
TEST_NAME_ENTRY_MAX_LENGTH,
TestSuiteManifest,
} from "@unflakable/js-api";
-import { UnflakableAssertionResult, UnflakableTestResult } from "./types";
+import {
+ UnflakableAssertionResult,
+ UnflakableJestConfig,
+ UnflakableTestResult,
+} from "./types";
import type { Config } from "@jest/types";
import chalk from "chalk";
import escapeStringRegexp from "escape-string-regexp";
import { debug as _debug } from "debug";
import {
+ getRepoRoot,
getTestSuiteManifest,
isTestQuarantined,
loadApiKey,
- loadConfigSync,
- QuarantineMode,
- UnflakableConfig,
+ loadGitRepo,
+ toPosix,
} from "@unflakable/plugins-common";
+import { TestEvents, UnsubscribeFn } from "jest-circus/runner";
+import { loadConfig } from "./config";
+import util from "util";
const debug = _debug("unflakable:runner");
-type TestFailure = { test: Test; testResult: TestResult };
-
-const wrapOnResult =
- ({
- attempt,
- cwd,
- manifest,
- onResult,
- quarantineMode,
- testFailures,
- }: {
- attempt: number;
- cwd: string;
- manifest: TestSuiteManifest | undefined;
- onResult: OnTestSuccess;
- quarantineMode: QuarantineMode;
- testFailures: TestFailure[];
- }) =>
- async (test: Test, testResult: TestResult): Promise => {
- const testResults = testResult.testResults.map(
- (assertionResult: AssertionResult): UnflakableAssertionResult => {
- const testFilename = path.relative(cwd, test.path);
- if (assertionResult.status === FAILED) {
- if (manifest === undefined) {
- debug(
- "Not quarantining test failure due to failure to fetch manifest"
- );
- } else if (quarantineMode === "no_quarantine") {
- debug(
- "Not quarantining test failure because quarantineMode is set to `no_quarantine`"
- );
- } else if (
- isTestQuarantined(manifest, testFilename, testKey(assertionResult))
- ) {
- debug(
- `Quarantining failed test ${JSON.stringify(
- testKey(assertionResult)
- )} from file ${testFilename}`
- );
- return {
- ...assertionResult,
- // Use a separate field instead of adding a new `status` to avoid confusing third-
- // party code that consumes the `Status` enum.
- _unflakableIsQuarantined: true,
- };
- }
- }
- return assertionResult;
- }
- );
-
- const numFailingTests = testResults.filter(
- (assertionResult) =>
- assertionResult.status === FAILED &&
- assertionResult._unflakableIsQuarantined !== true
- ).length;
- const numQuarantinedTests = testResults.filter(
- (assertionResult) => assertionResult._unflakableIsQuarantined === true
- ).length;
- const processedTestResult: UnflakableTestResult =
- attempt === 0
- ? {
- ...testResult,
- // NB: If this value is non-zero, the whole Jest run will terminate with a non-zero exit
- // code.
- numFailingTests,
- _unflakableAttempt: attempt,
- _unflakableNumQuarantinedTests: numQuarantinedTests,
- testResults,
- }
- : // Don't double-count retried tests or SummaryReporter will produce confusing results.
- {
- ...testResult,
- numFailingTests: 0,
- numPassingTests: 0,
- numPendingTests: 0,
- numTodoTests: 0,
- _unflakableAttempt: attempt,
- _unflakableNumQuarantinedTests: numQuarantinedTests,
- testResults,
- };
-
- if (numFailingTests > 0 || numQuarantinedTests > 0) {
- testFailures.push({ test, testResult });
- }
-
- await onResult(test, processedTestResult);
- };
+type TestFailure = { test: Test; testResult: UnflakableTestResult };
class UnflakableRunner {
+ readonly supportsEventEmitters = true;
+
private readonly context?: TestRunnerContext;
- private readonly cwd: string;
private readonly globalConfig: Config.GlobalConfig;
private readonly manifest: Promise;
- private readonly unflakableConfig: UnflakableConfig;
+ private readonly unflakableConfig: UnflakableJestConfig;
+
+ private readonly capturedOutput: {
+ [key in string]: {
+ stderr: string;
+ stdout: string;
+ };
+ } = {};
+
+ private testEventHandlers: {
+ [key in keyof TestEvents]?: ((
+ eventData: TestEvents[key]
+ ) => void | Promise)[];
+ } = {};
constructor(globalConfig: Config.GlobalConfig, context?: TestRunnerContext) {
- this.cwd = process.cwd();
- this.unflakableConfig = loadConfigSync(globalConfig.rootDir);
+ debug("constructor");
+ this.unflakableConfig = loadConfig(globalConfig.rootDir);
const testSuiteId = this.unflakableConfig.enabled
? this.unflakableConfig.testSuiteId
@@ -158,6 +88,193 @@ class UnflakableRunner {
this.globalConfig = globalConfig;
}
+ // We expose an on() method that TestScheduler can call to register its event callbacks:
+ // https://github.com/jestjs/jest/blob/7cf50065ace0f0fffeb695a7980e404a17d3b761/packages/jest-core/src/TestScheduler.ts#L264.
+ // We also return an unsubscribe function from each call, although unsubscribing doesn't seem to
+ // serve much of a purpose since the test runner goes out of scope immediately after Jest calls
+ // the unsubscribe functions. We just do the same thing Jest does and unregister our own handlers
+ // before the inner TestRunner goes out of scope, and then clear the TestScheduler's registered
+ // callbacks from UnflakableTestRunner when TestScheduler calls its unsubscribe functions. This
+ // may be needed to break circular references and ensure that everything gets GCed.
+ on(
+ eventName: Name,
+ listener: (eventData: TestEvents[Name]) => void | Promise
+ ): UnsubscribeFn {
+ debug(`subscribing to \`${eventName}\` listener`);
+ if (this.testEventHandlers[eventName] === undefined) {
+ this.testEventHandlers[eventName] = [];
+ }
+
+ type EventListener = (eventData: TestEvents[Name]) => void | Promise;
+
+ (this.testEventHandlers[eventName] as EventListener[]).push(listener);
+
+ return () => {
+ debug(`unsubscribing from \`${eventName}\` listener`);
+ const idx = (
+ this.testEventHandlers[eventName] as EventListener[]
+ ).indexOf(listener);
+ if (idx !== -1) {
+ (this.testEventHandlers[eventName] as EventListener[]).splice(idx, 1);
+ }
+ };
+ }
+
+ private async isFailureTestIndependent(
+ testFilePath: string,
+ assertionResult: UnflakableAssertionResult
+ ): Promise {
+ if (typeof this.unflakableConfig.isFailureTestIndependent === "function") {
+ return this.unflakableConfig.isFailureTestIndependent({
+ failure: assertionResult.failureMessages.join("\n"),
+ stdout: assertionResult._unflakableCapturedStdout ?? "",
+ stderr: assertionResult._unflakableCapturedStderr ?? "",
+ testFilePath,
+ testName: [...assertionResult.ancestorTitles, assertionResult.title],
+ });
+ } else if (Array.isArray(this.unflakableConfig.isFailureTestIndependent)) {
+ return this.unflakableConfig.isFailureTestIndependent.some(
+ (regex) =>
+ regex.test(assertionResult.failureMessages.join("\n")) ||
+ regex.test(assertionResult._unflakableCapturedStdout ?? "") ||
+ regex.test(assertionResult._unflakableCapturedStderr ?? "")
+ );
+ }
+ return false;
+ }
+
+ // Called after each test *file* runs successfully (which may include failed tests, but the test
+ // file itself didn't throw any errors when it was loaded). This function modifies
+ // `testResult.testResults` by adding our own fields, and returns an updated
+ // `UnflakableTestResult` that also includes some of our own fields. In the case of retries, we
+ // also clear stats that would otherwise result in double-counted tests being emitted by the
+ // SummaryReporter.
+ private async onResult(
+ attempt: number,
+ manifest: TestSuiteManifest | undefined,
+ repoRoot: string,
+ testsToRetry: TestFailure[],
+ test: Test,
+ testResult: TestResult
+ ): Promise {
+ debug(`onResult attempt=${attempt} path=\`${test.path}\``);
+
+ await Promise.all(
+ testResult.testResults.map(
+ async (assertionResult: UnflakableAssertionResult): Promise => {
+ const testFilename = toPosix(path.relative(repoRoot, test.path));
+
+ const key = JSON.stringify(testKey(assertionResult, false));
+ if (this.capturedOutput[key] !== undefined) {
+ assertionResult._unflakableCapturedStderr =
+ this.capturedOutput[key].stderr;
+ assertionResult._unflakableCapturedStdout =
+ this.capturedOutput[key].stdout;
+ }
+
+ delete this.capturedOutput[key];
+
+ if (assertionResult.status === FAILED) {
+ try {
+ assertionResult._unflakableIsFailureTestIndependent =
+ await this.isFailureTestIndependent(
+ testResult.testFilePath,
+ assertionResult
+ );
+ debug(
+ `Failure is${
+ assertionResult._unflakableIsFailureTestIndependent === true
+ ? ""
+ : " not"
+ } test independent`
+ );
+ } catch (e) {
+ process.stderr.write(
+ chalk.red(
+ `ERROR: Failed to evaluate isFailureTestIndependent: ${util.inspect(
+ e
+ )}\n`
+ )
+ );
+ }
+
+ if (manifest === undefined) {
+ debug(
+ "Not quarantining test failure due to failure to fetch manifest"
+ );
+ } else if (
+ this.unflakableConfig.quarantineMode === "no_quarantine"
+ ) {
+ debug(
+ "Not quarantining test failure because quarantineMode is set to `no_quarantine`"
+ );
+ } else {
+ const isQuarantined = isTestQuarantined(
+ manifest,
+ testFilename,
+ testKey(assertionResult, true)
+ );
+ debug(
+ `Test is ${
+ isQuarantined ? "" : "NOT "
+ }quarantined: ${JSON.stringify(
+ testKey(assertionResult, false)
+ )} in file ${testFilename}`
+ );
+
+ // Use a separate field instead of adding a new `status` to avoid confusing third-
+ // party code that consumes the `Status` enum.
+ assertionResult._unflakableIsQuarantined = isQuarantined;
+ }
+ }
+ }
+ )
+ );
+
+ // We don't treat test-independent failures as failing tests at this point because that would
+ // cause Jest to terminate with a non-zero exit code, and we don't know yet if any subsequent
+ // attempts will pass.
+ const numFailingTests = testResult.testResults.filter(
+ (assertionResult: UnflakableAssertionResult) =>
+ assertionResult.status === FAILED &&
+ assertionResult._unflakableIsQuarantined !== true &&
+ assertionResult._unflakableIsFailureTestIndependent !== true
+ ).length;
+
+ // We retry any type of failure, including quarantined and test-independent failures.
+ if (
+ testResult.testResults.some(
+ (assertionResult) => assertionResult.status === FAILED
+ )
+ ) {
+ testsToRetry.push({ test, testResult });
+ }
+
+ (testResult as UnflakableTestResult)._unflakableAttempt = attempt;
+ if (attempt === 0) {
+ // NB: If this value is non-zero, the whole Jest run will terminate with a non-zero exit
+ // code.
+ testResult.numFailingTests = numFailingTests;
+ } else {
+ // Don't double-count retried tests or SummaryReporter will produce confusing results.
+ testResult.numFailingTests = 0;
+ testResult.numPassingTests = 0;
+ testResult.numPendingTests = 0;
+ testResult.numTodoTests = 0;
+ }
+ }
+
+ // FIXME: test what happens when running Jest with multiple --projects arguments. Jest seems to
+ // create a separate "context" per project and associate that context with each test in the
+ // project.
+
+ // EmittingTestRunnerInterface in Jest 28+.
+ async runTests(
+ tests: Array,
+ watcher: TestWatcher,
+ options: TestRunnerOptions
+ ): Promise;
+ // CallbackTestRunnerInterface in Jest 28+, and any version < Jest 28.
async runTests(
tests: Array,
watcher: TestWatcher,
@@ -165,59 +282,83 @@ class UnflakableRunner {
onResult: OnTestSuccess,
onFailure: OnTestFailure,
options: TestRunnerOptions
+ ): Promise;
+ async runTests(
+ tests: Array,
+ watcher: TestWatcher,
+ onStartOrOptions: OnTestStart | TestRunnerOptions,
+ onResult?: OnTestSuccess,
+ onFailure?: OnTestFailure,
+ options?: TestRunnerOptions
): Promise {
- let testFailures = await this.runTestsImpl(
+ debug("runTests");
+ const repoRoot =
+ this.unflakableConfig.enabled && this.unflakableConfig.gitAutoDetect
+ ? await (async (): Promise => {
+ const git = await loadGitRepo();
+ return git !== null
+ ? await getRepoRoot(git)
+ : this.globalConfig.rootDir;
+ })()
+ : this.globalConfig.rootDir;
+
+ let testsToRetry = await this.runTestsImpl(
tests,
watcher,
- onStart,
+ onResult !== undefined ? (onStartOrOptions as OnTestStart) : undefined,
onResult,
onFailure,
- options,
- this.cwd,
+ onResult !== undefined
+ ? (options as TestRunnerOptions)
+ : (onStartOrOptions as TestRunnerOptions),
+ repoRoot,
this.globalConfig,
- this.context,
- this.unflakableConfig,
- await this.manifest,
0 // attempt
);
- if (!this.unflakableConfig.enabled || testFailures.length === 0) {
- return;
- }
-
const attempts =
- this.unflakableConfig.failureRetries > 0
+ this.unflakableConfig.enabled && this.unflakableConfig.failureRetries > 0
? this.unflakableConfig.failureRetries + 1
: 1;
// NB: jest-circus also supports failure retries, but it's configured via the `jest` object
// in each test file's environment, not via the global config that we have control over.
for (
let attempt = 1;
- testFailures.length !== 0 && attempt < attempts;
+ testsToRetry.length !== 0 && attempt < attempts;
attempt++
) {
+ const numTestsToRetry = testsToRetry.reduce(
+ (count, { testResult }) =>
+ count +
+ testResult.testResults.filter(
+ // NB: We retry all failed tests, but quarantined and test-independent failures
+ // aren't counted in numFailingTests.
+ (assertion) => assertion.status === "failed"
+ ).length,
+ 0
+ );
process.stderr.write(
chalk.stderr.yellow.bold(
- `Retrying ${testFailures.reduce(
- (count, { testResult }) => count + testResult.numFailingTests,
- 0
- )} failed test(s) from ${testFailures.length} file(s) -- ${
- attempts - attempt - 1
- } ${attempts - attempt - 1 === 1 ? "retry" : "retries"} remaining\n`
- )
+ `Retrying ${numTestsToRetry} failed test${
+ numTestsToRetry === 1 ? "" : "s"
+ } from ${testsToRetry.length} file${
+ testsToRetry.length === 1 ? "" : "s"
+ } -- ${attempts - attempt - 1} ${
+ attempts - attempt - 1 === 1 ? "retry" : "retries"
+ } remaining`
+ ) + "\n"
);
// Similar to how we skip quarantined tests when quarantineMode is "skip_tests", we need to
// re-run each failed file separately so that we can pass a custom testNamePattern regex to
// each. This ensures that we only rerun the failed tests in each file.
- testFailures = await testFailures.reduce(
+ testsToRetry = await testsToRetry.reduce(
(promise, { test, testResult }) =>
- promise.then(async (newTestFailures) => {
+ promise.then(async (newTestsToRetry) => {
const failedTestPattern = testResult.testResults
.filter(
(assertionResult: UnflakableAssertionResult) =>
- assertionResult.status === FAILED &&
- assertionResult._unflakableIsQuarantined !== true
+ assertionResult.status === FAILED
)
.map((failedTest) => {
const testId = testKey(failedTest, false);
@@ -236,20 +377,21 @@ class UnflakableRunner {
...this.globalConfig,
testNamePattern,
});
- return newTestFailures.concat(
+ return newTestsToRetry.concat(
await this.runTestsImpl(
[test],
watcher,
- onStart,
+ onResult !== undefined
+ ? (onStartOrOptions as OnTestStart)
+ : undefined,
// We re-wrap it in the next iteration.
onResult,
onFailure,
- options,
- this.cwd,
+ onResult !== undefined
+ ? (options as TestRunnerOptions)
+ : (onStartOrOptions as TestRunnerOptions),
+ repoRoot,
filteredGlobalConfig,
- this.context,
- this.unflakableConfig,
- await this.manifest,
attempt
)
);
@@ -259,70 +401,171 @@ class UnflakableRunner {
}
}
+ // Returns an array of tests that should be retried, which includes both quarantined and
+ // test-independent failures.
private async runTestsImpl(
tests: Test[],
watcher: TestWatcher,
- onStart: OnTestStart,
- onResult: OnTestSuccess,
- onFailure: OnTestFailure,
+ onStart: OnTestStart | undefined,
+ onResult: OnTestSuccess | undefined,
+ onFailure: OnTestFailure | undefined,
options: TestRunnerOptions,
- cwd: string,
+ repoRoot: string,
globalConfig: Config.GlobalConfig,
- context: TestRunnerContext | undefined,
- unflakableConfig: UnflakableConfig,
- manifest: TestSuiteManifest | undefined,
attempt: number
): Promise {
- const testFailures: TestFailure[] = [];
-
- const onResultImpl = this.unflakableConfig.enabled
- ? wrapOnResult({
- attempt,
- cwd,
- manifest,
- onResult,
- quarantineMode: unflakableConfig.quarantineMode,
- testFailures,
- })
- : onResult;
-
- const runTests = (
+ debug("runTestsImpl");
+ const testsToRetry: TestFailure[] = [];
+ const manifest = await this.manifest;
+
+ const runTests = async (
globalConfig: Config.GlobalConfig,
tests: Array,
watcher: TestWatcher,
options: TestRunnerOptions
): Promise => {
- const testRunner = new TestRunner(globalConfig, context ?? {});
+ const testRunner = new TestRunner(globalConfig, this.context ?? {});
- // We have to give up on per-event type safety here to maintain compatibility with versions
- // prior to 26.2.0.
- type EventListener = (eventData: unknown) => void | Promise;
- type EventSubscriber = (
- eventName: string,
- listener: EventListener
- ) => () => void;
+ const onResultImpl =
+ onResult !== undefined && this.unflakableConfig.enabled
+ ? async (test: Test, result: TestResult): Promise => {
+ // NB: We call this first because it modifies `result`.
+ await this.onResult(
+ attempt,
+ manifest,
+ repoRoot,
+ testsToRetry,
+ test,
+ result
+ );
+ await onResult(test, result);
+ }
+ : onResult;
// The event emitter interface was introduced in Jest 26.2.0 (see
// https://github.com/facebook/jest/pull/10227).
if ((testRunner as unknown as { on?: unknown }).on !== undefined) {
const eventEmittingTestRunner = testRunner as unknown as {
- on: EventSubscriber;
+ on: (
+ eventName: Name,
+ listener: (eventData: TestEvents[Name]) => void | Promise
+ ) => UnsubscribeFn;
};
- eventEmittingTestRunner.on("test-file-start", (([test]: [Test]) =>
- onStart(test)) as EventListener);
- eventEmittingTestRunner.on("test-file-success", (([test, result]: [
- Test,
- TestResult
- ]) => onResultImpl(test, result)) as EventListener);
- eventEmittingTestRunner.on("test-file-failure", (([test, error]: [
- Test,
- SerializableError
- ]) => onFailure(test, error)) as EventListener);
-
- return testRunner.runTests.length === 6
- ? // Jest prior to 28.0.0 expects the callback arguments (see
- // https://github.com/facebook/jest/pull/12641).
+
+ // EmittingTestRunnerInterface in Jest 28+.
+ const supportsEventEmitters = (
+ testRunner as {
+ supportsEventEmitters?: boolean;
+ }
+ ).supportsEventEmitters;
+
+ const unsubscribes = [
+ ...(onStart !== undefined
+ ? [
+ eventEmittingTestRunner.on(
+ "test-file-start",
+ ([test]: [Test]) => onStart(test)
+ ),
+ ]
+ : []),
+ ...(onResultImpl !== undefined
+ ? [
+ eventEmittingTestRunner.on(
+ "test-file-success",
+ ([test, result]: [Test, TestResult]) =>
+ onResultImpl(test, result)
+ ),
+ ]
+ : []),
+ ...(onFailure !== undefined
+ ? [
+ eventEmittingTestRunner.on(
+ "test-file-failure",
+ ([test, error]: [Test, SerializableError]) =>
+ onFailure(test, error)
+ ),
+ ]
+ : []),
+ ...(supportsEventEmitters === true
+ ? [
+ eventEmittingTestRunner.on(
+ "test-case-result",
+ ([testPath, assertionResult]: [
+ string,
+ UnflakableAssertionResult
+ ]): void => {
+ debug(
+ `on(test-case-result) path=\`${testPath}\` title=%o status=%o`,
+ [
+ ...assertionResult.ancestorTitles,
+ assertionResult.title,
+ ],
+ assertionResult.status
+ );
+
+ this.capturedOutput[
+ JSON.stringify(testKey(assertionResult, false))
+ ] = {
+ stdout: assertionResult._unflakableCapturedStdout ?? "",
+ stderr: assertionResult._unflakableCapturedStderr ?? "",
+ };
+ }
+ ),
+ ]
+ : []),
+ ...Object.entries(this.testEventHandlers).flatMap(
+ ([eventName, listeners]) =>
+ listeners.map((listener) => {
+ if (
+ eventName === "test-file-success" &&
+ this.unflakableConfig.enabled
+ ) {
+ return eventEmittingTestRunner.on(
+ "test-file-success",
+ async ([
+ test,
+ result,
+ ]: TestEvents["test-file-success"]): Promise => {
+ // NB: We call this first because it modifies `result`.
+ await this.onResult(
+ attempt,
+ manifest,
+ repoRoot,
+ testsToRetry,
+ test,
+ result
+ );
+ // NB: This triggers Jest's ReporterDispatcher to call onTestResult() for each
+ // reporter.
+ return (
+ listener as (
+ eventData: TestEvents["test-file-success"]
+ ) => void | Promise
+ )([test, result]);
+ }
+ );
+ } else {
+ return eventEmittingTestRunner.on(
+ eventName as keyof TestEvents,
+ listener as (
+ eventData: TestEvents[keyof TestEvents]
+ ) => void | Promise
+ );
+ }
+ })
+ ),
+ ];
+
+ await (supportsEventEmitters === true
+ ? // EmittingTestRunnerInterface in Jest 28+.
(
+ testRunner.runTests as unknown as (
+ tests: Array,
+ watcher: TestWatcher,
+ options: TestRunnerOptions
+ ) => Promise
+ )(tests, watcher, options)
+ : (
testRunner.runTests as unknown as (
tests: Array,
watcher: TestWatcher,
@@ -340,15 +583,9 @@ class UnflakableRunner {
undefined,
undefined,
options
- )
- : // Jest >= 28.0.0 no longer expects the callback arguments.
- (
- testRunner.runTests as unknown as (
- tests: Array,
- watcher: TestWatcher,
- options: TestRunnerOptions
- ) => Promise
- )(tests, watcher, options);
+ ));
+
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
} else {
// Prior to Jest 26.2.0, use the legacy callback interface.
return (
@@ -378,7 +615,8 @@ class UnflakableRunner {
if (
manifest !== undefined &&
manifest.quarantined_tests.length > 0 &&
- unflakableConfig.quarantineMode === "skip_tests"
+ this.unflakableConfig.enabled &&
+ this.unflakableConfig.quarantineMode === "skip_tests"
) {
debug(
`Skipping ${manifest.quarantined_tests.length} quarantined test(s)`
@@ -396,7 +634,7 @@ class UnflakableRunner {
// Then, we run the remaining test files normally below.
await tests
.reduce((promise, test) => {
- const relPath = path.relative(cwd, test.path);
+ const relPath = toPosix(path.relative(repoRoot, test.path));
const quarantinedTestsInFile = quarantinedTestsByFile[relPath];
if (
quarantinedTestsInFile !== undefined &&
@@ -446,7 +684,11 @@ class UnflakableRunner {
}, Promise.resolve())
.then(() => {
const normalTestFiles = tests.filter(
- (test) => !(path.relative(cwd, test.path) in quarantinedTestsByFile)
+ (test) =>
+ !(
+ toPosix(path.relative(repoRoot, test.path)) in
+ quarantinedTestsByFile
+ )
);
if (normalTestFiles.length > 0) {
debug(
@@ -460,7 +702,7 @@ class UnflakableRunner {
} else {
await runTests(globalConfig, tests, watcher, options);
}
- return testFailures;
+ return testsToRetry;
}
}
diff --git a/packages/jest-plugin/src/test-runner.ts b/packages/jest-plugin/src/test-runner.ts
new file mode 100644
index 0000000..4f50009
--- /dev/null
+++ b/packages/jest-plugin/src/test-runner.ts
@@ -0,0 +1,87 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import { TestEvents, TestFileEvent, TestResult } from "@jest/test-result";
+import { JestEnvironment } from "@jest/environment";
+import { Config } from "@jest/types";
+import Runtime from "jest-runtime";
+import circus from "jest-circus/runner";
+import { debug as _debug } from "debug";
+import util from "util";
+import { UnflakableAssertionResult } from "./types";
+
+const baseStderrWrite = process.stderr.write.bind(process.stderr);
+const baseStdoutWrite = process.stdout.write.bind(process.stdout);
+
+const debug = _debug("unflakable:test-runner");
+// Don't capture our own debug output as test output.
+debug.log = (...args: unknown[]): boolean =>
+ baseStderrWrite(util.format(...args) + "\n");
+
+const write =
+ (base: NodeJS.WriteStream["write"], capture: (buf: string) => void) =>
+ (
+ buffer: Uint8Array | string,
+ encodingOrCb?: BufferEncoding | ((err?: Error) => void),
+ cb?: (err?: Error) => void
+ ): boolean => {
+ capture(
+ typeof buffer === "string"
+ ? buffer
+ : typeof encodingOrCb === "string"
+ ? new util.TextDecoder(encodingOrCb).decode(buffer)
+ : buffer.toString()
+ );
+ return base(buffer, encodingOrCb as BufferEncoding | undefined, cb);
+ };
+
+export default (
+ globalConfig: Config.GlobalConfig,
+ config: Config.ProjectConfig,
+ environment: JestEnvironment,
+ runtime: Runtime,
+ testPath: string,
+ sendMessageToJest?: TestFileEvent
+): Promise => {
+ debug(`initialize pid=${process.pid} testPath=\`${testPath}\``);
+
+ let capturedStderr = "";
+ let capturedStdout = "";
+
+ process.stderr.write = write(
+ baseStderrWrite,
+ (buf) => (capturedStderr += buf)
+ );
+ process.stdout.write = write(
+ baseStdoutWrite,
+ (buf) => (capturedStdout += buf)
+ );
+
+ // TODO: Support other test runners?
+ return circus(
+ globalConfig,
+ config,
+ environment,
+ runtime,
+ testPath,
+ sendMessageToJest !== undefined
+ ? (
+ eventName: T,
+ args: TestEvents[T]
+ ): unknown => {
+ debug(
+ `sendMessageToJest pid=${process.pid} testPath=\`${testPath}\` eventName=\`${eventName}\``
+ );
+
+ if (eventName === "test-case-result") {
+ const result = args[1] as UnflakableAssertionResult;
+ result._unflakableCapturedStderr = capturedStderr;
+ result._unflakableCapturedStdout = capturedStdout;
+ capturedStdout = "";
+ capturedStderr = "";
+ }
+
+ return sendMessageToJest(eventName, args);
+ }
+ : undefined
+ );
+};
diff --git a/packages/jest-plugin/src/types.ts b/packages/jest-plugin/src/types.ts
index 8c73927..3f72bbf 100644
--- a/packages/jest-plugin/src/types.ts
+++ b/packages/jest-plugin/src/types.ts
@@ -1,21 +1,52 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import type {
AggregatedResult,
AssertionResult,
TestResult,
} from "@jest/test-result";
+import {
+ UnflakableConfig,
+ UnflakableConfigEnabled,
+} from "@unflakable/plugins-common";
+
+export type IsFailureTestIndependentFn = (args: {
+ failure: string;
+ stderr: string;
+ stdout: string;
+ testFilePath: string;
+ testName: string[];
+}) => boolean | Promise;
+
+export type UnflakableJestConfigInner = {
+ isFailureTestIndependent?: RegExp[] | IsFailureTestIndependentFn;
+ __unstableIsFailureTestIndependent?: undefined;
+};
+
+export type UnflakableJestConfig = UnflakableConfig & UnflakableJestConfigInner;
+
+export type UnflakableJestConfigEnabled = UnflakableConfigEnabled &
+ UnflakableJestConfigInner;
export type UnflakableAssertionResult = AssertionResult & {
+ _unflakableCapturedStderr?: string;
+ _unflakableCapturedStdout?: string;
+ _unflakableIsFailureTestIndependent?: boolean;
_unflakableIsQuarantined?: boolean;
};
-export type UnflakableTestResult = TestResult & {
+export type UnflakableTestResult = Omit & {
_unflakableAttempt?: number;
- // Added by runner.
- _unflakableNumQuarantinedTests?: number;
- // Added by reporter.
- _unflakableNumFlakyTests?: number;
+ testResults: UnflakableAssertionResult[];
+};
+
+// Counts added by reporter.
+export type UnflakableTestResultWithCounts = UnflakableTestResult & {
+ _unflakableNumQuarantinedTests: number;
+ _unflakableNumFlakyTests: number;
+ // This represents a *subset* of numPassingTests and should not be added to it, or we'll be
+ // double-counting passes.
+ _unflakableNumPassingTestsWithIndependentFailures: number;
};
export type UnflakableAggregatedResult = Omit<
@@ -24,3 +55,20 @@ export type UnflakableAggregatedResult = Omit<
> & {
testResults: UnflakableTestResult[];
};
+
+export type UnflakableAggregatedResultWithCounts = Omit<
+ AggregatedResult,
+ "testResults"
+> & {
+ _unflakableNumFlakyTests: number;
+ _unflakableNumQuarantinedTests: number;
+ _unflakableNumQuarantinedSuites: number;
+
+ // These represent *subsets* of numPassedTests/numPassedTestSuites and should not be added to
+ // those values. Rather, they are intended to be consumed by the SummaryReporter to show which
+ // tests/suites would have failed were it not for the failures being test-independent.
+ _unflakableNumPassedTestsWithIndependentFailures: number;
+ _unflakableNumPassedTestSuitesWithIndependentFailures: number;
+
+ testResults: UnflakableTestResultWithCounts[];
+};
diff --git a/packages/jest-plugin/src/utils.ts b/packages/jest-plugin/src/utils.ts
index 2bdc0d7..2138d0b 100644
--- a/packages/jest-plugin/src/utils.ts
+++ b/packages/jest-plugin/src/utils.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import type { AssertionResult, Status } from "@jest/test-result";
import jestPackage from "jest/package.json";
@@ -14,7 +14,7 @@ export const PASSED: Status = "passed";
export const testKey = (
assertionResult: AssertionResult,
- normalize = true
+ normalize: boolean
): string[] => {
const fullKey = [...assertionResult.ancestorTitles, assertionResult.title];
return normalize ? normalizeTestName(fullKey) : fullKey;
diff --git a/packages/jest-plugin/src/vendored/SummaryReporter.ts b/packages/jest-plugin/src/vendored/SummaryReporter.ts
index 3298410..5f54f95 100644
--- a/packages/jest-plugin/src/vendored/SummaryReporter.ts
+++ b/packages/jest-plugin/src/vendored/SummaryReporter.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
/*
This file includes portions of a Jest source code file originally downloaded from:
@@ -39,7 +39,7 @@ import { BaseReporter, ReporterOnStartOptions } from "@jest/reporters";
import { getSummary } from "./getSummary";
import getSnapshotSummary from "./getSnapshotSummary";
import { getResultHeader } from "./getResultHeader";
-import { UnflakableAggregatedResult } from "../types";
+import { UnflakableAggregatedResultWithCounts } from "../types";
const TEST_SUMMARY_THRESHOLD = 20;
@@ -106,7 +106,7 @@ export default class SummaryReporter extends BaseReporter {
onRunComplete(
contexts: Set | undefined,
- aggregatedResults: UnflakableAggregatedResult
+ aggregatedResults: UnflakableAggregatedResultWithCounts
): void {
const { numTotalTestSuites, testResults, wasInterrupted } =
aggregatedResults;
diff --git a/packages/jest-plugin/src/vendored/getResultHeader.ts b/packages/jest-plugin/src/vendored/getResultHeader.ts
index 37f80cf..86cc030 100644
--- a/packages/jest-plugin/src/vendored/getResultHeader.ts
+++ b/packages/jest-plugin/src/vendored/getResultHeader.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import { utils } from "@jest/reporters";
import { TestResult } from "@jest/test-result";
diff --git a/packages/jest-plugin/src/vendored/getSummary.ts b/packages/jest-plugin/src/vendored/getSummary.ts
index fda7bbf..5b88c23 100644
--- a/packages/jest-plugin/src/vendored/getSummary.ts
+++ b/packages/jest-plugin/src/vendored/getSummary.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
/*
This file includes portions of a Jest source code file originally downloaded from:
@@ -31,72 +31,14 @@ All modifications to the above referenced file are copyrighted and licensed unde
forth in the LICENSE file at the root of this repository.
*/
-import type { SummaryOptions, Test } from "@jest/reporters";
-import {
- UnflakableAggregatedResult,
- UnflakableAssertionResult,
- UnflakableTestResult,
-} from "../types";
+import type { SummaryOptions } from "@jest/reporters";
+import { UnflakableAggregatedResultWithCounts } from "../types";
import * as JestUtil from "jest-util";
-import type { AssertionResult } from "@jest/test-result";
import chalk from "chalk";
import { formatTime } from "./formatTime";
const PROGRESS_BAR_WIDTH = 40;
-const getValuesCurrentTestCases = (
- currentTestCases: {
- test: Test;
- testCaseResult: UnflakableAssertionResult;
- }[] = []
-): {
- numFailingTests: number;
- numPassingTests: number;
- numPendingTests: number;
- numQuarantinedTests: number;
- numTodoTests: number;
- numTotalTests: number;
-} => {
- let numFailingTests = 0;
- let numPassingTests = 0;
- let numPendingTests = 0;
- let numQuarantinedTests = 0;
- let numTodoTests = 0;
- let numTotalTests = 0;
- currentTestCases.forEach((testCase) => {
- if (testCase.testCaseResult._unflakableIsQuarantined === true) {
- numQuarantinedTests++;
- } else {
- switch (testCase.testCaseResult.status) {
- case "failed":
- numFailingTests++;
- break;
- case "passed":
- numPassingTests++;
- break;
- case "skipped":
- numPendingTests++;
- break;
- case "todo":
- numTodoTests++;
- break;
- default:
- break;
- }
- }
- numTotalTests++;
- });
-
- return {
- numFailingTests,
- numPassingTests,
- numPendingTests,
- numQuarantinedTests,
- numTodoTests,
- numTotalTests,
- };
-};
-
const renderTime = (
runTime: number,
estimatedTime: number,
@@ -131,7 +73,7 @@ const renderTime = (
};
export const getSummary = (
- aggregatedResults: UnflakableAggregatedResult,
+ aggregatedResults: UnflakableAggregatedResultWithCounts,
options?: SummaryOptions
): string => {
let runTime = (Date.now() - aggregatedResults.startTime) / 1000;
@@ -139,33 +81,6 @@ export const getSummary = (
runTime = Math.floor(runTime);
}
- const valuesForCurrentTestCases = getValuesCurrentTestCases(
- (
- options as {
- // Not defined in Jest < 26.2.
- currentTestCases?:
- | { test: Test; testCaseResult: AssertionResult }[]
- | undefined;
- }
- )?.currentTestCases ?? []
- );
-
- let suitesQuarantined = 0,
- testsFlaky = 0,
- testsQuarantined = 0;
- aggregatedResults.testResults.forEach((testResult: UnflakableTestResult) => {
- testsFlaky += testResult._unflakableNumFlakyTests ?? 0;
- testsQuarantined += testResult._unflakableNumQuarantinedTests ?? 0;
- if (
- !testResult.skipped &&
- testResult.numFailingTests === 0 &&
- testResult.testExecError === undefined &&
- (testResult._unflakableNumQuarantinedTests ?? 0) > 0
- ) {
- suitesQuarantined += 1;
- }
- });
-
const estimatedTime = options?.estimatedTime ?? 0;
const snapshotResults = aggregatedResults.snapshot;
const snapshotsAdded = snapshotResults.added;
@@ -177,16 +92,22 @@ export const getSummary = (
const snapshotsTotal = snapshotResults.total;
const snapshotsUpdated = snapshotResults.updated;
const suitesFailed = aggregatedResults.numFailedTestSuites;
- const suitesPassed =
- aggregatedResults.numPassedTestSuites - suitesQuarantined;
+ const suitesPassed = aggregatedResults.numPassedTestSuites;
+ const suitesPassedWithIndependentFailures =
+ aggregatedResults._unflakableNumPassedTestSuitesWithIndependentFailures;
const suitesPending = aggregatedResults.numPendingTestSuites;
+ const suitesQuarantined = aggregatedResults._unflakableNumQuarantinedSuites;
const suitesRun = suitesFailed + suitesPassed + suitesQuarantined;
const suitesTotal = aggregatedResults.numTotalTestSuites;
const testsFailed = aggregatedResults.numFailedTests;
+ const testsFlaky = aggregatedResults._unflakableNumFlakyTests;
const testsPassed = aggregatedResults.numPassedTests;
+ const testsPassedWithIndependentFailures =
+ aggregatedResults._unflakableNumPassedTestsWithIndependentFailures;
const testsPending = aggregatedResults.numPendingTests;
+ const testsQuarantined = aggregatedResults._unflakableNumQuarantinedTests;
const testsTodo = aggregatedResults.numTodoTests;
- const testsTotal = aggregatedResults.numTotalTests + testsQuarantined;
+ const testsTotal = aggregatedResults.numTotalTests;
const width = options?.width ?? 0;
const suites = `${chalk.bold("Test Suites: ")}${
@@ -200,46 +121,42 @@ export const getSummary = (
? chalk.bold.yellow(`${suitesPending} skipped`) + ", "
: ""
}${
- suitesPassed > 0 ? chalk.bold.green(`${suitesPassed} passed`) + ", " : ""
+ suitesPassed > 0
+ ? chalk.bold.green(
+ `${suitesPassed} passed${
+ suitesPassedWithIndependentFailures > 0
+ ? ` (${suitesPassedWithIndependentFailures} with test-independent failures)`
+ : ""
+ }`
+ ) + ", "
+ : ""
}${
suitesRun !== suitesTotal ? `${suitesRun} of ${suitesTotal}` : suitesTotal
} total`;
- const updatedTestsFailed = Math.max(
- testsFailed + valuesForCurrentTestCases.numFailingTests - testsFlaky,
- 0
- );
- const updatedTestsQuarantined =
- testsQuarantined + valuesForCurrentTestCases.numQuarantinedTests;
- const updatedTestsPending =
- testsPending + valuesForCurrentTestCases.numPendingTests;
- const updatedTestsTodo = testsTodo + valuesForCurrentTestCases.numTodoTests;
- const updatedTestsPassed =
- testsPassed + valuesForCurrentTestCases.numPassingTests;
- const updatedTestsTotal =
- testsTotal + valuesForCurrentTestCases.numTotalTests;
-
const tests =
chalk.bold("Tests: ") +
- (updatedTestsFailed > 0
- ? chalk.bold.red(`${updatedTestsFailed} failed`) + ", "
- : "") +
+ (testsFailed > 0 ? chalk.bold.red(`${testsFailed} failed`) + ", " : "") +
(testsFlaky > 0
? chalk.bold.magentaBright(`${testsFlaky} flaky`) + ", "
: "") +
- (updatedTestsQuarantined > 0
- ? chalk.bold.yellow(`${updatedTestsQuarantined} quarantined`) + ", "
+ (testsQuarantined > 0
+ ? chalk.bold.yellow(`${testsQuarantined} quarantined`) + ", "
: "") +
- (updatedTestsPending > 0
- ? chalk.bold.yellow(`${updatedTestsPending} skipped`) + ", "
+ (testsPending > 0
+ ? chalk.bold.yellow(`${testsPending} skipped`) + ", "
: "") +
- (updatedTestsTodo > 0
- ? chalk.bold.magenta(`${updatedTestsTodo} todo`) + ", "
- : "") +
- (updatedTestsPassed > 0
- ? chalk.bold.green(`${updatedTestsPassed} passed`) + ", "
+ (testsTodo > 0 ? chalk.bold.magenta(`${testsTodo} todo`) + ", " : "") +
+ (testsPassed > 0
+ ? chalk.bold.green(
+ `${testsPassed} passed${
+ testsPassedWithIndependentFailures > 0
+ ? ` (${testsPassedWithIndependentFailures} with test-independent failures)`
+ : ""
+ }`
+ ) + ", "
: "") +
- `${updatedTestsTotal} total`;
+ `${testsTotal} total`;
const snapshots =
chalk.bold("Snapshots: ") +
diff --git a/packages/jest-plugin/test/.eslintrc.js b/packages/jest-plugin/test/.eslintrc.js
index 4d0da9e..b7a3a23 100644
--- a/packages/jest-plugin/test/.eslintrc.js
+++ b/packages/jest-plugin/test/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
env: {
diff --git a/babel.config.js b/packages/jest-plugin/test/babel.config.js
similarity index 83%
rename from babel.config.js
rename to packages/jest-plugin/test/babel.config.js
index 18869fd..d3cffba 100644
--- a/babel.config.js
+++ b/packages/jest-plugin/test/babel.config.js
@@ -1,6 +1,8 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
/* eslint-env node */
+
+/** @type {import('@babel/core').ConfigFunction} */
module.exports = (api) => {
api.cache.using(() => process.env.NODE_ENV);
return {
diff --git a/packages/jest-plugin/test/integration-input/jest.config.js b/packages/jest-plugin/test/integration-input/jest.config.js
index 3fb908f..75d15b8 100644
--- a/packages/jest-plugin/test/integration-input/jest.config.js
+++ b/packages/jest-plugin/test/integration-input/jest.config.js
@@ -1,12 +1,8 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
module.exports = {
clearMocks: true,
- maxWorkers: 2,
- // The /dist path is required until https://github.com/facebook/jest/pull/11961 is fixed, which
- // appears not to be until Jest 28.x.
- reporters: ["@unflakable/jest-plugin/dist/reporter"],
- runner: "@unflakable/jest-plugin/dist/runner",
+ // maxWorkers: 2,
// Default changed in Jest 29 (see
// https://github.com/facebook/jest/blob/94c06ef0aa9b327f3c400610b861e7308b29ee0d/docs/UpgradingToJest29.md).
@@ -21,7 +17,7 @@ module.exports = {
"^.+\\.[jt]sx?$": [
"babel-jest",
{
- configFile: "../../../../babel.config.js",
+ configFile: "../babel.config.js",
},
],
},
diff --git a/packages/jest-plugin/test/integration-input/package.json b/packages/jest-plugin/test/integration-input/package.json
index 73e67cf..f931f29 100644
--- a/packages/jest-plugin/test/integration-input/package.json
+++ b/packages/jest-plugin/test/integration-input/package.json
@@ -6,7 +6,8 @@
"@unflakable/js-api": "workspace:^",
"jest": "25.1.0 - 29",
"jest-each": "25.1.0 - 29",
- "jest-environment-node": "25.1.0 - 29"
+ "jest-environment-node": "25.1.0 - 29",
+ "typescript": "^4.9.5"
},
"scripts": {
"test": "jest",
diff --git a/packages/jest-plugin/test/integration-input/src/fail.test.ts b/packages/jest-plugin/test/integration-input/src/fail.test.ts
index d89a4b0..7448062 100644
--- a/packages/jest-plugin/test/integration-input/src/fail.test.ts
+++ b/packages/jest-plugin/test/integration-input/src/fail.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
describe("describe block", () => {
(process.env.SKIP_FAILURES !== undefined ? it.skip : it)(
@@ -6,6 +6,8 @@ describe("describe block", () => {
// properly.
"should ([escape regex]?.*$ fail",
() => {
+ process.stderr.write("fail stderr\n");
+ process.stdout.write("fail stdout\n");
if (process.env.TEST_SNAPSHOTS !== undefined) {
expect({ foo: true }).toMatchInlineSnapshot(`
Object {
@@ -13,7 +15,7 @@ describe("describe block", () => {
}
`);
} else {
- throw new Error();
+ throw new Error("test failed\nnew line");
}
}
);
diff --git a/packages/jest-plugin/test/integration-input/src/flake.test.ts b/packages/jest-plugin/test/integration-input/src/flake.test.ts
index 0b0633b..9aaf8f0 100644
--- a/packages/jest-plugin/test/integration-input/src/flake.test.ts
+++ b/packages/jest-plugin/test/integration-input/src/flake.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import fs from "fs/promises";
@@ -7,25 +7,26 @@ import fs from "fs/promises";
async (i) => {
if (process.env.FLAKY_TEST_TEMP === undefined) {
throw new Error("missing FLAKY_TEST_TEMP environment variable");
+ } else if (process.env.FLAKE_FAIL_COUNT === undefined) {
+ throw new Error("missing FLAKE_FAIL_COUNT environment variable");
}
const tempFilePath = `${process.env.FLAKY_TEST_TEMP}${i}`;
- // We can't maintain in-memory state between test tries, so we write to a temp file to indicate
- // that it's not the first attempt.
- const exists = await fs
- .stat(tempFilePath)
- .then(() => true)
- .catch(() => false);
- await fs.writeFile(tempFilePath, "");
+ // We can't maintain in-memory state between test tries, so we write to a temp file.
+ const attempt = await fs
+ .readFile(tempFilePath, { encoding: "utf8" })
+ .then(Number.parseInt)
+ .catch(() => 0);
+ await fs.writeFile(tempFilePath, (attempt + 1).toString());
if (process.env.TEST_SNAPSHOTS !== undefined) {
- expect({ exists }).toMatchInlineSnapshot(`
+ expect({ exists: attempt > 0 }).toMatchInlineSnapshot(`
Object {
"exists": true,
}
`);
- } else if (!exists) {
- throw new Error("first try should fail");
+ } else if (attempt < Number.parseInt(process.env.FLAKE_FAIL_COUNT)) {
+ throw new Error(`first try should fail`);
}
}
);
diff --git a/packages/jest-plugin/test/integration-input/src/invalid.test.ts b/packages/jest-plugin/test/integration-input/src/invalid.test.ts
index 626b065..7760c16 100644
--- a/packages/jest-plugin/test/integration-input/src/invalid.test.ts
+++ b/packages/jest-plugin/test/integration-input/src/invalid.test.ts
@@ -1,3 +1,3 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
throw new Error("invalid test file");
diff --git a/packages/jest-plugin/test/integration-input/src/mixed.test.ts b/packages/jest-plugin/test/integration-input/src/mixed.test.ts
index 16b867f..05b0bf7 100644
--- a/packages/jest-plugin/test/integration-input/src/mixed.test.ts
+++ b/packages/jest-plugin/test/integration-input/src/mixed.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
// This test contains both a failed test and a quarantined one, which the reporter should treat as
// a failed test file. However, the quarantined test should still be reported as having been
@@ -10,14 +10,15 @@ describe("mixed", () => {
() => {
if (process.env.TEST_SNAPSHOTS !== undefined) {
// NB: If we include a description here, Jest treats the snapshot as obsolete if the test
- // is skipped (and exits with a non-zero code) since it uses a string equality match instead
+ // is skipped (and exits with a non-zero code) since it uses a string equality match
+ // instead
// of prefix check:
// https://github.com/facebook/jest/blob/54eadb65a9f9ce789df6cf92df82cdbda68c0d4b/packages/jest-snapshot/src/State.ts#L99
expect({ foo: false }).toMatchSnapshot();
} else {
// Have the snapshot pass here so that Jest doesn't treat it as obsolete.
expect({ foo: true }).toMatchSnapshot();
- throw new Error();
+ throw new Error("mixed quarantined test failed");
}
}
);
@@ -25,7 +26,9 @@ describe("mixed", () => {
(process.env.SKIP_FAILURES !== undefined ? it.skip : it)(
"mixed: should fail",
() => {
- throw new Error();
+ process.stderr.write("mixed fail stderr\n");
+ process.stdout.write("mixed fail stdout\n");
+ throw new Error("mixed test failed\nnew line");
}
);
diff --git a/packages/jest-plugin/test/integration-input/src/pass.test.ts b/packages/jest-plugin/test/integration-input/src/pass.test.ts
index 8e79dbf..dbd1e58 100644
--- a/packages/jest-plugin/test/integration-input/src/pass.test.ts
+++ b/packages/jest-plugin/test/integration-input/src/pass.test.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
it("should pass", () => {
if (process.env.TEST_SNAPSHOTS !== undefined) {
diff --git a/packages/jest-plugin/test/integration-input/src/quarantined.test.ts b/packages/jest-plugin/test/integration-input/src/quarantined.test.ts
index f8461e1..1b12471 100644
--- a/packages/jest-plugin/test/integration-input/src/quarantined.test.ts
+++ b/packages/jest-plugin/test/integration-input/src/quarantined.test.ts
@@ -1,10 +1,10 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
describe("describe block", () => {
(process.env.SKIP_QUARANTINED !== undefined ? it.skip : it)(
"should be quarantined",
() => {
- throw new Error();
+ throw new Error("quarantined test failed");
}
);
});
diff --git a/packages/jest-plugin/test/integration/jest.config.js b/packages/jest-plugin/test/integration/jest.config.js
index e7329eb..ceb5c34 100644
--- a/packages/jest-plugin/test/integration/jest.config.js
+++ b/packages/jest-plugin/test/integration/jest.config.js
@@ -1,16 +1,20 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
module.exports = {
+ setupFilesAfterEnv: ["./src/matchers.ts"],
testEnvironment: "node",
transform: {
"^.+\\.[jt]s$": [
"babel-jest",
{
- configFile: "../../../../babel.config.js",
+ configFile: "../babel.config.js",
},
],
},
+ // NB: This should be greater than TEST_TIMEOUT_MS used by the watchdog in runTestCase().
+ testTimeout: 120000,
+
verbose: true,
};
diff --git a/packages/jest-plugin/test/integration/package.json b/packages/jest-plugin/test/integration/package.json
index 5a1e554..26d5f5a 100644
--- a/packages/jest-plugin/test/integration/package.json
+++ b/packages/jest-plugin/test/integration/package.json
@@ -2,19 +2,28 @@
"name": "jest-integration",
"private": true,
"devDependencies": {
+ "@babel/core": "^7.22.9",
+ "@babel/preset-env": "^7.22.9",
+ "@babel/preset-typescript": "^7.22.5",
+ "@jest/expect-utils": "25.1.0 - 29",
"@types/temp": "^0.9.1",
"@unflakable/jest-plugin": "workspace:^",
"@unflakable/js-api": "workspace:^",
- "cross-env": "^7.0.3",
- "deep-equal": "^2.0.5",
- "fetch-mock-jest": "^1.5.1",
+ "es6-promisify": "^7.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "expect": "25.1.0 - 29",
"jest": "25.1.0 - 29",
- "jest-cli": "25.1.0 - 29",
"jest-environment-node": "25.1.0 - 29",
- "temp": "^0.9.4"
+ "jest-get-type": "25.1.0 - 29",
+ "jest-matcher-utils": "25.1.0 - 29",
+ "mockttp": "^3.9.2",
+ "semver": "^7.5.4",
+ "tmp": "^0.2.1",
+ "typescript": "^4.9.5",
+ "unflakable-test-common": "workspace:^"
},
"scripts": {
- "test": "cross-env NODE_OPTIONS=\"--require ./src/force-color.js\" jest --useStderr --verbose",
+ "test": "jest --useStderr --verbose",
"typecheck": "tsc --build"
}
}
diff --git a/packages/jest-plugin/test/integration/src/basic.test.ts b/packages/jest-plugin/test/integration/src/basic.test.ts
index 7047ad9..c9427b3 100644
--- a/packages/jest-plugin/test/integration/src/basic.test.ts
+++ b/packages/jest-plugin/test/integration/src/basic.test.ts
@@ -1,83 +1,105 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import {
defaultExpectedResults,
integrationTest,
integrationTestSuite,
-} from "./common";
+} from "./test-wrappers";
-integrationTestSuite(() => {
- it("environment variables config + git auto-detect", () =>
- integrationTest({
- params: {},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-
- it("quarantine flaky test", () =>
- integrationTest({
- params: {
- quarantineFlake: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 3,
- failedTests: 2,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 2,
- quarantinedTests: 4,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
+integrationTestSuite((mockBackend) => {
+ it("environment variables config + git auto-detect", (done) =>
+ integrationTest(
+ {
+ params: {},
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- }));
+ mockBackend,
+ done
+ ));
- it("skip failures", () =>
- integrationTest({
- params: {
- skipFailures: true,
+ it("quarantine flaky test", (done) =>
+ integrationTest(
+ {
+ params: {
+ quarantineFlake: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 3,
+ failedTests: 2,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 2,
+ quarantinedTests: 4,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
},
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 1,
- failedTests: 0,
- flakyTests: 2,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 2,
- quarantinedTests: 2,
- skippedSuites: 1,
- skippedTests: 2,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
- },
- }));
+ mockBackend,
+ done
+ ));
- it("run should succeed when skipping failures and quarantining flaky test", () =>
- integrationTest({
- params: {
- quarantineFlake: true,
- skipFailures: true,
+ it("skip failures", (done) =>
+ integrationTest(
+ {
+ params: {
+ skipFailures: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 1,
+ failedTests: 0,
+ flakyTests: 2,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 2,
+ quarantinedTests: 2,
+ skippedSuites: 1,
+ skippedTests: 2,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
},
- expectedExitCode: 0,
- expectedResults: {
- failedSuites: 0,
- failedTests: 0,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 3,
- quarantinedTests: 4,
- skippedSuites: 1,
- skippedTests: 2,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
+ mockBackend,
+ done
+ ));
+
+ it("run should succeed when skipping failures and quarantining flaky test", (done) =>
+ integrationTest(
+ {
+ params: {
+ quarantineFlake: true,
+ skipFailures: true,
+ },
+ expectedExitCode: 0,
+ expectedResults: {
+ failedSuites: 0,
+ failedTests: 0,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 3,
+ quarantinedTests: 4,
+ skippedSuites: 1,
+ skippedTests: 2,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
},
- }));
+ mockBackend,
+ done
+ ));
});
diff --git a/packages/jest-plugin/test/integration/src/common.ts b/packages/jest-plugin/test/integration/src/common.ts
deleted file mode 100644
index 5f6cf8c..0000000
--- a/packages/jest-plugin/test/integration/src/common.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import mockFetchJest from "fetch-mock-jest";
-import { ResultCounts, runTestCase, TestCaseParams } from "./runTestCase";
-import * as cosmiconfig from "cosmiconfig";
-import type { OptionsSync } from "cosmiconfig";
-import { FetchMockSandbox, MockCall } from "fetch-mock";
-import jestPackage from "jest/package.json";
-
-const throwUnimplemented = (): never => {
- throw new Error("unimplemented");
-};
-
-// Mocking `fs` is brittle, and making actual filesystem modifications to, e.g., package.json, can
-// leave around artifacts that produce hard-to-debug side effects. Instead, we directly mock
-// the cosmiconfig package that the jest-plugin uses for reading its config and hope that it's
-// being used correctly. We still test one case of the actual implementation through dogfooding.
-const mockConfigExplorer: ReturnType = {
- clearCaches: jest.fn(throwUnimplemented),
- clearLoadCache: jest.fn(throwUnimplemented),
- clearSearchCache: jest.fn(throwUnimplemented),
- load: jest.fn(throwUnimplemented),
- search: jest.fn(throwUnimplemented),
-};
-const mockCosmiconfig: typeof cosmiconfig = {
- ...jest.requireActual("cosmiconfig"),
- cosmiconfigSync: jest.fn((moduleName: string, options?: OptionsSync) => {
- 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 mockConfigExplorer;
- }),
-};
-jest.mock("cosmiconfig", () => mockCosmiconfig);
-
-// Jest calls exit() (the `exit` NPM package, not process.exit directly) if our custom reporter or
-// runners throw an exception. This in turn causes the whole test run to exit rather than reporting
-// the specific test as a failure. Instead, we mock exit() to log a message and continue execution.
-const mockExit = jest.fn();
-jest.mock("exit", () => mockExit);
-
-jest.mock("node-fetch", () => mockFetchJest.sandbox());
-
-jest.mock("simple-git");
-jest.setTimeout(30000);
-
-const originalStderrWrite = process.stderr.write.bind(process.stderr);
-
-export type TestCase = {
- params: Partial;
- expectedExitCode: number;
- expectedResults: ResultCounts;
-};
-
-export const defaultExpectedResults: ResultCounts = {
- failedSuites: 4,
- failedTests: 2,
- flakyTests: 2,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 1,
- quarantinedTests: 2,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
-};
-
-export const integrationTest = async (testCase: TestCase): Promise => {
- const mockFetch = jest.requireMock<
- jest.MockInstance & FetchMockSandbox
- >("node-fetch");
- await runTestCase(
- {
- config: null,
- expectedApiKey: "MOCK_API_KEY",
- expectedBranch: "MOCK_BRANCH",
- expectedCommit: "MOCK_COMMIT",
- expectedFailureRetries: 2,
- expectedFlakeTestNameSuffix: "",
- expectedSuiteId: "MOCK_SUITE_ID",
- expectPluginToBeEnabled: true,
- expectResultsToBeUploaded: true,
- expectQuarantinedTestsToBeQuarantined: true,
- expectQuarantinedTestsToBeSkipped: false,
- expectSnapshots: 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,
- },
- quarantineFlake: false,
- skipFailures: false,
- skipFlake: false,
- skipQuarantined: false,
- testNamePattern: undefined,
- ...testCase.params,
- envVars: {
- UNFLAKABLE_API_KEY: "MOCK_API_KEY",
- UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID",
- ...testCase.params.envVars,
- },
- },
- testCase.expectedExitCode,
- testCase.expectedResults,
- mockConfigExplorer,
- mockExit,
- mockFetch
- );
-};
-
-export const integrationTestSuite = (runTests: () => void): void => {
- beforeEach(() => {
- (mockConfigExplorer.search as jest.Mock).mockClear();
- mockExit.mockClear();
-
- jest
- .requireMock("node-fetch")
- .reset()
- .catch((url, request) => {
- throw new Error(
- `Unexpected ${
- request.method?.toUpperCase().toString() ?? "undefined"
- } request to ${url} ${
- request.body !== null && request.body !== undefined
- ? `with body ${request.body.toString()}`
- : "without body"
- } and headers ${JSON.stringify(request.headers ?? {})}`
- );
- });
-
- // Don't propagate environment variables from the calling environment to the underlying tests,
- // which can lead to different results across environments and leak state between tests that
- // manipulate the environment.
- process.env = {
- NODE_ENV: "test",
- FORCE_COLOR: "3",
- };
-
- let elapsedMs = 0;
- Date.now = jest.fn(() => {
- const date =
- new Date(Date.UTC(2022, 0, 23, 4, 5, 6, 789)).valueOf() + elapsedMs;
- elapsedMs += 200;
- return date;
- });
-
- // Restore original.
- process.stderr.write = originalStderrWrite;
- });
-
- const jestMinorVersion = jestPackage.version.match(/^[^.]+\.[^.]+/);
- const nodeMajorVersion = process.version.match(/^[^.]+/);
-
- describe(`Jest ${
- jestMinorVersion !== null ? jestMinorVersion[0] : jestPackage.version
- }`, () => {
- // Only use Node major version for test name.
- describe(`Node ${
- nodeMajorVersion !== null ? nodeMajorVersion[0] : process.version
- }`, () => {
- runTests();
- });
- });
-};
diff --git a/packages/jest-plugin/test/integration/src/config.test.ts b/packages/jest-plugin/test/integration/src/config.test.ts
index 3140496..59976d0 100644
--- a/packages/jest-plugin/test/integration/src/config.test.ts
+++ b/packages/jest-plugin/test/integration/src/config.test.ts
@@ -1,51 +1,63 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import {
defaultExpectedResults,
integrationTest,
integrationTestSuite,
-} from "./common";
+} from "./test-wrappers";
-integrationTestSuite(() => {
- it("set test suite ID via environment", () =>
- integrationTest({
- params: {
- envVars: {
- UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID_ENV",
+integrationTestSuite((mockBackend) => {
+ it("set test suite ID via environment", (done) =>
+ integrationTest(
+ {
+ params: {
+ envVars: {
+ UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID_ENV",
+ },
+ expectedSuiteId: "MOCK_SUITE_ID_ENV",
},
- expectedSuiteId: "MOCK_SUITE_ID_ENV",
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
- it("set test suite ID via config", () =>
- integrationTest({
- params: {
- config: {
- testSuiteId: "MOCK_SUITE_ID_CONFIG",
+ 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",
},
- envVars: {
- UNFLAKABLE_SUITE_ID: undefined,
- },
- expectedSuiteId: "MOCK_SUITE_ID_CONFIG",
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
- it("set test suite ID via environment (override config)", () =>
- integrationTest({
- params: {
- config: {
- testSuiteId: "MOCK_SUITE_ID_CONFIG",
- },
- envVars: {
- UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID_ENV",
+ it("set test suite ID via environment (override config)", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ testSuiteId: "MOCK_SUITE_ID_CONFIG",
+ },
+ envVars: {
+ UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID_ENV",
+ },
+ expectedSuiteId: "MOCK_SUITE_ID_ENV",
},
- expectedSuiteId: "MOCK_SUITE_ID_ENV",
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
});
diff --git a/packages/jest-plugin/test/integration/src/disable-plugin.test.ts b/packages/jest-plugin/test/integration/src/disable-plugin.test.ts
new file mode 100644
index 0000000..d70dd1a
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/disable-plugin.test.ts
@@ -0,0 +1,90 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("disable plugin via config", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ enabled: false,
+ },
+ expectPluginToBeEnabled: false,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 5,
+ failedTests: 6,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("disable plugin via environment", (done) =>
+ integrationTest(
+ {
+ params: {
+ envVars: {
+ UNFLAKABLE_ENABLED: "false",
+ },
+ expectPluginToBeEnabled: false,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 5,
+ failedTests: 6,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("enable plugin via environment (override config)", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ enabled: false,
+ },
+ envVars: {
+ UNFLAKABLE_ENABLED: "true",
+ },
+ expectPluginToBeEnabled: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/disable-upload.test.ts b/packages/jest-plugin/test/integration/src/disable-upload.test.ts
new file mode 100644
index 0000000..59f680f
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/disable-upload.test.ts
@@ -0,0 +1,59 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("disable upload via config", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ uploadResults: false,
+ },
+ expectResultsToBeUploaded: false,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+
+ it("disable upload via environment", (done) =>
+ integrationTest(
+ {
+ params: {
+ envVars: {
+ UNFLAKABLE_UPLOAD_RESULTS: "false",
+ },
+ expectResultsToBeUploaded: false,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+
+ it("enable upload via environment (override config)", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ uploadResults: false,
+ },
+ envVars: {
+ UNFLAKABLE_UPLOAD_RESULTS: "true",
+ },
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/disablePlugin.test.ts b/packages/jest-plugin/test/integration/src/disablePlugin.test.ts
deleted file mode 100644
index 6d2a341..0000000
--- a/packages/jest-plugin/test/integration/src/disablePlugin.test.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import {
- defaultExpectedResults,
- integrationTest,
- integrationTestSuite,
-} from "./common";
-
-integrationTestSuite(() => {
- it("disable plugin via config", () =>
- integrationTest({
- params: {
- config: {
- enabled: false,
- },
- expectPluginToBeEnabled: false,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 5,
- failedTests: 6,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
- },
- }));
-
- it("disable plugin via environment", () =>
- integrationTest({
- params: {
- envVars: {
- UNFLAKABLE_ENABLED: "false",
- },
- expectPluginToBeEnabled: false,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 5,
- failedTests: 6,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
- },
- }));
-
- it("enable plugin via environment (override config)", () =>
- integrationTest({
- params: {
- config: {
- enabled: false,
- },
- envVars: {
- UNFLAKABLE_ENABLED: "true",
- },
- expectPluginToBeEnabled: true,
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/disableUpload.test.ts b/packages/jest-plugin/test/integration/src/disableUpload.test.ts
deleted file mode 100644
index 3e2253e..0000000
--- a/packages/jest-plugin/test/integration/src/disableUpload.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import {
- defaultExpectedResults,
- integrationTest,
- integrationTestSuite,
-} from "./common";
-
-integrationTestSuite(() => {
- it("disable upload via config", () =>
- integrationTest({
- params: {
- config: {
- uploadResults: false,
- },
- expectResultsToBeUploaded: false,
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-
- it("disable upload via environment", () =>
- integrationTest({
- params: {
- envVars: {
- UNFLAKABLE_UPLOAD_RESULTS: "false",
- },
- expectResultsToBeUploaded: false,
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-
- it("enable upload via environment (override config)", () =>
- integrationTest({
- params: {
- config: {
- uploadResults: false,
- },
- envVars: {
- UNFLAKABLE_UPLOAD_RESULTS: "true",
- },
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/force-color.js b/packages/jest-plugin/test/integration/src/force-color.js
index 9807cd1..0978afe 100644
--- a/packages/jest-plugin/test/integration/src/force-color.js
+++ b/packages/jest-plugin/test/integration/src/force-color.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// Jest changes FORCE_COLOR to 1 when it forks child processes. We execute this script with
// `node --require` so that each subprocess has the correct FORCE_COLOR value to produce the color
diff --git a/packages/jest-plugin/test/integration/src/git.test.ts b/packages/jest-plugin/test/integration/src/git.test.ts
index f6786d1..62c3d4f 100644
--- a/packages/jest-plugin/test/integration/src/git.test.ts
+++ b/packages/jest-plugin/test/integration/src/git.test.ts
@@ -1,77 +1,99 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+import path from "path";
import {
defaultExpectedResults,
integrationTest,
integrationTestSuite,
-} from "./common";
+} from "./test-wrappers";
-integrationTestSuite(() => {
- it("no git repo", () =>
- integrationTest({
- params: {
- expectedBranch: undefined,
- expectedCommit: undefined,
- git: {
- isRepo: false,
+integrationTestSuite((mockBackend) => {
+ it("no git repo", (done) =>
+ integrationTest(
+ {
+ params: {
+ expectedBranch: undefined,
+ expectedCommit: undefined,
+ // Without a repo, paths are relative to the Jest rootDir.
+ expectedRepoRelativePathPrefix: "",
+ git: {
+ isRepo: false,
+ },
},
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
// This tests the environment present in GitHub Actions for a `pull_request` event.
- it("git repo with detached HEAD", () =>
- 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",
+ 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",
},
- expectedCommit: "MOCK_PR_COMMIT",
- expectedBranch: "pull/MOCK_PR_NUMBER/merge",
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
- it("read branch/commit from environment", () =>
- integrationTest({
- params: {
- envVars: {
- UNFLAKABLE_BRANCH: "MOCK_BRANCH2",
- UNFLAKABLE_COMMIT: "MOCK_COMMIT2",
+ it("read branch/commit from environment", (done) =>
+ integrationTest(
+ {
+ params: {
+ envVars: {
+ UNFLAKABLE_BRANCH: "MOCK_BRANCH2",
+ UNFLAKABLE_COMMIT: "MOCK_COMMIT2",
+ },
+ expectedBranch: "MOCK_BRANCH2",
+ expectedCommit: "MOCK_COMMIT2",
},
- expectedBranch: "MOCK_BRANCH2",
- expectedCommit: "MOCK_COMMIT2",
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
- it("disable git auto-detection", () =>
- integrationTest({
- params: {
- config: {
- gitAutoDetect: false,
+ it("disable git auto-detection", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ gitAutoDetect: false,
+ },
+ expectedBranch: undefined,
+ expectedCommit: undefined,
+ // Without a repo, paths are relative to the Jest rootDir.
+ expectedRepoRelativePathPrefix: "",
},
- expectedBranch: undefined,
- expectedCommit: undefined,
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
});
diff --git a/packages/jest-plugin/test/integration/src/ignore-failures.test.ts b/packages/jest-plugin/test/integration/src/ignore-failures.test.ts
new file mode 100644
index 0000000..e8263b7
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/ignore-failures.test.ts
@@ -0,0 +1,24 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("set quarantineMode to ignore_failures explicitly", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ quarantineMode: "ignore_failures",
+ },
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/ignoreFailures.test.ts b/packages/jest-plugin/test/integration/src/ignoreFailures.test.ts
deleted file mode 100644
index d666d16..0000000
--- a/packages/jest-plugin/test/integration/src/ignoreFailures.test.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import {
- defaultExpectedResults,
- integrationTest,
- integrationTestSuite,
-} from "./common";
-
-integrationTestSuite(() => {
- it("set quarantineMode to ignore_failures explicitly", () =>
- integrationTest({
- params: {
- config: {
- quarantineMode: "ignore_failures",
- },
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/long-names.test.ts b/packages/jest-plugin/test/integration/src/long-names.test.ts
new file mode 100644
index 0000000..8284f15
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/long-names.test.ts
@@ -0,0 +1,58 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("test names longer than 4096 chars should be truncated", (done) =>
+ integrationTest(
+ {
+ params: {
+ expectedFlakeTestNameSuffix: "*".repeat(4096),
+ envVars: {
+ FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
+ },
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+
+ // Include an emoji here for good measure for our dogfooding.
+ it("quarantining should work for tests with names longer than 4096 chars 😅", (done) =>
+ integrationTest(
+ {
+ params: {
+ expectedFlakeTestNameSuffix: "*".repeat(4096),
+ envVars: {
+ FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
+ },
+ quarantineFlake: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 3,
+ failedTests: 2,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 2,
+ quarantinedTests: 4,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/longNames.test.ts b/packages/jest-plugin/test/integration/src/longNames.test.ts
deleted file mode 100644
index a95265b..0000000
--- a/packages/jest-plugin/test/integration/src/longNames.test.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import {
- defaultExpectedResults,
- integrationTest,
- integrationTestSuite,
-} from "./common";
-
-integrationTestSuite(() => {
- it("test names longer than 4096 chars should be truncated", () =>
- integrationTest({
- params: {
- expectedFlakeTestNameSuffix: "*".repeat(4096),
- envVars: {
- FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
- },
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-
- // Include an emoji here for good measure for our dogfooding.
- it("quarantining should work for tests with names longer than 4096 chars 😅", () =>
- integrationTest({
- params: {
- expectedFlakeTestNameSuffix: "*".repeat(4096),
- envVars: {
- FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
- },
- quarantineFlake: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 3,
- failedTests: 2,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 2,
- quarantinedTests: 4,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
- },
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/matchers.ts b/packages/jest-plugin/test/integration/src/matchers.ts
new file mode 100644
index 0000000..be025b9
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/matchers.ts
@@ -0,0 +1,101 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import {
+ MatcherHintOptions,
+ matcherErrorMessage,
+ matcherHint,
+ RECEIVED_COLOR,
+ printWithType,
+ printReceived,
+ getLabelPrinter,
+ printExpected,
+ INVERTED_COLOR,
+ stringify,
+} from "jest-matcher-utils";
+import type { SyncExpectationResult } from "expect";
+import { equals, iterableEquality } from "@jest/expect-utils";
+import * as getType from "jest-get-type";
+
+function toContainEqualTimes(
+ this: jest.MatcherContext,
+ received: Array | Set,
+ expected: unknown,
+ times: number
+): SyncExpectationResult {
+ const matcherName = "toContainEqualTimes";
+ const isNot = this.isNot;
+ const options: MatcherHintOptions = {
+ comment: "deep equality",
+ isNot,
+ promise: this.promise,
+ };
+
+ if (received === null) {
+ throw new Error(
+ matcherErrorMessage(
+ matcherHint(matcherName, undefined, undefined, options),
+ `${RECEIVED_COLOR("received")} value must not be null nor undefined`,
+ printWithType("Received", received, printReceived)
+ )
+ );
+ }
+
+ const matchIndices = Array.from(received).reduce(
+ (matchIndices: number[], item: unknown, index: number) =>
+ equals(item, expected, [...(this.customTesters ?? []), iterableEquality])
+ ? [...matchIndices, index]
+ : matchIndices,
+ []
+ );
+
+ const pass = matchIndices.length === times;
+
+ const message = (): string => {
+ const labelExpected = `Expected value ${times} time${
+ times !== 1 ? "s" : ""
+ }`;
+ const labelReceived = `Received ${(getType.getType ?? getType)(
+ received
+ )} with ${matchIndices.length} match${
+ matchIndices.length !== 1 ? "es" : ""
+ }`;
+ const printLabel = getLabelPrinter(labelExpected, labelReceived);
+
+ return (
+ matcherHint(matcherName, undefined, undefined, options) +
+ "\n\n" +
+ `${printLabel(labelExpected)}${
+ isNot === true ? "not " : ""
+ }${printExpected(expected)}\n` +
+ `${printLabel(labelReceived)}${isNot === true ? " " : ""}${
+ isNot === true && Array.isArray(received)
+ ? RECEIVED_COLOR(
+ `[${received
+ .map((item, i) => {
+ const stringified = stringify(item);
+ return matchIndices.includes(i)
+ ? INVERTED_COLOR(stringified)
+ : stringified;
+ })
+ .join(", ")}]`
+ )
+ : printReceived(received)
+ }`
+ );
+ };
+
+ return { message, pass };
+}
+
+expect.extend({
+ toContainEqualTimes,
+});
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace jest {
+ interface Matchers {
+ toContainEqualTimes(expected: unknown, times: number): R;
+ }
+ }
+}
diff --git a/packages/jest-plugin/test/integration/src/no-quarantine.test.ts b/packages/jest-plugin/test/integration/src/no-quarantine.test.ts
new file mode 100644
index 0000000..4d909b6
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/no-quarantine.test.ts
@@ -0,0 +1,36 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import { integrationTest, integrationTestSuite } from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("set quarantineMode to no_quarantine", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ quarantineMode: "no_quarantine",
+ },
+ expectQuarantinedTestsToBeQuarantined: false,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 5,
+ failedTests: 4,
+ flakyTests: 2,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/noQuarantine.test.ts b/packages/jest-plugin/test/integration/src/noQuarantine.test.ts
deleted file mode 100644
index 379eee0..0000000
--- a/packages/jest-plugin/test/integration/src/noQuarantine.test.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import { integrationTest, integrationTestSuite } from "./common";
-
-integrationTestSuite(() => {
- it("set quarantineMode to no_quarantine", () =>
- integrationTest({
- params: {
- config: {
- quarantineMode: "no_quarantine",
- },
- expectQuarantinedTestsToBeQuarantined: false,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 5,
- failedTests: 4,
- flakyTests: 2,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
- },
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/plugin-failures.test.ts b/packages/jest-plugin/test/integration/src/plugin-failures.test.ts
new file mode 100644
index 0000000..07ec795
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/plugin-failures.test.ts
@@ -0,0 +1,82 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("run should not fail due to error fetching manifest", (done) =>
+ integrationTest(
+ {
+ params: {
+ failToFetchManifest: true,
+ skipFailures: true,
+ skipFlake: true,
+ skipQuarantined: true,
+ },
+ expectedExitCode: 0,
+ expectedResults: {
+ failedSuites: 0,
+ failedTests: 0,
+ flakyTests: 0,
+ passedSuites: 2,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 3,
+ skippedTests: 6,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("reporter should print results even if upload fails", (done) =>
+ integrationTest(
+ {
+ params: {
+ failToUploadResults: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+
+ it("reporter should print results even if both manifest fetch and upload fail", (done) =>
+ integrationTest(
+ {
+ params: {
+ failToFetchManifest: true,
+ failToUploadResults: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 5,
+ failedTests: 4,
+ flakyTests: 2,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/pluginFailures.test.ts b/packages/jest-plugin/test/integration/src/pluginFailures.test.ts
deleted file mode 100644
index 32634b8..0000000
--- a/packages/jest-plugin/test/integration/src/pluginFailures.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import {
- defaultExpectedResults,
- integrationTest,
- integrationTestSuite,
-} from "./common";
-
-integrationTestSuite(() => {
- it("run should not fail due to error fetching manifest", () =>
- integrationTest({
- params: {
- failToFetchManifest: true,
- skipFailures: true,
- skipFlake: true,
- skipQuarantined: true,
- },
- expectedExitCode: 0,
- expectedResults: {
- failedSuites: 0,
- failedTests: 0,
- flakyTests: 0,
- passedSuites: 2,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 3,
- skippedTests: 6,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-
- it("reporter should print results even if upload fails", () =>
- integrationTest({
- params: {
- failToUploadResults: true,
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-
- it("reporter should print results even if both manifest fetch and upload fail", () =>
- integrationTest({
- params: {
- failToFetchManifest: true,
- failToUploadResults: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 5,
- failedTests: 4,
- flakyTests: 2,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
- },
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/retries.test.ts b/packages/jest-plugin/test/integration/src/retries.test.ts
index 10f6c9c..972ca92 100644
--- a/packages/jest-plugin/test/integration/src/retries.test.ts
+++ b/packages/jest-plugin/test/integration/src/retries.test.ts
@@ -1,46 +1,56 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import {
defaultExpectedResults,
integrationTest,
integrationTestSuite,
-} from "./common";
+} from "./test-wrappers";
-integrationTestSuite(() => {
- it("disable failure retries", () =>
- integrationTest({
- params: {
- config: {
- failureRetries: 0,
+integrationTestSuite((mockBackend) => {
+ it("disable failure retries", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ failureRetries: 0,
+ },
+ expectedFailureRetries: 0,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 4,
+ failedTests: 4,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 1,
+ quarantinedTests: 2,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
},
- expectedFailureRetries: 0,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 4,
- failedTests: 4,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 1,
- quarantinedTests: 2,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 1,
- failedSnapshots: 0,
- totalSnapshots: 1,
},
- }));
+ mockBackend,
+ done
+ ));
- it("set failure retries to 1", () =>
- integrationTest({
- params: {
- config: {
- failureRetries: 1,
+ it("set failure retries to 1", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ failureRetries: 1,
+ },
+ expectedFailureRetries: 1,
},
- expectedFailureRetries: 1,
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
},
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
+ mockBackend,
+ done
+ ));
});
diff --git a/packages/jest-plugin/test/integration/src/run-test-case.ts b/packages/jest-plugin/test/integration/src/run-test-case.ts
new file mode 100644
index 0000000..d27fd17
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/run-test-case.ts
@@ -0,0 +1,616 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import { tmpName, TmpNameOptions } from "tmp";
+import {
+ CreateTestSuiteRunInlineRequest,
+ TEST_NAME_ENTRY_MAX_LENGTH,
+ TestRunRecord,
+ TestSuiteManifest,
+} from "@unflakable/js-api";
+import { gunzipSync } from "zlib";
+import { UnflakableConfig } from "@unflakable/plugins-common";
+import { verifyOutput } from "./verify-output";
+import {
+ CONFIG_MOCK_ENV_VAR,
+ CosmiconfigMockParams,
+} from "unflakable-test-common/dist/config";
+import path from "path";
+import { promisify } from "es6-promisify";
+import { execFile } from "child_process";
+import {
+ MockBackend,
+ UnmatchedEndpoints,
+} from "unflakable-test-common/dist/mock-backend";
+import { CompletedRequest } from "mockttp";
+import { GIT_MOCK_ENV_VAR } from "unflakable-test-common/dist/git";
+import {
+ AsyncTestError,
+ spawnTestWithTimeout,
+} from "unflakable-test-common/dist/spawn";
+import * as util from "util";
+import * as fs from "fs/promises";
+
+// Jest times out after 120 seconds, so we bail early here to allow time to print the
+// captured output before Jest kills the test.
+const TEST_TIMEOUT_MS = 110000;
+
+const userAgentRegex = new RegExp(
+ "unflakable-js-api/(?:[-0-9.]|alpha|beta)+ unflakable-jest-plugin/(?:[-0-9.]|alpha|beta)+ \\(Jest [0-9]+\\.[0-9]+\\.[0-9]+; Node v[0-9]+\\.[0-9]+\\.[0-9]\\)"
+);
+
+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 type TestCaseParams = {
+ config: Partial | null;
+ configJs: string | null;
+ envVars: { [key in string]: string | undefined };
+ expectedApiKey: string;
+ expectedBranch: string | undefined;
+ expectedCommit: string | undefined;
+ expectedFailureRetries: number;
+ expectedFlakeTestNameSuffix: string;
+ expectedSuiteId: string;
+ expectedRepoRelativePathPrefix: string;
+ expectFailuresToBeTestIndependent: boolean;
+ expectFailuresFirstAttemptToBeTestIndependent: boolean;
+ expectFlakeToBeTestIndependent: boolean;
+ expectFlakeFirstAttemptToBeTestIndependent: boolean;
+ expectPluginToBeEnabled: boolean;
+ expectResultsToBeUploaded: boolean;
+ expectQuarantinedTestsToBeQuarantined: boolean;
+ expectQuarantinedTestsToBeSkipped: boolean;
+ expectQuarantinedTestsToBeTestIndependent: boolean;
+ expectQuarantinedTestsFirstAttemptToBeTestIndependent: boolean;
+ expectSnapshots: boolean;
+ failToFetchManifest: boolean;
+ failToUploadResults: boolean;
+ flakeFailCount: number;
+ git: SimpleGitMockParams;
+ quarantineFlake: boolean;
+ skipFailures: boolean;
+ skipFlake: boolean;
+ skipQuarantined: boolean;
+ testNamePattern: string | undefined;
+};
+
+const specRepoPath = (params: TestCaseParams, specNameStub: string): string =>
+ `${params.expectedRepoRelativePathPrefix}src/${specNameStub}.test.ts`;
+
+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$/;
+
+export type ResultCounts = {
+ passedSuites: number;
+ passedSuitesWithIndependentFailures: number;
+ passedTests: number;
+ passedTestsWithIndependentFailures: number;
+ failedSuites: number;
+ failedTests: number;
+ flakyTests: number;
+ quarantinedSuites: number;
+ quarantinedTests: number;
+ skippedSuites: number;
+ skippedTests: number;
+ passedSnapshots: number;
+ failedSnapshots: number;
+ totalSnapshots: number;
+};
+
+const verifyUploadResults = (
+ params: TestCaseParams,
+ expectedResults: ResultCounts,
+ request: CompletedRequest
+): void => {
+ const {
+ expectFailuresFirstAttemptToBeTestIndependent,
+ expectFailuresToBeTestIndependent,
+ expectFlakeFirstAttemptToBeTestIndependent,
+ expectFlakeToBeTestIndependent,
+ expectQuarantinedTestsFirstAttemptToBeTestIndependent,
+ expectQuarantinedTestsToBeQuarantined,
+ expectQuarantinedTestsToBeSkipped,
+ expectQuarantinedTestsToBeTestIndependent,
+ expectedBranch,
+ expectedCommit,
+ expectedFailureRetries,
+ expectedFlakeTestNameSuffix,
+ failToFetchManifest,
+ flakeFailCount,
+ quarantineFlake,
+ skipFailures,
+ skipFlake,
+ skipQuarantined,
+ testNamePattern,
+ } = params;
+
+ const parsedBody = JSON.parse(
+ gunzipSync(request.body.buffer).toString()
+ ) as CreateTestSuiteRunInlineRequest;
+
+ expect(request.headers["user-agent"]).toMatch(userAgentRegex);
+
+ const testNamePatternRegex =
+ testNamePattern !== undefined ? new RegExp(testNamePattern) : undefined;
+
+ 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).toEqual({
+ ...(expectedBranch !== undefined
+ ? {
+ branch: expectedBranch,
+ }
+ : {}),
+ ...(expectedCommit !== undefined
+ ? {
+ commit: expectedCommit,
+ }
+ : {}),
+ start_time: expect.stringMatching(TIMESTAMP_REGEX) as string,
+ end_time: expect.stringMatching(TIMESTAMP_REGEX) as string,
+ test_runs: expect.arrayContaining(
+ [
+ ...(skipFailures
+ ? ([] as TestRunRecord[])
+ : ([
+ {
+ filename: specRepoPath(params, "fail"),
+ name: ["describe block", "should ([escape regex]?.*$ fail"],
+ attempts: Array(expectedFailureRetries + 1)
+ .fill(0)
+ .map((_, idx) => ({
+ duration_ms: expect.any(Number) as number,
+ result: "fail",
+ ...(expectFailuresToBeTestIndependent ||
+ (expectFailuresFirstAttemptToBeTestIndependent && idx === 0)
+ ? {
+ failure_reason: "independent",
+ }
+ : {}),
+ })),
+ },
+ ] as TestRunRecord[])),
+ ...[1, 2].flatMap((i) =>
+ skipFlake ||
+ (expectQuarantinedTestsToBeSkipped &&
+ quarantineFlake &&
+ !failToFetchManifest) ||
+ (testNamePattern !== undefined &&
+ `should be flaky ${i}${expectedFlakeTestNameSuffix}`.match(
+ testNamePattern
+ ) === null)
+ ? []
+ : [
+ {
+ filename: specRepoPath(params, "flake"),
+ name: [
+ `should be flaky ${i}${expectedFlakeTestNameSuffix}`.substring(
+ 0,
+ TEST_NAME_ENTRY_MAX_LENGTH
+ ),
+ ],
+ attempts: [
+ ...Array(flakeFailCount)
+ .fill(0)
+ .map((_, idx) => ({
+ duration_ms: expect.any(Number) as number,
+ result:
+ quarantineFlake &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ ...(expectFlakeToBeTestIndependent ||
+ (expectFlakeFirstAttemptToBeTestIndependent &&
+ idx === 0)
+ ? {
+ failure_reason: "independent",
+ }
+ : {}),
+ })),
+ ...(expectedFailureRetries > 0
+ ? [
+ {
+ duration_ms: expect.any(Number) as number,
+ result: "pass",
+ },
+ ]
+ : []),
+ ],
+ } as TestRunRecord,
+ ]
+ ),
+ ...(skipQuarantined ||
+ (expectQuarantinedTestsToBeSkipped && !failToFetchManifest) ||
+ (testNamePattern !== undefined &&
+ "mixed mixed: should be quarantined".match(testNamePattern) !== null)
+ ? []
+ : ([
+ {
+ filename: specRepoPath(params, "mixed"),
+ name: ["mixed", "mixed: should be quarantined"],
+ attempts: Array(expectedFailureRetries + 1)
+ .fill(0)
+ .map((_, idx) => ({
+ duration_ms: expect.any(Number) as number,
+ result:
+ failToFetchManifest ||
+ !expectQuarantinedTestsToBeQuarantined
+ ? "fail"
+ : "quarantined",
+ ...(expectQuarantinedTestsToBeTestIndependent ||
+ (expectQuarantinedTestsFirstAttemptToBeTestIndependent &&
+ idx === 0)
+ ? {
+ failure_reason: "independent",
+ }
+ : {}),
+ })),
+ },
+ ] as TestRunRecord[])),
+ ...(skipFailures ||
+ (testNamePattern !== undefined &&
+ "mixed mixed: should fail".match(testNamePattern) !== null)
+ ? []
+ : ([
+ {
+ filename: specRepoPath(params, "mixed"),
+ name: ["mixed", "mixed: should fail"],
+ attempts: Array(expectedFailureRetries + 1)
+ .fill(0)
+ .map((_, idx) => ({
+ duration_ms: expect.any(Number) as number,
+ result: "fail",
+ ...(expectFailuresToBeTestIndependent ||
+ (expectFailuresFirstAttemptToBeTestIndependent && idx === 0)
+ ? {
+ failure_reason: "independent",
+ }
+ : {}),
+ })),
+ },
+ ] as TestRunRecord[])),
+ ...(testNamePattern === undefined ||
+ "mixed mixed: should pass".match(testNamePattern) === null
+ ? [
+ {
+ filename: specRepoPath(params, "mixed"),
+ name: ["mixed", "mixed: should pass"],
+ attempts: [
+ {
+ duration_ms: expect.any(Number) as number,
+ result: "pass",
+ },
+ ],
+ } as TestRunRecord,
+ ]
+ : []),
+ ...(testNamePattern === undefined ||
+ "should pass".match(testNamePattern) !== null
+ ? [
+ {
+ filename: specRepoPath(params, "pass"),
+ name: ["should pass"],
+ attempts: [
+ {
+ duration_ms: expect.any(Number) as number,
+ result: "pass",
+ },
+ ],
+ } as TestRunRecord,
+ ]
+ : []),
+ ...(skipQuarantined ||
+ (expectQuarantinedTestsToBeSkipped && !failToFetchManifest) ||
+ (testNamePattern !== undefined &&
+ "describe block should be quarantined".match(testNamePattern) ===
+ null)
+ ? []
+ : ([
+ {
+ filename: specRepoPath(params, "quarantined"),
+ name: ["describe block", "should be quarantined"],
+ attempts: Array(expectedFailureRetries + 1)
+ .fill(0)
+ .map((_, idx) => ({
+ duration_ms: expect.any(Number) as number,
+ result:
+ failToFetchManifest ||
+ !expectQuarantinedTestsToBeQuarantined
+ ? "fail"
+ : "quarantined",
+ ...(expectQuarantinedTestsToBeTestIndependent ||
+ (expectQuarantinedTestsFirstAttemptToBeTestIndependent &&
+ idx === 0)
+ ? {
+ failure_reason: "independent",
+ }
+ : {}),
+ })),
+ },
+ ] as TestRunRecord[])),
+ ].filter(
+ (runRecord) =>
+ testNamePatternRegex === undefined ||
+ testNamePatternRegex.test(runRecord.name.join(" "))
+ )
+ ) as TestRunRecord[],
+ });
+ // Make sure there aren't any extra tests reported.
+ expect(parsedBody.test_runs).toHaveLength(
+ expectedResults.failedTests +
+ expectedResults.flakyTests +
+ expectedResults.quarantinedTests +
+ expectedResults.passedTests
+ );
+};
+
+const addBackendExpectations = async (
+ params: TestCaseParams,
+ expectedResults: ResultCounts,
+ mockBackend: MockBackend,
+ onError: (e: unknown) => void
+): Promise => {
+ const {
+ expectedApiKey,
+ expectedBranch,
+ expectedCommit,
+ expectedFlakeTestNameSuffix,
+ expectedSuiteId,
+ expectPluginToBeEnabled,
+ expectResultsToBeUploaded,
+ failToFetchManifest,
+ failToUploadResults,
+ quarantineFlake,
+ } = params;
+
+ const manifest: TestSuiteManifest = {
+ quarantined_tests: [
+ {
+ test_id: "TEST_QUARANTINED",
+ filename: specRepoPath(params, "quarantined"),
+ name: ["describe block", "should be quarantined"],
+ },
+ {
+ test_id: "TEST_QUARANTINED2",
+ filename: specRepoPath(params, "mixed"),
+ name: ["mixed", "mixed: should be quarantined"],
+ },
+ ...(quarantineFlake
+ ? [
+ {
+ test_id: "TEST_FLAKE",
+ filename: specRepoPath(params, "flake"),
+ name: [
+ `should be flaky 1${expectedFlakeTestNameSuffix}`.substring(
+ 0,
+ TEST_NAME_ENTRY_MAX_LENGTH
+ ),
+ ],
+ },
+ {
+ test_id: "TEST_FLAKE",
+ filename: specRepoPath(params, "flake"),
+ name: [
+ `should be flaky 2${expectedFlakeTestNameSuffix}`.substring(
+ 0,
+ TEST_NAME_ENTRY_MAX_LENGTH
+ ),
+ ],
+ },
+ ]
+ : []),
+ ],
+ };
+
+ return mockBackend.addExpectations(
+ onError,
+ failToFetchManifest ? null : manifest,
+ (request) => verifyUploadResults(params, expectedResults, request),
+ failToUploadResults
+ ? null
+ : {
+ run_id: MOCK_RUN_ID,
+ suite_id: expectedSuiteId,
+ ...(expectedBranch !== undefined
+ ? {
+ branch: expectedBranch,
+ }
+ : {}),
+ ...(expectedCommit !== undefined
+ ? {
+ commit: expectedCommit,
+ }
+ : {}),
+ },
+ userAgentRegex,
+ {
+ expectPluginToBeEnabled,
+ expectResultsToBeUploaded,
+ expectedApiKey,
+ expectedSuiteId,
+ }
+ );
+};
+
+export const runTestCase = async (
+ params: TestCaseParams,
+ expectedExitCode: number,
+ expectedResults: ResultCounts,
+ mockBackend: MockBackend
+): Promise => {
+ const {
+ flakeFailCount,
+ git,
+ skipFailures,
+ skipFlake,
+ skipQuarantined,
+ testNamePattern,
+ } = params;
+
+ const asyncTestError: AsyncTestError = { error: undefined };
+
+ const unmatchedRequestEndpoints = await addBackendExpectations(
+ params,
+ expectedResults,
+ mockBackend,
+ (error) => {
+ if (asyncTestError.error === undefined) {
+ asyncTestError.error = error ?? new Error("undefined error");
+ } else {
+ console.error("Multiple failed fetch expectations", error);
+ }
+ }
+ );
+
+ const configPath =
+ params.configJs !== null
+ ? (await promisify(tmpName)({
+ prefix: "unflakable-jest-integration-config",
+ })) + ".js"
+ : null;
+ if (configPath !== null) {
+ await fs.writeFile(
+ configPath,
+ Buffer.from(params.configJs as string, "utf8")
+ );
+ }
+
+ const integrationInputPath = path.join("..", "integration-input");
+ const configMockParams: CosmiconfigMockParams =
+ configPath !== null
+ ? {
+ expectedSearchFrom: path.resolve(integrationInputPath),
+ pathToLoad: configPath,
+ }
+ : {
+ expectedSearchFrom: path.resolve(integrationInputPath),
+ searchResult:
+ params.config !== null
+ ? {
+ config: params.config,
+ filepath: path.join(
+ "MOCK_BASE",
+ "packages",
+ "jest-plugin",
+ "test",
+ "integration-input",
+ "package.json"
+ ),
+ }
+ : null,
+ };
+
+ // We don't directly invoke `jest` 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 jestBin = (
+ await util.promisify(execFile)("yarn", ["bin", "jest"], {
+ cwd: integrationInputPath,
+ // yarn.CMD isn't executable without a shell on Windows.
+ shell: process.platform === "win32",
+ })
+ ).stdout.trimEnd();
+
+ const args = [
+ "--require",
+ require.resolve("./force-color.js"),
+ "--require",
+ require.resolve("unflakable-test-common/dist/mock-cosmiconfig"),
+ "--require",
+ require.resolve("unflakable-test-common/dist/mock-git"),
+ // Uncomment to enable debugger.
+ // "--inspect",
+ jestBin,
+ "--reporters",
+ "@unflakable/jest-plugin/dist/reporter",
+ "--runner",
+ "@unflakable/jest-plugin/dist/runner",
+ "--testRunner",
+ "@unflakable/jest-plugin/dist/test-runner",
+ ...(skipFailures
+ ? ["--testPathIgnorePatterns", "/src/invalid\\.test\\.ts"]
+ : []),
+ ...(testNamePattern !== undefined
+ ? ["--testNamePattern", testNamePattern]
+ : []),
+ ];
+
+ const env = {
+ ...params.envVars,
+ DEBUG: process.env.TEST_DEBUG,
+ // The flaky test needs external state to know when it's being retried so that it can pass.
+ FLAKY_TEST_TEMP: await promisify(tmpName)({
+ prefix: "unflakable-jest-integration-flaky-test",
+ }),
+ PATH: process.env.PATH,
+ UNFLAKABLE_API_BASE_URL: `http://localhost:${mockBackend.apiServerPort}`,
+ [CONFIG_MOCK_ENV_VAR]: JSON.stringify(configMockParams),
+ [GIT_MOCK_ENV_VAR]: JSON.stringify(git),
+ ...(skipFailures ? { SKIP_FAILURES: "1" } : {}),
+ ...(skipFlake ? { SKIP_FLAKE: "1" } : {}),
+ ...(skipQuarantined ? { SKIP_QUARANTINED: "1" } : {}),
+ FLAKE_FAIL_COUNT: flakeFailCount.toString(),
+ // Windows requires these environment variables to be propagated.
+ ...(process.platform === "win32"
+ ? {
+ APPDATA: process.env.APPDATA,
+ LOCALAPPDATA: process.env.LOCALAPPDATA,
+ TMP: process.env.TMP,
+ TEMP: process.env.TEMP,
+ }
+ : {}),
+ };
+
+ await spawnTestWithTimeout(
+ args,
+ env,
+ integrationInputPath,
+ TEST_TIMEOUT_MS,
+ async (_stdoutLines, stderrLines) => {
+ verifyOutput(
+ params,
+ stderrLines,
+ expectedResults,
+ mockBackend.apiServerPort
+ );
+ await mockBackend.checkExpectations(unmatchedRequestEndpoints);
+ },
+ expectedExitCode,
+ true,
+ asyncTestError
+ );
+};
diff --git a/packages/jest-plugin/test/integration/src/runTestCase.ts b/packages/jest-plugin/test/integration/src/runTestCase.ts
deleted file mode 100644
index 6d5afd6..0000000
--- a/packages/jest-plugin/test/integration/src/runTestCase.ts
+++ /dev/null
@@ -1,1396 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import * as temp from "temp";
-import {
- FetchMockSandbox,
- MockCall,
- MockRequest,
- MockResponse,
- MockMatcher,
-} from "fetch-mock";
-import { run } from "jest-cli";
-import escapeStringRegexp from "escape-string-regexp";
-import {
- CreateTestSuiteRunFromUploadRequest,
- CreateTestSuiteRunInlineRequest,
- TEST_NAME_ENTRY_MAX_LENGTH,
- TestAttemptResult,
- TestRunAttemptRecord,
- TestRunRecord,
- TestSuiteManifest,
- TestSuiteRunPendingSummary,
-} from "@unflakable/js-api";
-import { simpleGit, SimpleGit } from "simple-git";
-import type { Response as GitResponse, TaskOptions } from "simple-git";
-import deepEqual from "deep-equal";
-import * as cosmiconfig from "cosmiconfig";
-import { CosmiconfigResult } from "cosmiconfig/dist/types";
-import { gunzipSync } from "zlib";
-import { UnflakableConfig } from "@unflakable/plugins-common";
-
-const userAgentRegex = new RegExp(
- "unflakable-js-api/(?:[-0-9.]|alpha|beta)+ unflakable-jest-plugin/(?:[-0-9.]|alpha|beta)+ \\(Jest [0-9]+\\.[0-9]+\\.[0-9]+; Node v[0-9]+\\.[0-9]+\\.[0-9]\\)"
-);
-
-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[];
- };
-
-export type TestCaseParams = {
- config: Partial | null;
- envVars: { [key in string]: string | undefined };
- expectedApiKey: string;
- expectedBranch: string | undefined;
- expectedCommit: string | undefined;
- expectedFailureRetries: number;
- expectedFlakeTestNameSuffix: string;
- expectedSuiteId: string;
- expectPluginToBeEnabled: boolean;
- expectResultsToBeUploaded: boolean;
- expectQuarantinedTestsToBeQuarantined: boolean;
- expectQuarantinedTestsToBeSkipped: boolean;
- expectSnapshots: boolean;
- failToFetchManifest: boolean;
- failToUploadResults: boolean;
- git: SimpleGitMockParams;
- quarantineFlake: boolean;
- skipFailures: boolean;
- skipFlake: boolean;
- skipQuarantined: boolean;
- testNamePattern: string | undefined;
-};
-
-const originalStderrWrite = process.stderr.write.bind(process.stderr);
-
-const mockSimpleGit = (params: SimpleGitMockParams): void => {
- (simpleGit as jest.Mock).mockImplementationOnce(
- () =>
- ({
- checkIsRepo: jest.fn(
- () => Promise.resolve(params.isRepo) as GitResponse
- ),
- revparse: jest.fn((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 (options === "HEAD") {
- return Promise.resolve(params.commit) as GitResponse;
- } else {
- throw new Error(`unexpected options ${options.toString()}`);
- }
- }),
- raw: jest.fn((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)
- );
-};
-
-// These are the chalk-formatted strings that include console color codes.
-const FAIL =
- "\u001b[0m\u001b[7m\u001b[1m\u001b[31m FAIL \u001b[39m\u001b[22m\u001b[27m\u001b[0m";
-const PASS =
- "\u001b[0m\u001b[7m\u001b[1m\u001b[32m PASS \u001b[39m\u001b[22m\u001b[27m\u001b[0m";
-const QUARANTINED =
- "\u001b[0m\u001b[7m\u001b[1m\u001b[33m QUARANTINED \u001b[39m\u001b[22m\u001b[27m\u001b[0m";
-const formatTestFilename = (path: string, filename: string): string =>
- `\u001b[2m${path}\u001b[22m\u001b[1m${filename}\u001b[22m`;
-
-const testResultRegexMatch = (
- result: TestAttemptResult | "skipped",
- testName: string,
- indent?: number
-): RegExp =>
- new RegExp(
- `^${" ".repeat(indent ?? 4)}${escapeStringRegexp(
- result === "pass"
- ? // Green
- "\u001b[32m✓\u001b[39m"
- : result === "fail"
- ? // Red
- "\u001b[31m✕\u001b[39m"
- : result === "quarantined"
- ? // Yellow
- "\u001b[33m✕\u001b[39m"
- : result === "skipped"
- ? // Yellow
- "\u001b[33mâ—‹\u001b[39m"
- : ""
- )} \u001b\\[2m${result === "skipped" ? "skipped " : ""}${escapeStringRegexp(
- testName
- )}${
- result === "quarantined"
- ? escapeStringRegexp("\u001b[33m [quarantined]\u001b[39m")
- : ""
- // Test duration is only included if the test takes at least 1ms.
- }( \\([0-9]+ ms\\))?\u001b\\[22m$`,
- ""
- );
-
-export type ResultCounts = {
- passedSuites: number;
- passedTests: number;
- failedSuites: number;
- failedTests: number;
- flakyTests: number;
- quarantinedSuites: number;
- quarantinedTests: number;
- skippedSuites: number;
- skippedTests: number;
- passedSnapshots: number;
- failedSnapshots: number;
- totalSnapshots: number;
-};
-
-const countResults = ({
- expectPluginToBeEnabled,
- expectQuarantinedTestsToBeQuarantined,
- expectQuarantinedTestsToBeSkipped,
- expectedFailureRetries,
- expectedFlakeTestNameSuffix,
- expectSnapshots,
- failToFetchManifest,
- quarantineFlake,
- skipFailures,
- skipFlake,
- skipQuarantined,
- testNamePattern,
-}: TestCaseParams): ResultCounts => {
- const flakyTest1ShouldRun =
- !skipFlake &&
- (!expectPluginToBeEnabled ||
- !quarantineFlake ||
- !expectQuarantinedTestsToBeSkipped ||
- failToFetchManifest) &&
- (testNamePattern === undefined ||
- `should be flaky 1${expectedFlakeTestNameSuffix}`.match(
- testNamePattern
- ) !== null);
- const flakyTest2ShouldRun =
- !skipFlake &&
- (!expectPluginToBeEnabled ||
- !quarantineFlake ||
- !expectQuarantinedTestsToBeSkipped ||
- failToFetchManifest) &&
- (testNamePattern === undefined ||
- `should be flaky 2${expectedFlakeTestNameSuffix}`.match(
- testNamePattern
- ) !== null);
-
- const mixedFailTestShouldRun =
- !skipFailures &&
- (testNamePattern === undefined ||
- "mixed mixed: should fail".match(testNamePattern) !== null);
- const mixedQuarantinedTestShouldRun =
- !expectQuarantinedTestsToBeSkipped &&
- !skipQuarantined &&
- (testNamePattern === undefined ||
- "mixed mixed: should be quarantined".match(testNamePattern) !== null);
- const mixedPassTestShouldRun =
- testNamePattern === undefined ||
- "mixed mixed: should pass".match(testNamePattern) !== null;
-
- const quarantinedTestShouldRun =
- !expectQuarantinedTestsToBeSkipped &&
- !skipQuarantined &&
- (testNamePattern === undefined ||
- "describe block should be quarantined".match(testNamePattern) !== null);
-
- return [
- // fail.test.ts
- {
- failedSuites:
- (testNamePattern === undefined ||
- "describe block should ([escape regex]?.*$ fail".match(
- testNamePattern
- ) !== null) &&
- !skipFailures
- ? 1
- : 0,
- failedTests:
- (testNamePattern === undefined ||
- "describe block should ([escape regex]?.*$ fail".match(
- testNamePattern
- ) !== null) &&
- !skipFailures
- ? 1
- : 0,
- flakyTests: 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites:
- (testNamePattern !== undefined &&
- "describe block should ([escape regex]?.*$ fail".match(
- testNamePattern
- ) === null) ||
- skipFailures
- ? 1
- : 0,
- skippedTests:
- (testNamePattern !== undefined &&
- "describe block should ([escape regex]?.*$ fail".match(
- testNamePattern
- ) === null) ||
- skipFailures
- ? 1
- : 0,
- passedSnapshots: 0,
- failedSnapshots: expectSnapshots ? 1 : 0,
- totalSnapshots: expectSnapshots ? 1 : 0,
- },
- // flake.test.ts
- {
- failedSuites:
- (!expectPluginToBeEnabled || !quarantineFlake) &&
- (flakyTest1ShouldRun || flakyTest2ShouldRun)
- ? 1
- : 0,
- failedTests:
- !expectPluginToBeEnabled || expectedFailureRetries === 0
- ? (flakyTest1ShouldRun ? 1 : 0) + (flakyTest2ShouldRun ? 1 : 0)
- : 0,
- flakyTests:
- expectPluginToBeEnabled &&
- expectedFailureRetries > 0 &&
- !quarantineFlake
- ? (flakyTest1ShouldRun ? 1 : 0) + (flakyTest2ShouldRun ? 1 : 0)
- : 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites:
- expectPluginToBeEnabled &&
- quarantineFlake &&
- expectQuarantinedTestsToBeQuarantined &&
- (flakyTest1ShouldRun || flakyTest2ShouldRun)
- ? 1
- : 0,
- quarantinedTests:
- expectPluginToBeEnabled &&
- quarantineFlake &&
- expectQuarantinedTestsToBeQuarantined
- ? (flakyTest1ShouldRun ? 1 : 0) + (flakyTest2ShouldRun ? 1 : 0)
- : 0,
- skippedSuites: !flakyTest1ShouldRun && !flakyTest2ShouldRun ? 1 : 0,
- skippedTests:
- (!flakyTest1ShouldRun ? 1 : 0) + (!flakyTest2ShouldRun ? 1 : 0),
- passedSnapshots: 0,
- failedSnapshots: expectSnapshots ? 2 : 0,
- totalSnapshots: expectSnapshots ? 2 : 0,
- },
- // invalid.test.ts
- {
- // If skipFailures is enabled, then we exclude the whole file using a path regex.
- failedSuites: skipFailures ? 0 : 1,
- failedTests: 0,
- flakyTests: 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 0,
- skippedTests: 0,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- // mixed.test.ts
- {
- failedSuites:
- ((!expectPluginToBeEnabled ||
- !expectQuarantinedTestsToBeQuarantined ||
- failToFetchManifest) &&
- mixedQuarantinedTestShouldRun) ||
- mixedFailTestShouldRun
- ? 1
- : 0,
- failedTests:
- ((!expectPluginToBeEnabled ||
- !expectQuarantinedTestsToBeQuarantined ||
- failToFetchManifest) &&
- mixedQuarantinedTestShouldRun
- ? 1
- : 0) + (mixedFailTestShouldRun ? 1 : 0),
- flakyTests: 0,
- passedSuites:
- !mixedFailTestShouldRun &&
- !mixedQuarantinedTestShouldRun &&
- mixedPassTestShouldRun
- ? 1
- : 0,
- passedTests: mixedPassTestShouldRun ? 1 : 0,
- quarantinedSuites:
- expectPluginToBeEnabled &&
- expectQuarantinedTestsToBeQuarantined &&
- !failToFetchManifest &&
- !mixedFailTestShouldRun &&
- mixedQuarantinedTestShouldRun
- ? 1
- : 0,
- quarantinedTests:
- expectPluginToBeEnabled &&
- expectQuarantinedTestsToBeQuarantined &&
- !failToFetchManifest &&
- mixedQuarantinedTestShouldRun
- ? 1
- : 0,
- skippedSuites:
- !mixedQuarantinedTestShouldRun &&
- !mixedFailTestShouldRun &&
- !mixedPassTestShouldRun
- ? 1
- : 0,
- skippedTests:
- (!mixedQuarantinedTestShouldRun ? 1 : 0) +
- (!mixedFailTestShouldRun ? 1 : 0) +
- (!mixedPassTestShouldRun ? 1 : 0),
- passedSnapshots:
- !expectSnapshots && mixedQuarantinedTestShouldRun ? 1 : 0,
- failedSnapshots: expectSnapshots && mixedQuarantinedTestShouldRun ? 1 : 0,
- totalSnapshots: mixedQuarantinedTestShouldRun ? 1 : 0,
- },
- // pass.test.ts
- {
- failedSuites: 0,
- failedTests: 0,
- flakyTests: 0,
- passedSuites:
- testNamePattern === undefined ||
- "should pass".match(testNamePattern) !== null
- ? 1
- : 0,
- passedTests:
- testNamePattern === undefined ||
- "should pass".match(testNamePattern) !== null
- ? 1
- : 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites:
- testNamePattern !== undefined &&
- "should pass".match(testNamePattern) === null
- ? 1
- : 0,
- skippedTests:
- testNamePattern !== undefined &&
- "should pass".match(testNamePattern) === null
- ? 1
- : 0,
- passedSnapshots: expectSnapshots ? 1 : 0,
- failedSnapshots: 0,
- totalSnapshots: expectSnapshots ? 1 : 0,
- },
- // quarantined.test.ts
- {
- failedSuites:
- (!expectPluginToBeEnabled ||
- !expectQuarantinedTestsToBeQuarantined ||
- failToFetchManifest) &&
- quarantinedTestShouldRun
- ? 1
- : 0,
- failedTests:
- (!expectPluginToBeEnabled ||
- !expectQuarantinedTestsToBeQuarantined ||
- failToFetchManifest) &&
- quarantinedTestShouldRun
- ? 1
- : 0,
- flakyTests: 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites:
- expectPluginToBeEnabled &&
- expectQuarantinedTestsToBeQuarantined &&
- !failToFetchManifest &&
- quarantinedTestShouldRun
- ? 1
- : 0,
- quarantinedTests:
- expectPluginToBeEnabled &&
- expectQuarantinedTestsToBeQuarantined &&
- !failToFetchManifest &&
- quarantinedTestShouldRun
- ? 1
- : 0,
- skippedSuites: !quarantinedTestShouldRun ? 1 : 0,
- skippedTests: !quarantinedTestShouldRun ? 1 : 0,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- ].reduce((a: ResultCounts, b: ResultCounts) => ({
- failedSuites: a.failedSuites + b.failedSuites,
- failedTests: a.failedTests + b.failedTests,
- flakyTests: a.flakyTests + b.flakyTests,
- passedSuites: a.passedSuites + b.passedSuites,
- passedTests: a.passedTests + b.passedTests,
- quarantinedSuites: a.quarantinedSuites + b.quarantinedSuites,
- quarantinedTests: a.quarantinedTests + b.quarantinedTests,
- skippedSuites: a.skippedSuites + b.skippedSuites,
- skippedTests: a.skippedTests + b.skippedTests,
- passedSnapshots: a.passedSnapshots + b.passedSnapshots,
- failedSnapshots: a.failedSnapshots + b.failedSnapshots,
- totalSnapshots: a.totalSnapshots + b.totalSnapshots,
- }));
-};
-
-const uploadResultsMatcher =
- (
- {
- expectedBranch,
- expectedCommit,
- expectedFailureRetries,
- expectedFlakeTestNameSuffix,
- expectQuarantinedTestsToBeQuarantined,
- expectQuarantinedTestsToBeSkipped,
- failToFetchManifest,
- quarantineFlake,
- skipFailures,
- skipFlake,
- skipQuarantined,
- testNamePattern,
- }: TestCaseParams,
- results: ResultCounts
- ): MockMatcher =>
- (_url, { body, headers }) => {
- const parsedBody = JSON.parse(
- gunzipSync(body as string).toString()
- ) as CreateTestSuiteRunInlineRequest;
-
- expect((headers as { [key in string]: string })["User-Agent"]).toMatch(
- userAgentRegex
- );
-
- const testNamePatternRegex =
- testNamePattern !== undefined ? new RegExp(testNamePattern) : undefined;
-
- 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).toEqual({
- ...(expectedBranch !== undefined
- ? {
- branch: expectedBranch,
- }
- : {}),
- ...(expectedCommit !== undefined
- ? {
- commit: expectedCommit,
- }
- : {}),
- start_time: "2022-01-23T04:05:06.789Z",
- end_time: expect.stringMatching(/2022-01-23T04:05:..\..89Z/) as string,
- test_runs: expect.arrayContaining(
- [
- ...(skipFailures
- ? ([] as TestRunRecord[])
- : ([
- {
- filename: "../integration-input/src/fail.test.ts",
- name: ["describe block", "should ([escape regex]?.*$ fail"],
- attempts: Array(
- expectedFailureRetries + 1
- ).fill({
- duration_ms: expect.any(Number) as number,
- result: "fail",
- }),
- },
- ] as TestRunRecord[])),
- ...(skipFlake ||
- (expectQuarantinedTestsToBeSkipped &&
- quarantineFlake &&
- !failToFetchManifest) ||
- (testNamePattern !== undefined &&
- `should be flaky 1${expectedFlakeTestNameSuffix}`.match(
- testNamePattern
- ) === null)
- ? []
- : [
- {
- filename: "../integration-input/src/flake.test.ts",
- name: [
- `should be flaky 1${expectedFlakeTestNameSuffix}`.substring(
- 0,
- TEST_NAME_ENTRY_MAX_LENGTH
- ),
- ],
- attempts: [
- {
- duration_ms: expect.any(Number) as number,
- result:
- quarantineFlake &&
- !failToFetchManifest &&
- expectQuarantinedTestsToBeQuarantined
- ? "quarantined"
- : "fail",
- },
- ...(expectedFailureRetries > 0
- ? [
- {
- duration_ms: expect.any(Number) as number,
- result: "pass",
- },
- ]
- : []),
- ],
- } as TestRunRecord,
- ]),
- ...(skipFlake ||
- (expectQuarantinedTestsToBeSkipped &&
- quarantineFlake &&
- !failToFetchManifest) ||
- (testNamePattern !== undefined &&
- `should be flaky 2${expectedFlakeTestNameSuffix}`.match(
- testNamePattern
- ) === null)
- ? []
- : [
- {
- filename: "../integration-input/src/flake.test.ts",
- name: [
- `should be flaky 2${expectedFlakeTestNameSuffix}`.substring(
- 0,
- TEST_NAME_ENTRY_MAX_LENGTH
- ),
- ],
- attempts: [
- {
- duration_ms: expect.any(Number) as number,
- result:
- quarantineFlake &&
- !failToFetchManifest &&
- expectQuarantinedTestsToBeQuarantined
- ? "quarantined"
- : "fail",
- },
- ...(expectedFailureRetries > 0
- ? [
- {
- duration_ms: expect.any(Number) as number,
- result: "pass",
- },
- ]
- : []),
- ],
- } as TestRunRecord,
- ]),
- ...(skipQuarantined ||
- (expectQuarantinedTestsToBeSkipped && !failToFetchManifest) ||
- (testNamePattern !== undefined &&
- "mixed mixed: should be quarantined".match(testNamePattern) !==
- null)
- ? []
- : ([
- {
- filename: "../integration-input/src/mixed.test.ts",
- name: ["mixed", "mixed: should be quarantined"],
- attempts: Array(
- expectedFailureRetries + 1
- ).fill({
- duration_ms: expect.any(Number) as number,
- result:
- failToFetchManifest ||
- !expectQuarantinedTestsToBeQuarantined
- ? "fail"
- : "quarantined",
- }),
- },
- ] as TestRunRecord[])),
- ...(skipFailures ||
- (testNamePattern !== undefined &&
- "mixed mixed: should fail".match(testNamePattern) !== null)
- ? []
- : ([
- {
- filename: "../integration-input/src/mixed.test.ts",
- name: ["mixed", "mixed: should fail"],
- attempts: Array(
- expectedFailureRetries + 1
- ).fill({
- duration_ms: expect.any(Number) as number,
- result: "fail",
- }),
- },
- ] as TestRunRecord[])),
- ...(testNamePattern === undefined ||
- "mixed mixed: should pass".match(testNamePattern) === null
- ? [
- {
- filename: "../integration-input/src/mixed.test.ts",
- name: ["mixed", "mixed: should pass"],
- attempts: [
- {
- duration_ms: expect.any(Number) as number,
- result: "pass",
- },
- ],
- } as TestRunRecord,
- ]
- : []),
- ...(testNamePattern === undefined ||
- "should pass".match(testNamePattern) !== null
- ? [
- {
- filename: "../integration-input/src/pass.test.ts",
- name: ["should pass"],
- attempts: [
- {
- duration_ms: expect.any(Number) as number,
- result: "pass",
- },
- ],
- } as TestRunRecord,
- ]
- : []),
- ...(skipQuarantined ||
- (expectQuarantinedTestsToBeSkipped && !failToFetchManifest) ||
- (testNamePattern !== undefined &&
- "describe block should be quarantined".match(testNamePattern) ===
- null)
- ? []
- : ([
- {
- filename: "../integration-input/src/quarantined.test.ts",
- name: ["describe block", "should be quarantined"],
- attempts: Array(
- expectedFailureRetries + 1
- ).fill({
- duration_ms: expect.any(Number) as number,
- result:
- failToFetchManifest ||
- !expectQuarantinedTestsToBeQuarantined
- ? "fail"
- : "quarantined",
- }),
- },
- ] as TestRunRecord[])),
- ].filter(
- (runRecord) =>
- testNamePatternRegex === undefined ||
- testNamePatternRegex.test(runRecord.name.join(" "))
- )
- ) as TestRunRecord[],
- });
- // Make sure there aren't any extra tests reported.
- expect(parsedBody.test_runs).toHaveLength(
- results.failedTests +
- results.flakyTests +
- results.quarantinedTests +
- results.passedTests
- );
- return true;
- };
-
-const addFetchMockExpectations = (
- params: TestCaseParams,
- results: ResultCounts,
- fetchMock: jest.MockInstance & FetchMockSandbox
-): void => {
- const {
- expectedApiKey,
- expectedBranch,
- expectedCommit,
- expectedFlakeTestNameSuffix,
- expectedSuiteId,
- expectResultsToBeUploaded,
- failToFetchManifest,
- failToUploadResults,
- quarantineFlake,
- } = params;
- fetchMock.get(
- {
- url: `https://app.unflakable.com/api/v1/test-suites/${expectedSuiteId}/manifest`,
- headers: {
- Authorization: `Bearer ${expectedApiKey}`,
- },
- matcher: (_url, { headers }) => {
- expect((headers as { [key in string]: string })["User-Agent"]).toMatch(
- userAgentRegex
- );
- return true;
- },
- },
- failToFetchManifest
- ? {
- throws: new Error("mock request failure"),
- }
- : {
- body: {
- quarantined_tests: [
- {
- test_id: "TEST_QUARANTINED",
- filename: "../integration-input/src/quarantined.test.ts",
- name: ["describe block", "should be quarantined"],
- },
- {
- test_id: "TEST_QUARANTINED2",
- filename: "../integration-input/src/mixed.test.ts",
- name: ["mixed", "mixed: should be quarantined"],
- },
- ...(quarantineFlake
- ? [
- {
- test_id: "TEST_FLAKE",
- filename: "../integration-input/src/flake.test.ts",
- name: [
- `should be flaky 1${expectedFlakeTestNameSuffix}`.substring(
- 0,
- TEST_NAME_ENTRY_MAX_LENGTH
- ),
- ],
- },
- {
- test_id: "TEST_FLAKE",
- filename: "../integration-input/src/flake.test.ts",
- name: [
- `should be flaky 2${expectedFlakeTestNameSuffix}`.substring(
- 0,
- TEST_NAME_ENTRY_MAX_LENGTH
- ),
- ],
- },
- ]
- : []),
- ],
- } as TestSuiteManifest,
- status: 200,
- },
- {
- repeat: failToFetchManifest ? 3 : 1,
- }
- );
- if (expectResultsToBeUploaded) {
- const uploadUrl =
- "https://s3.mock.amazonaws.com/unflakable-backend-mock-test-uploads/teams/MOCK_TEAM_ID/" +
- `suites/${expectedSuiteId}/runs/upload/MOCK_UPLOAD_ID?X-Amz-Signature=MOCK_SIGNATURE`;
- fetchMock.postOnce(
- {
- url: `https://app.unflakable.com/api/v1/test-suites/${expectedSuiteId}/runs/upload`,
- headers: {
- Authorization: `Bearer ${expectedApiKey}`,
- "Content-Type": "application/json",
- },
- matcher: (_url, { body }) => {
- expect(body).toBe(undefined);
- return true;
- },
- },
- (): MockResponse => ({
- body: {
- upload_id: "MOCK_UPLOAD_ID",
- },
- headers: {
- Location: uploadUrl,
- },
- status: 201,
- })
- );
-
- let runRequest: CreateTestSuiteRunInlineRequest | null = null;
- fetchMock.putOnce(
- {
- url: uploadUrl,
- headers: {
- "Content-Encoding": "gzip",
- "Content-Type": "application/json",
- },
- matcher: uploadResultsMatcher(params, results),
- },
- (_url: string, { body }: MockRequest): MockResponse => {
- runRequest = JSON.parse(
- gunzipSync(body as string).toString()
- ) as CreateTestSuiteRunInlineRequest;
-
- return {
- status: 200,
- };
- }
- );
- fetchMock.post(
- {
- url: `https://app.unflakable.com/api/v1/test-suites/${expectedSuiteId}/runs`,
- headers: {
- Authorization: `Bearer ${expectedApiKey}`,
- "Content-Type": "application/json",
- },
- matcher: (_url, { body }) => {
- const parsedBody = JSON.parse(
- body as string
- ) as CreateTestSuiteRunFromUploadRequest;
- expect(parsedBody.upload_id).toBe("MOCK_UPLOAD_ID");
- return true;
- },
- },
- (): MockResponse => {
- expect(runRequest).not.toBeNull();
- const parsedRequest = runRequest as CreateTestSuiteRunInlineRequest;
-
- if (failToUploadResults) {
- return {
- throws: new Error("mock request failure"),
- };
- }
-
- return {
- body: {
- 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:
- results.failedTests +
- results.flakyTests +
- results.quarantinedTests +
- results.passedTests,
- num_pass: results.passedTests,
- num_fail: results.failedSuites,
- num_flake: results.flakyTests,
- num_quarantined: results.quarantinedSuites,
- } as TestSuiteRunPendingSummary,
- status: 201,
- };
- },
- {
- repeat: failToUploadResults ? 3 : 1,
- }
- );
- }
-};
-
-const verifyOutput = (
- {
- expectPluginToBeEnabled,
- expectQuarantinedTestsToBeQuarantined,
- expectQuarantinedTestsToBeSkipped,
- expectResultsToBeUploaded,
- expectedFailureRetries,
- expectedFlakeTestNameSuffix,
- expectedSuiteId,
- failToFetchManifest,
- failToUploadResults,
- quarantineFlake,
- skipFailures,
- skipFlake,
- skipQuarantined,
- testNamePattern,
- }: TestCaseParams,
- stderrLines: (Uint8Array | string)[],
- results: ResultCounts
-): void => {
- // Make sure expected output is present and chalk-formatted correctly.
-
- /* eslint-disable @typescript-eslint/unbound-method */
-
- // Test our VerboseReporter customization.
- (testNamePattern === undefined ||
- "should pass".match(testNamePattern) !== null
- ? expect(stderrLines).toContain
- : expect(stderrLines).not.toContain)(
- `${PASS} ${formatTestFilename("../integration-input/src/", "pass.test.ts")}`
- );
- (testNamePattern === undefined ||
- "should pass".match(testNamePattern) !== null
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- // This test doesn't have a describe() block, so it's only indented 2 spaces.
- expect.stringMatching(testResultRegexMatch("pass", "should pass", 2))
- );
-
- (!skipFailures &&
- (testNamePattern === undefined ||
- "describe block should ([escape regex]?.*$ fail".match(
- testNamePattern
- ) !== null)
- ? expect(stderrLines).toContain
- : expect(stderrLines).not.toContain)(
- `${FAIL} ${formatTestFilename("../integration-input/src/", "fail.test.ts")}`
- );
- (!skipFailures &&
- (testNamePattern === undefined ||
- "describe block should ([escape regex]?.*$ fail".match(
- testNamePattern
- ) !== null)
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(
- testResultRegexMatch("fail", "should ([escape regex]?.*$ fail")
- )
- );
-
- const flakyTest1Name = `should be flaky 1${expectedFlakeTestNameSuffix}`;
- const flakyTest1ShouldRun =
- !skipFlake &&
- (!quarantineFlake ||
- failToFetchManifest ||
- !expectQuarantinedTestsToBeSkipped) &&
- (testNamePattern === undefined ||
- flakyTest1Name.match(testNamePattern) !== null);
- (flakyTest1ShouldRun
- ? expect(stderrLines).toContain
- : expect(stderrLines).not.toContain)(
- `${
- quarantineFlake &&
- !failToFetchManifest &&
- expectQuarantinedTestsToBeQuarantined
- ? `${QUARANTINED} `
- : ""
- }${FAIL} ${formatTestFilename(
- "../integration-input/src/",
- "flake.test.ts"
- )}`
- );
- // This test should fail then pass (though we're not verifying the order here).
- (flakyTest1ShouldRun
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(
- testResultRegexMatch(
- quarantineFlake && !failToFetchManifest ? "quarantined" : "fail",
- flakyTest1Name,
- 2
- )
- )
- );
- (expectPluginToBeEnabled && expectedFailureRetries > 0 && flakyTest1ShouldRun
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(testResultRegexMatch("pass", flakyTest1Name, 2))
- );
-
- const flakyTest2Name = `should be flaky 2${expectedFlakeTestNameSuffix}`;
- const flakyTest2ShouldRun =
- !skipFlake &&
- (!quarantineFlake ||
- failToFetchManifest ||
- !expectQuarantinedTestsToBeSkipped) &&
- (testNamePattern === undefined ||
- flakyTest2Name.match(testNamePattern) !== null);
- (flakyTest2ShouldRun
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(
- testResultRegexMatch(
- quarantineFlake && !failToFetchManifest ? "quarantined" : "fail",
- flakyTest2Name,
- 2
- )
- )
- );
- (expectPluginToBeEnabled && expectedFailureRetries > 0 && flakyTest2ShouldRun
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(testResultRegexMatch("pass", flakyTest2Name, 2))
- );
-
- (!skipQuarantined &&
- (!expectQuarantinedTestsToBeSkipped || failToFetchManifest) &&
- (testNamePattern === undefined ||
- "describe block should be quarantined".match(testNamePattern) !== null)
- ? expect(stderrLines).toContain
- : expect(stderrLines).not.toContain)(
- `${
- expectPluginToBeEnabled &&
- !failToFetchManifest &&
- expectQuarantinedTestsToBeQuarantined &&
- !expectQuarantinedTestsToBeSkipped
- ? `${QUARANTINED} `
- : ""
- }${FAIL} ${formatTestFilename(
- "../integration-input/src/",
- "quarantined.test.ts"
- )}`
- );
- (!skipQuarantined &&
- (testNamePattern === undefined ||
- "describe block should be quarantined".match(testNamePattern) !== null) &&
- !expectQuarantinedTestsToBeSkipped
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(
- testResultRegexMatch(
- expectPluginToBeEnabled &&
- !failToFetchManifest &&
- expectQuarantinedTestsToBeQuarantined
- ? "quarantined"
- : "fail",
- "should be quarantined"
- )
- )
- );
-
- const mixedFailTestShouldRun =
- !skipFailures &&
- (testNamePattern === undefined ||
- "mixed mixed: should fail".match(testNamePattern) !== null);
- const mixedQuarantinedTestShouldRun =
- !expectQuarantinedTestsToBeSkipped &&
- !skipQuarantined &&
- (testNamePattern === undefined ||
- "mixed mixed: should be quarantined".match(testNamePattern) !== null);
- const mixedPassTestShouldRun =
- testNamePattern === undefined ||
- "mixed mixed: should pass".match(testNamePattern) !== null;
-
- // Mixed file containing both a failed test and a quarantined one.
- (((!expectPluginToBeEnabled ||
- failToFetchManifest ||
- !expectQuarantinedTestsToBeQuarantined) &&
- mixedQuarantinedTestShouldRun) ||
- mixedFailTestShouldRun
- ? expect(stderrLines).toContain
- : expect(stderrLines).not.toContain)(
- `${FAIL} ${formatTestFilename(
- "../integration-input/src/",
- "mixed.test.ts"
- )}`
- );
- (mixedQuarantinedTestShouldRun
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(
- testResultRegexMatch(
- expectPluginToBeEnabled &&
- !failToFetchManifest &&
- expectQuarantinedTestsToBeQuarantined
- ? "quarantined"
- : "fail",
- "mixed: should be quarantined"
- )
- )
- );
- (mixedFailTestShouldRun
- ? expect(stderrLines).toContainEqual
- : expect(stderrLines).not.toContainEqual)(
- expect.stringMatching(testResultRegexMatch("fail", "mixed: should fail"))
- );
-
- expect(
- stderrLines.filter((line) =>
- testResultRegexMatch("pass", "mixed: should pass").test(line as string)
- )
- ).toHaveLength(mixedPassTestShouldRun ? 1 : 0);
-
- // The passed test gets skipped during the retries.
- if (mixedFailTestShouldRun || mixedQuarantinedTestShouldRun) {
- expect(
- stderrLines.filter((line) =>
- testResultRegexMatch("skipped", "mixed: should pass").test(
- line as string
- )
- )
- ).toHaveLength(
- testNamePattern !== undefined &&
- "mixed mixed: should pass".match(testNamePattern) === null &&
- expectPluginToBeEnabled
- ? expectedFailureRetries + 1
- : expectPluginToBeEnabled
- ? expectedFailureRetries
- : testNamePattern !== undefined &&
- "mixed mixed: should pass".match(testNamePattern) === null
- ? 1
- : 0
- );
- }
-
- // Test our SummaryReporter customization.
- expect(stderrLines).toContain(
- `\u001b[1mTest Suites: \u001b[22m${
- results.failedSuites !== 0
- ? `\u001b[1m\u001b[31m${results.failedSuites} failed\u001b[39m\u001b[22m, `
- : ""
- }${
- results.quarantinedSuites !== 0
- ? `\u001b[1m\u001b[33m${results.quarantinedSuites} quarantined\u001b[39m\u001b[22m, `
- : ""
- }${
- results.skippedSuites !== 0
- ? `\u001b[1m\u001b[33m${results.skippedSuites} skipped\u001b[39m\u001b[22m, `
- : ""
- }${
- results.passedSuites !== 0
- ? `\u001b[1m\u001b[32m${results.passedSuites} passed\u001b[39m\u001b[22m, `
- : ""
- }${
- results.skippedSuites !== 0
- ? `${
- results.failedSuites +
- results.quarantinedSuites +
- results.passedSuites
- } of ${
- results.failedSuites +
- results.quarantinedSuites +
- results.passedSuites +
- results.skippedSuites
- }`
- : results.failedSuites +
- results.quarantinedSuites +
- results.passedSuites
- } total`
- );
-
- expect(stderrLines).toContain(
- `\u001b[1mTests: \u001b[22m${
- results.failedTests !== 0
- ? `\u001b[1m\u001b[31m${results.failedTests} failed\u001b[39m\u001b[22m, `
- : ""
- }${
- results.flakyTests !== 0
- ? `\u001b[1m\u001b[95m${results.flakyTests} flaky\u001b[39m\u001b[22m, `
- : ""
- }${
- results.quarantinedTests !== 0
- ? `\u001b[1m\u001b[33m${results.quarantinedTests} quarantined\u001b[39m\u001b[22m, `
- : ""
- }${
- results.skippedTests !== 0
- ? `\u001b[1m\u001b[33m${results.skippedTests} skipped\u001b[39m\u001b[22m, `
- : ""
- }${
- results.passedTests !== 0
- ? `\u001b[1m\u001b[32m${results.passedTests} passed\u001b[39m\u001b[22m, `
- : ""
- }${
- results.failedTests +
- results.flakyTests +
- results.quarantinedTests +
- results.passedTests +
- results.skippedTests
- } total`
- );
-
- expect(stderrLines).toContain(
- `\u001b[1mSnapshots: \u001b[22m${
- results.failedSnapshots > 0
- ? `\u001b[1m\u001b[31m${results.failedSnapshots} failed\u001b[39m\u001b[22m, `
- : ""
- }${
- results.passedSnapshots > 0
- ? `\u001b[1m\u001b[32m${results.passedSnapshots} passed\u001b[39m\u001b[22m, `
- : ""
- }${results.totalSnapshots} total`
- );
- // None of the snapshots should be obsolete.
- expect(stderrLines).not.toContainEqual(
- expect.stringMatching(new RegExp("[0-9]+ snapshot(:?s)? obsolete"))
- );
-
- // The duration here is based on the mocked time, so it should be deterministic.
- expect(stderrLines).toContainEqual(
- expect.stringMatching(
- new RegExp(
- `${escapeStringRegexp("\u001b[1mTime:\u001b[22m ")}[0-9.]+ s`
- )
- )
- );
-
- expect(stderrLines).toContain(
- `\u001b[2mRan all test suites\u001b[22m\u001b[2m${
- testNamePattern !== undefined
- ? ` with tests matching \u001b[22m"${testNamePattern}"\u001b[2m`
- : ""
- }.\u001b[22m`
- );
-
- (expectPluginToBeEnabled && expectResultsToBeUploaded && !failToUploadResults
- ? expect(stderrLines).toContain
- : expect(stderrLines).not.toContain)(
- `Unflakable report: https://app.unflakable.com/test-suites/${expectedSuiteId}/runs/MOCK_RUN_ID`
- );
-};
-
-export const runTestCase = async (
- params: TestCaseParams,
- expectedExitCode: number,
- expectedResults: ResultCounts,
- mockConfigExplorer: ReturnType,
- mockExit: jest.Mock,
- fetchMock: jest.MockInstance & FetchMockSandbox
-): Promise => {
- const {
- expectPluginToBeEnabled,
- failToUploadResults,
- git,
- skipFailures,
- skipFlake,
- skipQuarantined,
- testNamePattern,
- } = params;
-
- (mockConfigExplorer.search as jest.Mock).mockImplementation(
- (searchFrom?: string): CosmiconfigResult => {
- expect(searchFrom).toMatch(
- new RegExp("packages/jest-plugin/test/integration-input$")
- );
- return params.config !== null
- ? {
- config: params.config,
- filepath:
- "MOCK_BASE/packages/jest-plugin/test/integration-input/package.json",
- }
- : null;
- }
- );
-
- mockSimpleGit(git);
-
- const results = countResults(params);
-
- // The flaky test needs external state to know when it's being retried so that it can pass.
- process.env.FLAKY_TEST_TEMP = temp.path();
-
- if (skipFailures) {
- process.env.SKIP_FAILURES = "true";
- } else {
- delete process.env.SKIP_FAILURES;
- }
-
- if (skipFlake) {
- process.env.SKIP_FLAKE = "true";
- } else {
- delete process.env.SKIP_FLAKE;
- }
-
- if (skipQuarantined) {
- process.env.SKIP_QUARANTINED = "true";
- } else {
- delete process.env.SKIP_QUARANTINED;
- }
-
- Object.entries(params.envVars).forEach(([name, value]) => {
- if (value !== undefined) {
- process.env[name] = value;
- } else {
- delete process.env[name];
- }
- });
-
- if (expectPluginToBeEnabled) {
- addFetchMockExpectations(params, results, fetchMock);
- }
-
- let stderrLines: (Uint8Array | string)[] = [];
- process.stderr.write = jest.fn(
- (
- buffer: Uint8Array | string,
- encoding?: BufferEncoding,
- cb?: (err?: Error) => void
- ): boolean => {
- stderrLines = [
- ...stderrLines,
- ...(typeof buffer === "string"
- ? buffer.split("\n")
- : [JSON.stringify(buffer)]),
- ];
- // split() adds an empty string if the delimiter is the last character; remove it here.
- if (stderrLines[stderrLines.length - 1] === "") {
- stderrLines.pop();
- }
- //originalStderrWrite(JSON.stringify(buffer));
- return originalStderrWrite(buffer, encoding, cb);
- }
- ) as typeof process.stderr.write;
-
- try {
- const runPromise = run(
- [
- // Needed so that exit() gets called and we can assert that it's the correct exit code.
- // NB: Despite the warning, we don't pass --detectOpenHandles since that would also enable
- // --runInBand and not test the plugin's ability to deal with parallel tests.
- "--forceExit",
- // --no-cache disables the cache that stores the past timings, which makes the output
- // non-deterministic since it gets bolded if it exceeds the expected time.
- "--no-cache",
- "--reporters",
- "@unflakable/jest-plugin/dist/reporter",
- "--runner",
- "@unflakable/jest-plugin/dist/runner",
- ...(skipFailures
- ? [
- "--testPathIgnorePatterns",
- "../integration-input/src/invalid\\.test\\.ts",
- ]
- : []),
- ...(testNamePattern !== undefined
- ? ["--testNamePattern", testNamePattern]
- : []),
- ],
- "../integration-input"
- );
-
- if (failToUploadResults) {
- await expect(runPromise).rejects.toThrow();
- } else {
- await runPromise;
- }
- } finally {
- process.stderr.write = originalStderrWrite;
- }
-
- // There shouldn't be any unexpected mock fetch calls.
- expect(fetchMock.calls("unmatched")).toHaveLength(0);
- expect(fetchMock).toBeDone();
-
- expect(results).toEqual(expectedResults);
- verifyOutput(params, stderrLines, results);
-
- expect(mockExit).toHaveBeenCalledTimes(1);
- expect(mockExit).toHaveBeenCalledWith(expectedExitCode);
-};
diff --git a/packages/jest-plugin/test/integration/src/skip-tests.test.ts b/packages/jest-plugin/test/integration/src/skip-tests.test.ts
new file mode 100644
index 0000000..6586044
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/skip-tests.test.ts
@@ -0,0 +1,141 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import { integrationTest, integrationTestSuite } from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("set quarantineMode to skip_tests", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ quarantineMode: "skip_tests",
+ },
+ expectQuarantinedTestsToBeSkipped: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 4,
+ failedTests: 2,
+ flakyTests: 2,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 1,
+ skippedTests: 2,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("test names longer than 4096 chars should be truncated w/ quarantineMode set to skip_tests", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ quarantineMode: "skip_tests",
+ },
+ envVars: {
+ FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
+ },
+ expectedFlakeTestNameSuffix: "*".repeat(4096),
+ expectQuarantinedTestsToBeSkipped: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 4,
+ failedTests: 2,
+ flakyTests: 2,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 1,
+ skippedTests: 2,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("quarantining should work for tests with names longer than 4096 chars w/ quarantineMode set to skip_tests", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ quarantineMode: "skip_tests",
+ },
+ envVars: {
+ FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
+ },
+ expectedFlakeTestNameSuffix: "*".repeat(4096),
+ expectQuarantinedTestsToBeSkipped: true,
+ quarantineFlake: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 3,
+ failedTests: 2,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 2,
+ skippedTests: 4,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("set quarantineMode to skip_tests and --testNamePattern that skips all other tests", (done) =>
+ integrationTest(
+ {
+ params: {
+ config: {
+ quarantineMode: "skip_tests",
+ },
+ expectQuarantinedTestsToBeSkipped: true,
+ expectResultsToBeUploaded: false,
+ skipFailures: true,
+ testNamePattern: "should be quarantined",
+ },
+ expectedExitCode: 0,
+ expectedResults: {
+ failedSuites: 0,
+ failedTests: 0,
+ flakyTests: 0,
+ passedSuites: 0,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 0,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 5,
+ skippedTests: 8,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/skipTests.test.ts b/packages/jest-plugin/test/integration/src/skipTests.test.ts
deleted file mode 100644
index 580bd08..0000000
--- a/packages/jest-plugin/test/integration/src/skipTests.test.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import { integrationTest, integrationTestSuite } from "./common";
-
-integrationTestSuite(() => {
- it("set quarantineMode to skip_tests", () =>
- integrationTest({
- params: {
- config: {
- quarantineMode: "skip_tests",
- },
- expectQuarantinedTestsToBeSkipped: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 4,
- failedTests: 2,
- flakyTests: 2,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 1,
- skippedTests: 2,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-
- it("test names longer than 4096 chars should be truncated w/ quarantineMode set to skip_tests", () =>
- integrationTest({
- params: {
- config: {
- quarantineMode: "skip_tests",
- },
- envVars: {
- FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
- },
- expectedFlakeTestNameSuffix: "*".repeat(4096),
- expectQuarantinedTestsToBeSkipped: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 4,
- failedTests: 2,
- flakyTests: 2,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 1,
- skippedTests: 2,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-
- it("quarantining should work for tests with names longer than 4096 chars w/ quarantineMode set to skip_tests", () =>
- integrationTest({
- params: {
- config: {
- quarantineMode: "skip_tests",
- },
- envVars: {
- FLAKE_TEST_NAME_SUFFIX: "*".repeat(4096),
- },
- expectedFlakeTestNameSuffix: "*".repeat(4096),
- expectQuarantinedTestsToBeSkipped: true,
- quarantineFlake: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 3,
- failedTests: 2,
- flakyTests: 0,
- passedSuites: 1,
- passedTests: 2,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 2,
- skippedTests: 4,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-
- it("set quarantineMode to skip_tests and --testNamePattern that skips all other tests", () =>
- integrationTest({
- params: {
- config: {
- quarantineMode: "skip_tests",
- },
- expectQuarantinedTestsToBeSkipped: true,
- expectResultsToBeUploaded: false,
- skipFailures: true,
- testNamePattern: "should be quarantined",
- },
- expectedExitCode: 0,
- expectedResults: {
- failedSuites: 0,
- failedTests: 0,
- flakyTests: 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 5,
- skippedTests: 8,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/snapshots.test.ts b/packages/jest-plugin/test/integration/src/snapshots.test.ts
index 2748bd8..afb9383 100644
--- a/packages/jest-plugin/test/integration/src/snapshots.test.ts
+++ b/packages/jest-plugin/test/integration/src/snapshots.test.ts
@@ -1,26 +1,30 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import {
defaultExpectedResults,
integrationTest,
integrationTestSuite,
-} from "./common";
+} from "./test-wrappers";
-integrationTestSuite(() => {
- it("use snapshot tests", () =>
- integrationTest({
- params: {
- envVars: {
- TEST_SNAPSHOTS: "true",
+integrationTestSuite((mockBackend) => {
+ it("use snapshot tests", (done) =>
+ integrationTest(
+ {
+ params: {
+ envVars: {
+ TEST_SNAPSHOTS: "true",
+ },
+ expectSnapshots: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ ...defaultExpectedResults,
+ passedSnapshots: 1,
+ failedSnapshots: 4,
+ totalSnapshots: 5,
},
- expectSnapshots: true,
- },
- expectedExitCode: 1,
- expectedResults: {
- ...defaultExpectedResults,
- passedSnapshots: 1,
- failedSnapshots: 4,
- totalSnapshots: 5,
},
- }));
+ mockBackend,
+ done
+ ));
});
diff --git a/packages/jest-plugin/test/integration/src/test-independence.test.ts b/packages/jest-plugin/test/integration/src/test-independence.test.ts
new file mode 100644
index 0000000..e60ef6f
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/test-independence.test.ts
@@ -0,0 +1,404 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import jestPackage from "jest/package.json";
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+import semverLt from "semver/functions/lt";
+
+integrationTestSuite((mockBackend) => {
+ // Test independence requires Jest 28+, so we skip these tests for earlier versions.
+ const itOrSkip = semverLt(jestPackage.version, "28.0.0") ? it.skip : it;
+
+ itOrSkip("test-independent flake should pass", (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: ({ failure }) =>
+ failure.includes("Error: first try should fail"),
+};
+`,
+ expectFlakeToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 3,
+ failedTests: 2,
+ flakyTests: 0,
+ passedSuites: 2,
+ passedSuitesWithIndependentFailures: 1,
+ passedTests: 4,
+ passedTestsWithIndependentFailures: 2,
+ quarantinedSuites: 1,
+ quarantinedTests: 2,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ );
+ });
+
+ itOrSkip(
+ "test-independent flake without failures should exit successfully",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: /Error: first try should fail/,
+};
+`,
+ expectFlakeToBeTestIndependent: true,
+ expectQuarantinedTestsToBeSkipped: true,
+ skipFailures: true,
+ skipQuarantined: true,
+ },
+ expectedExitCode: 0,
+ expectedResults: {
+ failedSuites: 0,
+ failedTests: 0,
+ flakyTests: 0,
+ passedSuites: 3,
+ passedSuitesWithIndependentFailures: 1,
+ passedTests: 4,
+ passedTestsWithIndependentFailures: 2,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 2,
+ skippedTests: 4,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip(
+ "test-independent fail followed by regular fail should fail",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+const fs = require("fs/promises");
+const path = require("path");
+module.exports = {
+ __unstableIsFailureTestIndependent: async ({ testFilePath, failure }) => {
+ if (failure.includes("Error: test failed")
+ || failure.includes("Error: mixed test failed")
+ ) {
+ const tempFilePath =
+ \`\${process.env.FLAKY_TEST_TEMP}-test-independent-\${path.basename(testFilePath)}\`;
+
+ const attempt = await fs
+ .readFile(tempFilePath, { encoding: "utf8" })
+ .then(Number.parseInt)
+ .catch(() => 0);
+ await fs.writeFile(tempFilePath, (attempt + 1).toString());
+
+ return attempt === 0;
+ } else {
+ return false;
+ }
+ }
+};
+`,
+ expectFailuresFirstAttemptToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip(
+ "quarantined test-independent fail followed by regular fail should be quarantined",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+const fs = require("fs/promises");
+const path = require("path");
+module.exports = {
+ __unstableIsFailureTestIndependent: async ({ testFilePath, failure }) => {
+ if (failure.includes("Error: quarantined test failed")
+ || failure.includes("Error: mixed quarantined test failed")
+ ) {
+ const tempFilePath =
+ \`\${process.env.FLAKY_TEST_TEMP}-test-independent-\${path.basename(testFilePath)}\`;
+
+ const attempt = await fs
+ .readFile(tempFilePath, { encoding: "utf8" })
+ .then(Number.parseInt)
+ .catch(() => 0);
+ await fs.writeFile(tempFilePath, (attempt + 1).toString());
+
+ return attempt === 0;
+ } else {
+ return false;
+ }
+ }
+};
+`,
+ expectQuarantinedTestsFirstAttemptToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip(
+ "test-independent fail then fail then pass should be flaky",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+const fs = require("fs/promises");
+const path = require("path");
+module.exports = {
+ __unstableIsFailureTestIndependent: async ({ testFilePath, testName, failure }) => {
+ if (failure.includes("Error: first try should fail")) {
+ const tempFilePath =
+ \`\${process.env.FLAKY_TEST_TEMP}-test-independent-\${testName[0][testName[0].length - 1]}\`;
+
+ const attempt = await fs
+ .readFile(tempFilePath, { encoding: "utf8" })
+ .then(Number.parseInt)
+ .catch(() => 0);
+ await fs.writeFile(tempFilePath, (attempt + 1).toString());
+
+ return attempt === 0;
+ } else {
+ return false;
+ }
+ }
+};
+`,
+ expectFlakeFirstAttemptToBeTestIndependent: true,
+ flakeFailCount: 2,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip(
+ "test-independent quarantined fail then fail then pass should be quarantined",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+const fs = require("fs/promises");
+const path = require("path");
+module.exports = {
+ __unstableIsFailureTestIndependent: async ({ testFilePath, testName, failure }) => {
+ if (failure.includes("Error: first try should fail")) {
+ const tempFilePath =
+ \`\${process.env.FLAKY_TEST_TEMP}-test-independent-\${testName[0][testName[0].length - 1]}\`;
+
+ const attempt = await fs
+ .readFile(tempFilePath, { encoding: "utf8" })
+ .then(Number.parseInt)
+ .catch(() => 0);
+ await fs.writeFile(tempFilePath, (attempt + 1).toString());
+
+ return attempt === 0;
+ } else {
+ return false;
+ }
+ }
+};
+`,
+ expectFlakeFirstAttemptToBeTestIndependent: true,
+ flakeFailCount: 2,
+ quarantineFlake: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 3,
+ failedTests: 2,
+ flakyTests: 0,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 2,
+ quarantinedTests: 4,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip("test-independent quarantined flake should pass", (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: ({ failure }) =>
+ failure.includes("Error: first try should fail"),
+};
+`,
+ expectFlakeToBeTestIndependent: true,
+ quarantineFlake: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 3,
+ failedTests: 2,
+ flakyTests: 0,
+ passedSuites: 2,
+ passedSuitesWithIndependentFailures: 1,
+ passedTests: 4,
+ passedTestsWithIndependentFailures: 2,
+ quarantinedSuites: 1,
+ quarantinedTests: 2,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+ },
+ },
+ mockBackend,
+ done
+ );
+ });
+
+ itOrSkip(
+ "repeated test-independent failures without pass should fail",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: "Error: (?:mixed )?test failed",
+};
+`,
+ expectFailuresToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip(
+ "repeated quarantined test-independent failures without pass should be quarantined",
+ (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: /Error: (?:mixed )?quarantined test failed/,
+};
+`,
+ expectQuarantinedTestsToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ }
+ );
+
+ itOrSkip("multi-line regex should match", (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: "Error: (?:mixed )?test failed\\nnew line",
+};
+`,
+ expectFailuresToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ });
+
+ itOrSkip("regex should match stderr", (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: "(?:^|\\n)(?:mixed )?fail stderr\\n",
+};
+`,
+ expectFailuresToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ });
+
+ itOrSkip("regex should match stdout", (done) => {
+ integrationTest(
+ {
+ params: {
+ configJs: `
+module.exports = {
+ __unstableIsFailureTestIndependent: "(?:^|\\n)(?:mixed )?fail stdout\\n",
+};
+`,
+ expectFailuresToBeTestIndependent: true,
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ );
+ });
+});
diff --git a/packages/jest-plugin/test/integration/src/test-name-pattern.test.ts b/packages/jest-plugin/test/integration/src/test-name-pattern.test.ts
new file mode 100644
index 0000000..a7786ad
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/test-name-pattern.test.ts
@@ -0,0 +1,112 @@
+// Copyright (c) 2022-2024 Developer Innovations, LLC
+
+import {
+ defaultExpectedResults,
+ integrationTest,
+ integrationTestSuite,
+} from "./test-wrappers";
+
+integrationTestSuite((mockBackend) => {
+ it("use --testNamePattern to have Jest filter tests", (done) =>
+ integrationTest(
+ {
+ params: {
+ testNamePattern: "(should .*fail|should be flaky)",
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 4,
+ failedTests: 2,
+ flakyTests: 2,
+ passedSuites: 0,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 0,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 2,
+ skippedTests: 4,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("use --testNamePattern with no matches", (done) =>
+ integrationTest(
+ {
+ params: {
+ expectResultsToBeUploaded: false,
+ skipFailures: true,
+ testNamePattern: "no matches",
+ },
+ expectedExitCode: 0,
+ expectedResults: {
+ failedSuites: 0,
+ failedTests: 0,
+ flakyTests: 0,
+ passedSuites: 0,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 0,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 5,
+ skippedTests: 8,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+
+ it("use a permissive --testNamePattern and ensure only failed tests are retried", (done) =>
+ integrationTest(
+ {
+ params: {
+ testNamePattern: ".*",
+ },
+ expectedExitCode: 1,
+ expectedResults: defaultExpectedResults,
+ },
+ mockBackend,
+ done
+ ));
+
+ it("use --testNamePattern with plugin disabled", (done) =>
+ integrationTest(
+ {
+ params: {
+ envVars: {
+ UNFLAKABLE_ENABLED: "false",
+ },
+ expectPluginToBeEnabled: false,
+ testNamePattern: "(should .*fail|should be flaky)",
+ },
+ expectedExitCode: 1,
+ expectedResults: {
+ failedSuites: 4,
+ failedTests: 4,
+ flakyTests: 0,
+ passedSuites: 0,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 0,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 0,
+ quarantinedTests: 0,
+ skippedSuites: 2,
+ skippedTests: 4,
+ passedSnapshots: 0,
+ failedSnapshots: 0,
+ totalSnapshots: 0,
+ },
+ },
+ mockBackend,
+ done
+ ));
+});
diff --git a/packages/jest-plugin/test/integration/src/test-wrappers.ts b/packages/jest-plugin/test/integration/src/test-wrappers.ts
new file mode 100644
index 0000000..7cae8b6
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/test-wrappers.ts
@@ -0,0 +1,131 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import jestPackage from "jest/package.json";
+import path from "path";
+import { ResultCounts, runTestCase, TestCaseParams } from "./run-test-case";
+import { MockBackend } from "unflakable-test-common/dist/mock-backend";
+import * as os from "os";
+import * as util from "util";
+
+export type TestCase = {
+ params: Partial;
+ expectedExitCode: number;
+ expectedResults: ResultCounts;
+};
+
+export const defaultExpectedResults: ResultCounts = {
+ failedSuites: 4,
+ failedTests: 2,
+ flakyTests: 2,
+ passedSuites: 1,
+ passedSuitesWithIndependentFailures: 0,
+ passedTests: 2,
+ passedTestsWithIndependentFailures: 0,
+ quarantinedSuites: 1,
+ quarantinedTests: 2,
+ skippedSuites: 0,
+ skippedTests: 0,
+ passedSnapshots: 1,
+ failedSnapshots: 0,
+ totalSnapshots: 1,
+};
+
+export const integrationTest = (
+ testCase: TestCase,
+ mockBackend: MockBackend,
+ done: jest.DoneCallback
+): void => {
+ void runTestCase(
+ {
+ config: null,
+ configJs: null,
+ expectedApiKey: "MOCK_API_KEY",
+ expectedBranch: "MOCK_BRANCH",
+ expectedCommit: "MOCK_COMMIT",
+ expectedFailureRetries: 2,
+ expectedFlakeTestNameSuffix: "",
+ expectedRepoRelativePathPrefix: "test/integration-input/",
+ expectedSuiteId: "MOCK_SUITE_ID",
+ expectFailuresToBeTestIndependent: false,
+ expectFailuresFirstAttemptToBeTestIndependent: false,
+ expectFlakeToBeTestIndependent: false,
+ expectFlakeFirstAttemptToBeTestIndependent: false,
+ expectPluginToBeEnabled: true,
+ expectResultsToBeUploaded: true,
+ expectQuarantinedTestsToBeQuarantined: true,
+ expectQuarantinedTestsToBeSkipped: false,
+ expectQuarantinedTestsToBeTestIndependent: false,
+ expectQuarantinedTestsFirstAttemptToBeTestIndependent: false,
+ expectSnapshots: false,
+ failToFetchManifest: false,
+ failToUploadResults: false,
+ flakeFailCount: 1,
+ 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/jest-plugin so that we're for sure testing the
+ // mocked output and not using real git commands.
+ repoRoot: path.resolve("../.."),
+ },
+ quarantineFlake: false,
+ skipFailures: false,
+ skipFlake: false,
+ skipQuarantined: false,
+ testNamePattern: undefined,
+ ...testCase.params,
+ envVars: {
+ UNFLAKABLE_API_KEY: "MOCK_API_KEY",
+ UNFLAKABLE_SUITE_ID: "MOCK_SUITE_ID",
+ ...testCase.params.envVars,
+ },
+ },
+ testCase.expectedExitCode,
+ testCase.expectedResults,
+ mockBackend
+ )
+ .then(done)
+ .catch((e) => {
+ // Ensures any chained `cause` gets printed.
+ done(util.inspect(e, { colors: true, depth: 5 }));
+ });
+};
+
+export const integrationTestSuite = (
+ runTests: (mockBackend: MockBackend) => void
+): void => {
+ const mockBackend = new MockBackend();
+
+ beforeEach(() => mockBackend.start());
+ afterEach(() => mockBackend.stop());
+
+ const jestMinorVersion = jestPackage.version.match(/^[^.]+\.[^.]+/);
+ const nodeMajorVersion = process.version.match(/^[^.]+/);
+
+ describe(`Jest ${
+ jestMinorVersion !== null ? jestMinorVersion[0] : jestPackage.version
+ }`, () => {
+ const platform = os.platform();
+ describe(
+ platform === "darwin"
+ ? `MacOS`
+ : platform === "linux"
+ ? "Linux"
+ : platform === "win32"
+ ? "Windows"
+ : platform,
+ () => {
+ // Only use Node major version for test name.
+ describe(`Node ${
+ nodeMajorVersion !== null ? nodeMajorVersion[0] : process.version
+ }`, () => {
+ runTests(mockBackend);
+ });
+ }
+ );
+ });
+};
diff --git a/packages/jest-plugin/test/integration/src/testNamePattern.test.ts b/packages/jest-plugin/test/integration/src/testNamePattern.test.ts
deleted file mode 100644
index 1669ba7..0000000
--- a/packages/jest-plugin/test/integration/src/testNamePattern.test.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
-
-import {
- defaultExpectedResults,
- integrationTest,
- integrationTestSuite,
-} from "./common";
-
-integrationTestSuite(() => {
- it("use --testNamePattern to have Jest filter tests", () =>
- integrationTest({
- params: {
- testNamePattern: "(should .*fail|should be flaky)",
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 4,
- failedTests: 2,
- flakyTests: 2,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 2,
- skippedTests: 4,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-
- it("use --testNamePattern with no matches", () =>
- integrationTest({
- params: {
- expectResultsToBeUploaded: false,
- skipFailures: true,
- testNamePattern: "no matches",
- },
- expectedExitCode: 0,
- expectedResults: {
- failedSuites: 0,
- failedTests: 0,
- flakyTests: 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 5,
- skippedTests: 8,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-
- it("use a permissive --testNamePattern and ensure only failed tests are retried", () =>
- integrationTest({
- params: {
- testNamePattern: ".*",
- },
- expectedExitCode: 1,
- expectedResults: defaultExpectedResults,
- }));
-
- it("use --testNamePattern with plugin disabled", () =>
- integrationTest({
- params: {
- envVars: {
- UNFLAKABLE_ENABLED: "false",
- },
- expectPluginToBeEnabled: false,
- testNamePattern: "(should .*fail|should be flaky)",
- },
- expectedExitCode: 1,
- expectedResults: {
- failedSuites: 4,
- failedTests: 4,
- flakyTests: 0,
- passedSuites: 0,
- passedTests: 0,
- quarantinedSuites: 0,
- quarantinedTests: 0,
- skippedSuites: 2,
- skippedTests: 4,
- passedSnapshots: 0,
- failedSnapshots: 0,
- totalSnapshots: 0,
- },
- }));
-});
diff --git a/packages/jest-plugin/test/integration/src/verify-output.ts b/packages/jest-plugin/test/integration/src/verify-output.ts
new file mode 100644
index 0000000..7fee318
--- /dev/null
+++ b/packages/jest-plugin/test/integration/src/verify-output.ts
@@ -0,0 +1,686 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+// These are the chalk-formatted strings that include console color codes.
+import escapeStringRegexp from "escape-string-regexp";
+import { MOCK_RUN_ID, ResultCounts, TestCaseParams } from "./run-test-case";
+import { TestAttemptResult } from "@unflakable/js-api";
+
+const FAIL_SYMBOL = process.platform === "win32" ? "×" : "✕";
+const PASS_SYMBOL = process.platform === "win32" ? "√" : "✓";
+
+const FAIL =
+ "\u001b[0m\u001b[7m\u001b[1m\u001b[31m FAIL \u001b[39m\u001b[22m\u001b[27m\u001b[0m";
+const PASS =
+ "\u001b[0m\u001b[7m\u001b[1m\u001b[32m PASS \u001b[39m\u001b[22m\u001b[27m\u001b[0m";
+const QUARANTINED =
+ "\u001b[0m\u001b[7m\u001b[1m\u001b[33m QUARANTINED \u001b[39m\u001b[22m\u001b[27m\u001b[0m";
+const SLOW_TEST_DETAIL_REGEX =
+ "\u001b\\[0m\u001b\\[1m\u001b\\[41m[0-9.]+ ?.?s\u001b\\[49m\u001b\\[22m\u001b\\[0m";
+const formatTestFilename = (dir: string, filename: string): string =>
+ `\u001b[2m${dir}\u001b[22m\u001b[1m${filename}\u001b[22m`;
+
+const specResultRegexMatch = (
+ result: "fail" | "pass" | "quarantined",
+ dir: string,
+ filename: string
+): RegExp =>
+ // NB: Includes possible slow test duration:
+ // https://github.com/jestjs/jest/blob/6d2632adae0f0fa1fe116d3b475fd9783d0de1b5/packages/jest-reporters/src/getResultHeader.ts#L43
+ new RegExp(
+ `^${escapeStringRegexp(
+ `${result === "quarantined" ? QUARANTINED + " " : ""}${
+ result === "pass" ? PASS : FAIL
+ } ${formatTestFilename(dir, filename)}`
+ )}(?: \\(${SLOW_TEST_DETAIL_REGEX}\\))?$`
+ );
+
+const testResultRegexMatch = (
+ result: TestAttemptResult | "skipped",
+ testName: string,
+ indent?: number,
+ isTestIndependent?: boolean
+): RegExp =>
+ new RegExp(
+ `^${" ".repeat(indent ?? 4)}${escapeStringRegexp(
+ result === "pass"
+ ? // Green
+ `\u001b[32m${PASS_SYMBOL}\u001b[39m`
+ : result === "fail"
+ ? // Red
+ `\u001b[31m${FAIL_SYMBOL}\u001b[39m`
+ : result === "quarantined"
+ ? // Yellow
+ `\u001b[33m${FAIL_SYMBOL}\u001b[39m`
+ : result === "skipped"
+ ? // Yellow
+ "\u001b[33mâ—‹\u001b[39m"
+ : ""
+ )} \u001b\\[2m${result === "skipped" ? "skipped " : ""}${escapeStringRegexp(
+ testName
+ )}${
+ isTestIndependent === true
+ ? escapeStringRegexp("\u001b[31m [test independent]\u001b[39m")
+ : ""
+ }${
+ result === "quarantined"
+ ? escapeStringRegexp("\u001b[33m [quarantined]\u001b[39m")
+ : ""
+ // Test duration is only included if the test takes at least 1ms.
+ }( \\([0-9]+ ms\\))?\u001b\\[22m$`,
+ ""
+ );
+
+export const verifyOutput = (
+ {
+ expectFailuresToBeTestIndependent,
+ expectFailuresFirstAttemptToBeTestIndependent,
+ expectFlakeToBeTestIndependent,
+ expectFlakeFirstAttemptToBeTestIndependent,
+ expectPluginToBeEnabled,
+ expectQuarantinedTestsToBeQuarantined,
+ expectQuarantinedTestsToBeSkipped,
+ expectQuarantinedTestsToBeTestIndependent,
+ expectQuarantinedTestsFirstAttemptToBeTestIndependent,
+ expectResultsToBeUploaded,
+ expectedFailureRetries,
+ expectedFlakeTestNameSuffix,
+ expectedSuiteId,
+ failToFetchManifest,
+ failToUploadResults,
+ flakeFailCount,
+ quarantineFlake,
+ skipFailures,
+ skipFlake,
+ skipQuarantined,
+ testNamePattern,
+ }: TestCaseParams,
+ stderrLines: (Uint8Array | string)[],
+ expectedResults: ResultCounts,
+ apiServerPort: number
+): void => {
+ // Make sure expected output is present and chalk-formatted correctly.
+
+ /* eslint-disable @typescript-eslint/unbound-method */
+
+ // Test our VerboseReporter customization.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(specResultRegexMatch("pass", "src/", "pass.test.ts")),
+ testNamePattern === undefined ||
+ "should pass".match(testNamePattern) !== null
+ ? 1
+ : 0
+ );
+
+ expect(stderrLines).toContainEqualTimes(
+ // This test doesn't have a describe() block, so it's only indented 2 spaces.
+ expect.stringMatching(testResultRegexMatch("pass", "should pass", 2)),
+ testNamePattern === undefined ||
+ "should pass".match(testNamePattern) !== null
+ ? 1
+ : 0
+ );
+
+ const numFailAttempts =
+ !skipFailures &&
+ (testNamePattern === undefined ||
+ "describe block should ([escape regex]?.*$ fail".match(
+ testNamePattern
+ ) !== null)
+ ? expectPluginToBeEnabled
+ ? 1 + expectedFailureRetries
+ : 1
+ : 0;
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(specResultRegexMatch("fail", "src/", "fail.test.ts")),
+ numFailAttempts
+ );
+
+ // Test-independent failure.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ "fail",
+ "should ([escape regex]?.*$ fail",
+ 4,
+ true // isTestIndependent
+ )
+ ),
+ expectPluginToBeEnabled &&
+ !skipFailures &&
+ (testNamePattern === undefined ||
+ "describe block should ([escape regex]?.*$ fail".match(
+ testNamePattern
+ ) !== null)
+ ? expectFailuresToBeTestIndependent
+ ? 1 + expectedFailureRetries
+ : expectFailuresFirstAttemptToBeTestIndependent
+ ? 1
+ : 0
+ : 0
+ );
+ // Non-test-independent failures.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch("fail", "should ([escape regex]?.*$ fail")
+ ),
+ !skipFailures &&
+ (testNamePattern === undefined ||
+ "describe block should ([escape regex]?.*$ fail".match(
+ testNamePattern
+ ) !== null)
+ ? expectPluginToBeEnabled
+ ? expectFailuresToBeTestIndependent
+ ? 0
+ : expectFailuresFirstAttemptToBeTestIndependent
+ ? expectedFailureRetries
+ : 1 + expectedFailureRetries
+ : 1
+ : 0
+ );
+
+ const flakyTest1Name = `should be flaky 1${expectedFlakeTestNameSuffix}`;
+ const flakyTest1ShouldRun =
+ !skipFlake &&
+ (!quarantineFlake ||
+ failToFetchManifest ||
+ !expectQuarantinedTestsToBeSkipped) &&
+ (testNamePattern === undefined ||
+ flakyTest1Name.match(testNamePattern) !== null);
+
+ // This test should fail then pass (though we're not verifying the order here).
+ // Test-independent failure.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ quarantineFlake &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ flakyTest1Name,
+ 2,
+ true
+ )
+ ),
+ expectPluginToBeEnabled && flakyTest1ShouldRun
+ ? expectFlakeToBeTestIndependent
+ ? flakeFailCount
+ : expectFlakeFirstAttemptToBeTestIndependent
+ ? 1
+ : 0
+ : 0
+ );
+ // Non-test-independent failures.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ quarantineFlake &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ flakyTest1Name,
+ 2
+ )
+ ),
+ flakyTest1ShouldRun
+ ? expectPluginToBeEnabled
+ ? expectFlakeToBeTestIndependent
+ ? 0 // These all ran above
+ : expectFlakeFirstAttemptToBeTestIndependent
+ ? Math.max(0, flakeFailCount - 1)
+ : flakeFailCount
+ : 1 // Plugin disabled
+ : 0
+ );
+
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(testResultRegexMatch("pass", flakyTest1Name, 2)),
+ expectPluginToBeEnabled && expectedFailureRetries > 0 && flakyTest1ShouldRun
+ ? 1
+ : 0
+ );
+
+ const flakyTest2Name = `should be flaky 2${expectedFlakeTestNameSuffix}`;
+ const flakyTest2ShouldRun =
+ !skipFlake &&
+ (!quarantineFlake ||
+ failToFetchManifest ||
+ !expectQuarantinedTestsToBeSkipped) &&
+ (testNamePattern === undefined ||
+ flakyTest2Name.match(testNamePattern) !== null);
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ quarantineFlake &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ flakyTest2Name,
+ 2,
+ expectFlakeToBeTestIndependent
+ )
+ ),
+ flakyTest2ShouldRun ? 1 : 0
+ );
+
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(testResultRegexMatch("pass", flakyTest2Name, 2)),
+ expectPluginToBeEnabled && expectedFailureRetries > 0 && flakyTest2ShouldRun
+ ? 1
+ : 0
+ );
+
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch(
+ quarantineFlake &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ "src/",
+ "flake.test.ts"
+ )
+ ),
+ flakyTest1ShouldRun || flakyTest2ShouldRun ? flakeFailCount : 0
+ );
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch("pass", "src/", "flake.test.ts")
+ ),
+ expectPluginToBeEnabled &&
+ (flakyTest1ShouldRun || flakyTest2ShouldRun) &&
+ expectedFailureRetries > 0
+ ? 1
+ : 0
+ );
+
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch("fail", "src/", "invalid.test.ts")
+ ),
+ !skipFailures ? 1 : 0
+ );
+
+ const numQuarantinedAttempts =
+ !skipQuarantined &&
+ (!expectQuarantinedTestsToBeSkipped || failToFetchManifest) &&
+ (testNamePattern === undefined ||
+ "describe block should be quarantined".match(testNamePattern) !== null)
+ ? expectPluginToBeEnabled
+ ? 1 + expectedFailureRetries
+ : 1
+ : 0;
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch(
+ expectPluginToBeEnabled &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined &&
+ !expectQuarantinedTestsToBeSkipped
+ ? "quarantined"
+ : "fail",
+ "src/",
+ "quarantined.test.ts"
+ )
+ ),
+ numQuarantinedAttempts
+ );
+
+ // Test-independent failure.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ expectPluginToBeEnabled &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ "should be quarantined",
+ 4,
+ true
+ )
+ ),
+ expectPluginToBeEnabled &&
+ !skipQuarantined &&
+ (testNamePattern === undefined ||
+ "describe block should be quarantined".match(testNamePattern) !==
+ null) &&
+ !expectQuarantinedTestsToBeSkipped
+ ? expectQuarantinedTestsToBeTestIndependent
+ ? 1 + expectedFailureRetries
+ : expectQuarantinedTestsFirstAttemptToBeTestIndependent
+ ? 1
+ : 0
+ : 0
+ );
+ // Non-test-independent failures.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ expectPluginToBeEnabled &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ "should be quarantined"
+ )
+ ),
+ !skipQuarantined &&
+ (testNamePattern === undefined ||
+ "describe block should be quarantined".match(testNamePattern) !==
+ null) &&
+ !expectQuarantinedTestsToBeSkipped
+ ? expectPluginToBeEnabled
+ ? expectQuarantinedTestsToBeTestIndependent
+ ? 0
+ : expectQuarantinedTestsFirstAttemptToBeTestIndependent
+ ? expectedFailureRetries
+ : 1 + expectedFailureRetries
+ : 1
+ : 0
+ );
+
+ const mixedFailTestShouldRun =
+ !skipFailures &&
+ (testNamePattern === undefined ||
+ "mixed mixed: should fail".match(testNamePattern) !== null);
+ const mixedQuarantinedTestShouldRun =
+ !expectQuarantinedTestsToBeSkipped &&
+ !skipQuarantined &&
+ (testNamePattern === undefined ||
+ "mixed mixed: should be quarantined".match(testNamePattern) !== null);
+ const mixedPassTestShouldRun =
+ testNamePattern === undefined ||
+ "mixed mixed: should pass".match(testNamePattern) !== null;
+
+ // Mixed file containing both a failed test and a quarantined one.
+ const numMixedFailingAttempts =
+ ((!expectPluginToBeEnabled ||
+ failToFetchManifest ||
+ !expectQuarantinedTestsToBeQuarantined) &&
+ mixedQuarantinedTestShouldRun) ||
+ mixedFailTestShouldRun
+ ? expectPluginToBeEnabled
+ ? 1 + expectedFailureRetries
+ : 1
+ : 0;
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch("fail", "src/", "mixed.test.ts")
+ ),
+ numMixedFailingAttempts
+ );
+ const numMixedQuarantinedAttempts =
+ expectPluginToBeEnabled &&
+ !failToFetchManifest &&
+ !mixedFailTestShouldRun &&
+ mixedQuarantinedTestShouldRun &&
+ expectQuarantinedTestsToBeQuarantined
+ ? 1 + expectedFailureRetries
+ : 0;
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch("quarantined", "src/", "mixed.test.ts")
+ ),
+ numMixedQuarantinedAttempts
+ );
+ const hasMixedPassingAttempt = skipFailures && skipQuarantined;
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ specResultRegexMatch("pass", "src/", "mixed.test.ts")
+ ),
+ hasMixedPassingAttempt ? 1 : 0
+ );
+
+ // Test-independent failure.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ expectPluginToBeEnabled &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ "mixed: should be quarantined",
+ 4,
+ true // isTestIndependent
+ )
+ ),
+ expectPluginToBeEnabled && mixedQuarantinedTestShouldRun
+ ? expectQuarantinedTestsToBeTestIndependent
+ ? 1 + expectedFailureRetries
+ : expectQuarantinedTestsFirstAttemptToBeTestIndependent
+ ? 1
+ : 0
+ : 0
+ );
+ // Non-test-independent failures.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ expectPluginToBeEnabled &&
+ !failToFetchManifest &&
+ expectQuarantinedTestsToBeQuarantined
+ ? "quarantined"
+ : "fail",
+ "mixed: should be quarantined"
+ )
+ ),
+ mixedQuarantinedTestShouldRun
+ ? expectPluginToBeEnabled
+ ? expectQuarantinedTestsToBeTestIndependent
+ ? 0
+ : expectQuarantinedTestsFirstAttemptToBeTestIndependent
+ ? expectedFailureRetries
+ : 1 + expectedFailureRetries
+ : 1
+ : 0
+ );
+
+ // Test-independent failure.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch(
+ "fail",
+ "mixed: should fail",
+ 4,
+ true // isTestIndependent
+ )
+ ),
+ expectPluginToBeEnabled && mixedFailTestShouldRun
+ ? expectFailuresToBeTestIndependent
+ ? 1 + expectedFailureRetries
+ : expectFailuresFirstAttemptToBeTestIndependent
+ ? 1
+ : 0
+ : 0
+ );
+ // Non-test-independent failures.
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(testResultRegexMatch("fail", "mixed: should fail")),
+ mixedFailTestShouldRun
+ ? expectPluginToBeEnabled
+ ? expectFailuresToBeTestIndependent
+ ? 0
+ : expectFailuresFirstAttemptToBeTestIndependent
+ ? expectedFailureRetries
+ : 1 + expectedFailureRetries
+ : 1
+ : 0
+ );
+
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(testResultRegexMatch("pass", "mixed: should pass")),
+ mixedPassTestShouldRun ? 1 : 0
+ );
+ expect(stderrLines).toContainEqualTimes(
+ expect.stringMatching(
+ testResultRegexMatch("skipped", "mixed: should pass")
+ ),
+ mixedFailTestShouldRun || mixedQuarantinedTestShouldRun
+ ? testNamePattern !== undefined &&
+ "mixed mixed: should pass".match(testNamePattern) === null &&
+ expectPluginToBeEnabled
+ ? expectedFailureRetries + 1
+ : expectPluginToBeEnabled
+ ? expectedFailureRetries
+ : testNamePattern !== undefined &&
+ "mixed mixed: should pass".match(testNamePattern) === null
+ ? 1
+ : 0
+ : 0
+ );
+
+ for (let attempt = 1; attempt < expectedFailureRetries; attempt++) {
+ const numTestsToRetry =
+ (!skipFailures && numFailAttempts > attempt ? 1 : 0) +
+ (flakeFailCount + 1 > attempt
+ ? (flakyTest1ShouldRun ? 1 : 0) + (flakyTest2ShouldRun ? 1 : 0)
+ : 0) +
+ (numQuarantinedAttempts > attempt ? 1 : 0) +
+ (numMixedFailingAttempts + numMixedQuarantinedAttempts > attempt
+ ? (mixedFailTestShouldRun ? 1 : 0) +
+ (mixedQuarantinedTestShouldRun ? 1 : 0)
+ : 0);
+ const numTestFilesToRetry =
+ (!skipFailures && numFailAttempts > attempt ? 1 : 0) +
+ ((flakyTest1ShouldRun || flakyTest2ShouldRun) &&
+ flakeFailCount + 1 > attempt
+ ? 1
+ : 0) +
+ (numQuarantinedAttempts > attempt ? 1 : 0) +
+ ((mixedFailTestShouldRun || mixedQuarantinedTestShouldRun) &&
+ numMixedFailingAttempts + numMixedQuarantinedAttempts > attempt
+ ? 1
+ : 0);
+ expect(numTestsToRetry).toBeGreaterThanOrEqual(numTestFilesToRetry);
+ expect(stderrLines).toContainEqualTimes(
+ `\u001b[33m\u001b[1mRetrying ${numTestsToRetry} failed test${
+ numTestsToRetry === 1 ? "" : "s"
+ } from ${numTestFilesToRetry} file${
+ numTestFilesToRetry === 1 ? "" : "s"
+ } -- ${expectedFailureRetries - attempt} ${
+ expectedFailureRetries - attempt === 1 ? "retry" : "retries"
+ } remaining\u001b[22m\u001b[39m`,
+ expectPluginToBeEnabled && numTestsToRetry > 0 ? 1 : 0
+ );
+ }
+
+ // Test our SummaryReporter customization.
+ expect(stderrLines).toContain(
+ `\u001b[1mTest Suites: \u001b[22m${
+ expectedResults.failedSuites !== 0
+ ? `\u001b[1m\u001b[31m${expectedResults.failedSuites} failed\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.quarantinedSuites !== 0
+ ? `\u001b[1m\u001b[33m${expectedResults.quarantinedSuites} quarantined\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.skippedSuites !== 0
+ ? `\u001b[1m\u001b[33m${expectedResults.skippedSuites} skipped\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.passedSuites !== 0
+ ? `\u001b[1m\u001b[32m${expectedResults.passedSuites} passed${
+ expectedResults.passedSuitesWithIndependentFailures > 0
+ ? ` (${expectedResults.passedSuitesWithIndependentFailures} with test-independent failures)`
+ : ""
+ }\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.skippedSuites !== 0
+ ? `${
+ expectedResults.failedSuites +
+ expectedResults.quarantinedSuites +
+ expectedResults.passedSuites
+ } of ${
+ expectedResults.failedSuites +
+ expectedResults.quarantinedSuites +
+ expectedResults.passedSuites +
+ expectedResults.skippedSuites
+ }`
+ : expectedResults.failedSuites +
+ expectedResults.quarantinedSuites +
+ expectedResults.passedSuites
+ } total`
+ );
+
+ expect(stderrLines).toContain(
+ `\u001b[1mTests: \u001b[22m${
+ expectedResults.failedTests !== 0
+ ? `\u001b[1m\u001b[31m${expectedResults.failedTests} failed\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.flakyTests !== 0
+ ? `\u001b[1m\u001b[95m${expectedResults.flakyTests} flaky\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.quarantinedTests !== 0
+ ? `\u001b[1m\u001b[33m${expectedResults.quarantinedTests} quarantined\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.skippedTests !== 0
+ ? `\u001b[1m\u001b[33m${expectedResults.skippedTests} skipped\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.passedTests !== 0
+ ? `\u001b[1m\u001b[32m${expectedResults.passedTests} passed${
+ expectedResults.passedTestsWithIndependentFailures > 0
+ ? ` (${expectedResults.passedTestsWithIndependentFailures} with test-independent failures)`
+ : ""
+ }\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.failedTests +
+ expectedResults.flakyTests +
+ expectedResults.quarantinedTests +
+ expectedResults.passedTests +
+ expectedResults.skippedTests
+ } total`
+ );
+
+ expect(stderrLines).toContain(
+ `\u001b[1mSnapshots: \u001b[22m${
+ expectedResults.failedSnapshots > 0
+ ? `\u001b[1m\u001b[31m${expectedResults.failedSnapshots} failed\u001b[39m\u001b[22m, `
+ : ""
+ }${
+ expectedResults.passedSnapshots > 0
+ ? `\u001b[1m\u001b[32m${expectedResults.passedSnapshots} passed\u001b[39m\u001b[22m, `
+ : ""
+ }${expectedResults.totalSnapshots} total`
+ );
+ // None of the snapshots should be obsolete.
+ expect(stderrLines).not.toContainEqual(
+ expect.stringMatching(new RegExp("[0-9]+ snapshot(:?s)? obsolete"))
+ );
+
+ // The duration here is based on the mocked time, so it should be deterministic.
+ expect(stderrLines).toContainEqual(
+ expect.stringMatching(
+ new RegExp(
+ `${escapeStringRegexp(
+ "\u001b[1mTime:\u001b[22m "
+ )}(?:\u001b\\[1m\u001b\\[33m[0-9.]+ s\u001b\\[39m\u001b\\[22m|[0-9.]+ s)(?:, estimated [0-9.]+ s)?`
+ )
+ )
+ );
+
+ expect(stderrLines).toContain(
+ `\u001b[2mRan all test suites\u001b[22m\u001b[2m${
+ testNamePattern !== undefined
+ ? ` with tests matching \u001b[22m"${testNamePattern}"\u001b[2m`
+ : ""
+ }.\u001b[22m`
+ );
+
+ (expectPluginToBeEnabled && expectResultsToBeUploaded && !failToUploadResults
+ ? expect(stderrLines).toContain
+ : expect(stderrLines).not.toContain)(
+ `Unflakable report: http://localhost:${apiServerPort}/test-suites/${expectedSuiteId}/runs/${MOCK_RUN_ID}`
+ );
+};
diff --git a/packages/jest-plugin/tsconfig.json b/packages/jest-plugin/tsconfig.json
index c2f62b5..97b22c5 100644
--- a/packages/jest-plugin/tsconfig.json
+++ b/packages/jest-plugin/tsconfig.json
@@ -7,19 +7,21 @@
"module": "esnext",
// Ensure Rollup build fails if there are type errors.
"noEmitOnError": true,
- // Removes DOM types.
- "lib": ["ES2019"],
+ // Remove DOM types and support 2-argument Error constructor that takes a cause.
+ "lib": ["ES2022"],
// Avoids conflicting global definitions from, e.g., jasmine.
"types": ["node", "jest"],
// Some versions of Jest (e.g., 28.0.0) have internally broken types.
- "skipLibCheck": true,
- "sourceMap": true
+ "skipLibCheck": true
},
"include": [
".eslintrc.js",
+ "jest.config.js",
+ "jest-circus.d.ts",
+ "rollup.config.mjs",
"src",
"test/.eslintrc.js",
- "rollup.config.mjs",
+ "test/babel.config.js",
"window.d.ts"
]
}
diff --git a/packages/jest-plugin/window.d.ts b/packages/jest-plugin/window.d.ts
index acaa11a..e0f548f 100644
--- a/packages/jest-plugin/window.d.ts
+++ b/packages/jest-plugin/window.d.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
export {};
diff --git a/packages/js-api/.eslintrc.js b/packages/js-api/.eslintrc.js
index 02409c8..ff96017 100644
--- a/packages/js-api/.eslintrc.js
+++ b/packages/js-api/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
extends: ["../../.eslintrc-ts.js"],
diff --git a/packages/js-api/LICENSE b/packages/js-api/LICENSE
index 53bf01b..fc24ed8 100644
--- a/packages/js-api/LICENSE
+++ b/packages/js-api/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022-2023 Developer Innovations, LLC
+Copyright (c) 2022-2024 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
diff --git a/packages/js-api/README.md b/packages/js-api/README.md
index 6479c9f..f106ae7 100644
--- a/packages/js-api/README.md
+++ b/packages/js-api/README.md
@@ -5,7 +5,6 @@
[](https://www.npmjs.com/package/@unflakable/js-api)
-[](https://twitter.com/unflakable)
# Unflakable JavaScript API
diff --git a/packages/js-api/package.json b/packages/js-api/package.json
index 6ea366b..d3a805e 100644
--- a/packages/js-api/package.json
+++ b/packages/js-api/package.json
@@ -8,7 +8,7 @@
"bugs": "https://github.com/unflakable/unflakable-javascript/issues",
"homepage": "https://unflakable.com",
"license": "MIT",
- "version": "0.3.0",
+ "version": "0.3.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
@@ -32,10 +32,11 @@
},
"devDependencies": {
"@types/async-retry": "^1.4.5",
- "@types/node-fetch": "^2.6.2"
+ "@types/node-fetch": "^2.6.2",
+ "rimraf": "^5.0.1"
},
"scripts": {
- "build": "rm -rf dist && tsc --noEmit && tsc -p src",
+ "build": "rimraf dist && tsc --noEmit && tsc -p src",
"build:watch": "tsc --build --watch"
}
}
diff --git a/packages/js-api/src/consts.ts b/packages/js-api/src/consts.ts
index cf19eab..8823e7f 100644
--- a/packages/js-api/src/consts.ts
+++ b/packages/js-api/src/consts.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
export const JS_API_VERSION: string =
// eslint-disable-next-line @typescript-eslint/no-var-requires
diff --git a/packages/js-api/src/index.ts b/packages/js-api/src/index.ts
index 778720c..5ff206e 100644
--- a/packages/js-api/src/index.ts
+++ b/packages/js-api/src/index.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import nodeFetch, { RequestInit, Response } from "node-fetch";
import _debug = require("debug");
@@ -15,7 +15,7 @@ const fetch = (url: string, init?: RequestInit): Promise => {
return retry(
() =>
nodeFetch(url, init).then((response) => {
- if (response.status === 503) {
+ if (response.status >= 500 && response.status <= 599) {
throw new Error(
`Server returned ${response.status} ${response.statusText}`
);
@@ -50,10 +50,12 @@ export type TestSuiteManifest = {
};
export type TestAttemptResult = "pass" | "fail" | "quarantined";
+export type TestAttemptFailureReason = "independent";
export type TestRunAttemptRecord = {
start_time?: string;
end_time?: string;
duration_ms?: number;
+ failure_reason?: TestAttemptFailureReason;
result: TestAttemptResult;
};
export type TestRunRecord = {
diff --git a/packages/plugins-common/.eslintrc.js b/packages/plugins-common/.eslintrc.js
index 02409c8..ff96017 100644
--- a/packages/plugins-common/.eslintrc.js
+++ b/packages/plugins-common/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
extends: ["../../.eslintrc-ts.js"],
diff --git a/packages/plugins-common/README.md b/packages/plugins-common/README.md
index 16369e5..f9b835d 100644
--- a/packages/plugins-common/README.md
+++ b/packages/plugins-common/README.md
@@ -4,8 +4,6 @@
-[](https://twitter.com/unflakable)
-
# Unflakable JavaScript Plugins Common Library
This package contains code to be shared between all the Unflakable JavaScript plugins.
diff --git a/packages/plugins-common/package.json b/packages/plugins-common/package.json
index ac4dd1b..9315964 100644
--- a/packages/plugins-common/package.json
+++ b/packages/plugins-common/package.json
@@ -24,10 +24,11 @@
"@unflakable/js-api": "workspace:^",
"cosmiconfig": "^7.0.1",
"debug": "^4.3.3",
+ "rimraf": "^5.0.1",
"simple-git": "^3.16.0"
},
"scripts": {
- "build": "rm -rf dist && tsc --noEmit && tsc -p src",
+ "build": "rimraf dist && tsc --noEmit && tsc -p src",
"build:watch": "tsc --build --watch"
}
}
diff --git a/packages/plugins-common/src/config.ts b/packages/plugins-common/src/config.ts
index e0434d2..23c171f 100644
--- a/packages/plugins-common/src/config.ts
+++ b/packages/plugins-common/src/config.ts
@@ -1,27 +1,39 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import { CosmiconfigResult } from "cosmiconfig/dist/types";
import { cosmiconfig, cosmiconfigSync } from "cosmiconfig";
import _debug from "debug";
import { EnvVar, suiteIdOverride, uploadResultsOverride } from "./env";
+import * as util from "util";
const debug = _debug("unflakable:plugins-common:config");
export type QuarantineMode = "no_quarantine" | "skip_tests" | "ignore_failures";
-export type UnflakableConfig = {
+type UnflakableConfigInner = {
apiBaseUrl: string | undefined;
- enabled: boolean;
failureRetries: number;
gitAutoDetect: boolean;
quarantineMode: QuarantineMode;
- testSuiteId: string;
uploadResults: boolean;
};
-type UnflakableConfigFile = Omit & {
+export type UnflakableConfigEnabled = {
+ enabled: true;
+ testSuiteId: string;
+} & UnflakableConfigInner;
+
+export type UnflakableConfig =
+ | UnflakableConfigEnabled
+ | ({
+ enabled: false;
+ testSuiteId: string | undefined;
+ } & UnflakableConfigInner);
+
+export type UnflakableConfigFile = {
+ enabled: boolean;
testSuiteId: string | undefined;
-};
+} & UnflakableConfigInner;
const defaultConfig: UnflakableConfigFile = {
apiBaseUrl: undefined,
@@ -33,9 +45,10 @@ const defaultConfig: UnflakableConfigFile = {
uploadResults: true,
};
-const validateConfig = (
- configResult: NonNullable
-): UnflakableConfigFile => {
+const validateConfig = (
+ configResult: NonNullable,
+ validateExtraConfig: (config: CosmiconfigResult) => [T, string[]]
+): UnflakableConfigFile & T => {
debug(`Loaded config from ${configResult.filepath}`);
// NB: typeof null is "object"
if (typeof configResult.config !== "object" || configResult.config === null) {
@@ -46,11 +59,13 @@ const validateConfig = (
);
}
+ const [extraConfig, validExtraConfigKeys] = validateExtraConfig(configResult);
+
return Object.entries(configResult.config as { [s: string]: unknown }).reduce(
- (result: UnflakableConfigFile, [key, value]: [string, unknown]) => {
+ (result: UnflakableConfigFile & T, [key, value]: [string, unknown]) => {
const throwUnexpected = (): never => {
throw new Error(
- `Unexpected value \`${JSON.stringify(value)}\` for ${JSON.stringify(
+ `Unexpected value \`${util.format(value)}\` for ${JSON.stringify(
key
)} found in ${configResult.filepath}`
);
@@ -128,12 +143,16 @@ const validateConfig = (
return throwUnexpected();
}
default:
- throw new Error(
- `Unknown Unflakable config option \`${key}\` found in ${configResult.filepath}`
- );
+ if (!validExtraConfigKeys.includes(key)) {
+ throw new Error(
+ `Unknown Unflakable config option \`${key}\` found in ${configResult.filepath}`
+ );
+ }
+
+ return result;
}
},
- { ...defaultConfig }
+ { ...defaultConfig, ...extraConfig }
);
};
@@ -141,7 +160,10 @@ const apiBaseUrlOverride = new EnvVar("UNFLAKABLE_API_BASE_URL");
const apiKey = new EnvVar("UNFLAKABLE_API_KEY");
const enabledOverride = new EnvVar("UNFLAKABLE_ENABLED");
-const mergeConfigWithEnv = (config: UnflakableConfigFile): UnflakableConfig => {
+const mergeConfigWithEnv = (
+ config: UnflakableConfigFile,
+ cliTestSuiteId?: string
+): UnflakableConfig => {
if (enabledOverride.value !== undefined) {
const enabled = !["false", "0"].includes(enabledOverride.value);
debug(
@@ -177,7 +199,13 @@ const mergeConfigWithEnv = (config: UnflakableConfigFile): UnflakableConfig => {
config.uploadResults = uploadResults;
}
- if (suiteIdOverride.value !== undefined && suiteIdOverride.value.length > 0) {
+ if (cliTestSuiteId !== undefined) {
+ debug(`Using suite ID \`${cliTestSuiteId}\` from CLI`);
+ return { ...config, testSuiteId: cliTestSuiteId };
+ } else if (
+ suiteIdOverride.value !== undefined &&
+ suiteIdOverride.value.length > 0
+ ) {
debug(
`Using suite ID \`${suiteIdOverride.value}\` from environment variable ${suiteIdOverride.name}`
);
@@ -187,12 +215,17 @@ const mergeConfigWithEnv = (config: UnflakableConfigFile): UnflakableConfig => {
config.testSuiteId.length > 0
) {
debug(`Using suite ID \`${config.testSuiteId}\` from config file`);
- return config as UnflakableConfig;
- } else {
+ return config.enabled
+ ? // TypeScript has trouble inferring that these types are correct otherwise.
+ { ...config, enabled: true, testSuiteId: config.testSuiteId }
+ : { ...config, enabled: false };
+ } else if (config.enabled) {
throw new Error(
`Unflakable test suite ID not found in config file or ${suiteIdOverride.name} environment ` +
"variable"
);
+ } else {
+ return { ...config, enabled: false };
}
};
@@ -214,9 +247,18 @@ export const setCosmiconfig = (config: typeof cosmiconfig): void => {
).__unflakableCosmiconfig = config;
};
-const loadConfigFile = async (
- searchFrom: string
-): Promise => {
+export const setCosmiconfigSync = (config: typeof cosmiconfigSync): void => {
+ (
+ globalThis as {
+ __unflakableCosmiconfigSync?: typeof cosmiconfigSync;
+ }
+ ).__unflakableCosmiconfigSync = config;
+};
+
+const loadConfigFile = async (
+ searchFrom: string,
+ validateExtraConfig: (config: CosmiconfigResult) => [T, string[]]
+): Promise => {
const configExplorer = (
(
globalThis as {
@@ -229,32 +271,60 @@ const loadConfigFile = async (
debug(`Searching for config from directory \`${searchFrom}\` upward`);
const configResult = await configExplorer.search(searchFrom);
if (configResult !== null) {
- return validateConfig(configResult);
+ return validateConfig(configResult, validateExtraConfig);
} else {
debug("No config file found; using defaults");
- return { ...defaultConfig };
+ return {
+ ...defaultConfig,
+ ...((validateExtraConfig !== undefined
+ ? validateExtraConfig(configResult)
+ : {}) as T),
+ };
}
};
-export const loadConfig = (searchFrom: string): Promise =>
- loadConfigFile(searchFrom).then(mergeConfigWithEnv);
+export const loadConfig = (
+ searchFrom: string,
+ validateExtraConfig: (config: CosmiconfigResult) => [T, string[]],
+ testSuiteId?: string
+): Promise =>
+ loadConfigFile(searchFrom, validateExtraConfig).then((config) =>
+ mergeConfigWithEnv(config, testSuiteId)
+ );
-const loadConfigFileSync = (searchFrom: string): UnflakableConfigFile => {
- const configExplorer = cosmiconfigSync("unflakable", {
+const loadConfigFileSync = (
+ searchFrom: string,
+ validateExtraConfig: (config: CosmiconfigResult) => [T, string[]]
+): UnflakableConfigFile & T => {
+ const configExplorer = (
+ (
+ globalThis as {
+ __unflakableCosmiconfigSync?: typeof cosmiconfigSync;
+ }
+ ).__unflakableCosmiconfigSync ?? cosmiconfigSync
+ )("unflakable", {
searchPlaces: SEARCH_PLACES,
});
debug(`Searching for config from directory \`${searchFrom}\` upward`);
const configResult = configExplorer.search(searchFrom);
if (configResult !== null) {
- return validateConfig(configResult);
+ return validateConfig(configResult, validateExtraConfig);
} else {
debug("No config file found; using defaults");
- return { ...defaultConfig };
+ return {
+ ...defaultConfig,
+ ...((validateExtraConfig !== undefined
+ ? validateExtraConfig(configResult)
+ : {}) as T),
+ };
}
};
-export const loadConfigSync = (searchFrom: string): UnflakableConfig =>
- mergeConfigWithEnv(loadConfigFileSync(searchFrom));
+export const loadConfigSync = (
+ searchFrom: string,
+ validateExtraConfig: (config: CosmiconfigResult) => [T, string[]]
+): UnflakableConfig & T =>
+ mergeConfigWithEnv(loadConfigFileSync(searchFrom, validateExtraConfig));
export const loadApiKey = (): string => {
if (apiKey.value !== undefined && apiKey.value !== "") {
diff --git a/packages/plugins-common/src/env.ts b/packages/plugins-common/src/env.ts
index 8ac5d40..a89b628 100644
--- a/packages/plugins-common/src/env.ts
+++ b/packages/plugins-common/src/env.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import process from "process";
diff --git a/packages/plugins-common/src/git.ts b/packages/plugins-common/src/git.ts
index 7a1e104..e2907f7 100644
--- a/packages/plugins-common/src/git.ts
+++ b/packages/plugins-common/src/git.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
import _debug from "debug";
import { simpleGit, SimpleGit, SimpleGitFactory } from "simple-git";
@@ -84,22 +84,20 @@ export const loadGitRepo = async (): Promise => {
};
export const autoDetectGit = async (
+ git: SimpleGit,
log: (message: string) => void
): Promise<{
branch: string | undefined;
commit: string | undefined;
}> => {
try {
- const git = await loadGitRepo();
- if (git !== null) {
- const commit = await getCurrentGitCommit(git);
- const branch = await getCurrentGitBranch(git, commit);
+ const commit = await getCurrentGitCommit(git);
+ const branch = await getCurrentGitBranch(git, commit);
- return {
- branch,
- commit,
- };
- }
+ return {
+ branch,
+ commit,
+ };
} catch (e) {
log(
`WARNING: Unflakable failed to auto-detect current git branch and commit: ${
diff --git a/packages/plugins-common/src/index.ts b/packages/plugins-common/src/index.ts
index c934470..5e9e953 100644
--- a/packages/plugins-common/src/index.ts
+++ b/packages/plugins-common/src/index.ts
@@ -1,12 +1,17 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import path from "path";
export {
QuarantineMode,
UnflakableConfig,
+ UnflakableConfigEnabled,
+ UnflakableConfigFile,
loadApiKey,
loadConfig,
loadConfigSync,
setCosmiconfig,
+ setCosmiconfigSync,
} from "./config";
export {
EnvVar,
@@ -23,3 +28,8 @@ export {
} from "./git";
export { getTestSuiteManifest } from "./manifest";
export { isTestQuarantined, normalizeTestName } from "./quarantine";
+
+// On Windows, we need to convert backslashes to forward slashes before reporting results to the
+// backend or checking whether tests are quarantined.
+export const toPosix = (file: string): string =>
+ file.split(path.sep).join(path.posix.sep);
diff --git a/packages/plugins-common/src/manifest.ts b/packages/plugins-common/src/manifest.ts
index f118f69..c552b0c 100644
--- a/packages/plugins-common/src/manifest.ts
+++ b/packages/plugins-common/src/manifest.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
import _debug from "debug";
import {
@@ -30,9 +30,10 @@ export const getTestSuiteManifest = ({
})
.catch((e: Error) => {
log(
- chalk.red(
- `ERROR: Failed to get Unflakable manifest: ${e.toString()}\n`
- ) + chalk.yellow.bold("Test failures will NOT be quarantined.\n")
+ chalk.red(`ERROR: Failed to get Unflakable manifest: ${e.toString()}`) +
+ "\n" +
+ chalk.yellow.bold("Test failures will NOT be quarantined.") +
+ "\n"
);
return undefined;
})
diff --git a/packages/plugins-common/src/quarantine.ts b/packages/plugins-common/src/quarantine.ts
index 0fc27f5..e8c83fb 100644
--- a/packages/plugins-common/src/quarantine.ts
+++ b/packages/plugins-common/src/quarantine.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
// 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.
@@ -31,13 +31,13 @@ export const normalizeTestName = (fullTestName: string[]): string[] =>
export const isTestQuarantined = (
manifest: TestSuiteManifest,
- testFilename: string,
+ posixTestFilename: string,
fullTestName: string[]
): boolean => {
const testName = normalizeTestName(fullTestName);
return manifest.quarantined_tests.some(
(quarantinedTest) =>
- quarantinedTest.filename === testFilename &&
+ quarantinedTest.filename === posixTestFilename &&
deepEqual(quarantinedTest.name, testName)
);
};
diff --git a/packages/plugins-common/src/tsconfig.json b/packages/plugins-common/src/tsconfig.json
index b2d7ce7..4810377 100644
--- a/packages/plugins-common/src/tsconfig.json
+++ b/packages/plugins-common/src/tsconfig.json
@@ -7,6 +7,7 @@
"lib": ["ES2019"],
// Required by Rollup (consumed by Rollup in the *-plugin packages).
"module": "esnext",
+ "sourceMap": true,
// Avoids conflicting global definitions from, e.g., jasmine.
"types": ["node"]
},
diff --git a/packages/test-common/.eslintrc.js b/packages/test-common/.eslintrc.js
new file mode 100644
index 0000000..ff96017
--- /dev/null
+++ b/packages/test-common/.eslintrc.js
@@ -0,0 +1,5 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+module.exports = {
+ extends: ["../../.eslintrc-ts.js"],
+};
diff --git a/packages/cypress-plugin/test/integration-common/package.json b/packages/test-common/package.json
similarity index 51%
rename from packages/cypress-plugin/test/integration-common/package.json
rename to packages/test-common/package.json
index 0749244..548d460 100644
--- a/packages/cypress-plugin/test/integration-common/package.json
+++ b/packages/test-common/package.json
@@ -1,34 +1,28 @@
{
- "name": "cypress-integration-common",
+ "name": "unflakable-test-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": {
+ "cosmiconfig": "^7.0.1",
"debug": "^4.3.3",
- "expect": "^29.5.0",
- "simple-git": "^3.16.0"
+ "deep-equal": "^2.0.5",
+ "expect": "25.1.0 - 29",
+ "mockttp": "^3.9.2",
+ "simple-git": "^3.16.0",
+ "tree-kill": "^1.2.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-typescript": "^11.1.1",
+ "@types/jest": "25.1.0 - 29",
"@unflakable/plugins-common": "workspace:^",
+ "rimraf": "^5.0.1",
"rollup": "^3.21.1",
+ "rollup-plugin-dts": "^5.3.0",
"typescript": "^4.9.5"
},
"scripts": {
"build": "yarn clean && tsc --noEmit && tsc --noEmit -p src && rollup --config",
- "clean": "rm -rf dist/"
+ "clean": "rimraf dist/"
}
}
diff --git a/packages/test-common/rollup.config.mjs b/packages/test-common/rollup.config.mjs
new file mode 100644
index 0000000..9ad0ca1
--- /dev/null
+++ b/packages/test-common/rollup.config.mjs
@@ -0,0 +1,75 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import path from "path";
+import pluginCommonJs from "@rollup/plugin-commonjs";
+import pluginNodeResolve from "@rollup/plugin-node-resolve";
+import pluginTypescript from "@rollup/plugin-typescript";
+import pluginDts from "rollup-plugin-dts";
+
+// 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").IsExternal}
+ */
+const isExternal = (id) =>
+ !id.startsWith(".") &&
+ !path.isAbsolute(id) &&
+ !id.startsWith("src/") &&
+ !["@unflakable/plugins-common"].includes(id);
+
+/**
+ * @type {import("rollup").NormalizedInputOptions[]}
+ */
+export default [
+ {
+ input: [
+ "src/config.ts",
+ "src/git.ts",
+ "src/mock-backend.ts",
+ "src/mock-cosmiconfig.ts",
+ "src/mock-git.ts",
+ "src/spawn.ts",
+ ],
+ output: {
+ dir: "dist",
+ format: "cjs",
+ // Jest 28+ provides a .default export, while Jest < 28 directly exports the expect() function
+ // as its top-level module.exports value. Using "compat" here lets us
+ // `import { default as expect } from "expect"` with both versions.
+ interop: (id) => (id === "expect" ? "compat" : "default"),
+ },
+ external: isExternal,
+ // 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,
+ },
+ },
+ // Rollup types so that UnflakableConfig from @unflakable/plugins-common is bundled. Otherwise,
+ // the integration-input* packages would need to depend on @unflakable/plugins-common, which we
+ // don't want since we need to test that @unflakable/cypress-plugin bundles everything it needs
+ // to.
+ {
+ input: "dist/config.d.ts",
+ output: {
+ file: "dist/config.d.ts",
+ format: "cjs",
+ },
+ external: isExternal,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ plugins: [
+ pluginNodeResolve({ preferBuiltins: true }),
+ pluginDts({
+ respectExternal: true,
+ }),
+ ],
+ },
+];
diff --git a/packages/test-common/src/config.ts b/packages/test-common/src/config.ts
new file mode 100644
index 0000000..c9492da
--- /dev/null
+++ b/packages/test-common/src/config.ts
@@ -0,0 +1,108 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import _debug from "debug";
+import {
+ UnflakableConfig,
+ setCosmiconfig,
+ setCosmiconfigSync,
+} from "@unflakable/plugins-common";
+import {
+ cosmiconfig,
+ cosmiconfigSync,
+ Options,
+ OptionsSync,
+} from "cosmiconfig";
+import { default as expect } from "expect";
+
+const debug = _debug("unflakable:test-common:config");
+
+const throwUnimplemented = (): never => {
+ throw new Error("unimplemented");
+};
+
+export type CosmiconfigMockParams = {
+ expectedSearchFrom: string;
+} & (
+ | { pathToLoad: string; searchResult?: undefined }
+ | {
+ pathToLoad?: undefined;
+ 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.expectedSearchFrom);
+ if (params.pathToLoad !== undefined) {
+ return cosmiconfig(moduleName, options).load(params.pathToLoad);
+ } else {
+ return Promise.resolve(params.searchResult);
+ }
+ },
+ };
+ }
+ );
+
+ setCosmiconfigSync(
+ (
+ moduleName: string,
+ options?: OptionsSync
+ ): 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.expectedSearchFrom);
+ if (params.pathToLoad !== undefined) {
+ return cosmiconfigSync(moduleName, options).load(params.pathToLoad);
+ } else {
+ return params.searchResult;
+ }
+ },
+ };
+ }
+ );
+};
diff --git a/packages/cypress-plugin/test/integration-common/src/git.ts b/packages/test-common/src/git.ts
similarity index 96%
rename from packages/cypress-plugin/test/integration-common/src/git.ts
rename to packages/test-common/src/git.ts
index a35a569..4ae081d 100644
--- a/packages/cypress-plugin/test/integration-common/src/git.ts
+++ b/packages/test-common/src/git.ts
@@ -1,11 +1,11 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 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");
+const debug = _debug("unflakable:test-common:git");
export type SimpleGitMockRef = {
sha: string;
diff --git a/packages/test-common/src/mock-backend.ts b/packages/test-common/src/mock-backend.ts
new file mode 100644
index 0000000..70dcb0a
--- /dev/null
+++ b/packages/test-common/src/mock-backend.ts
@@ -0,0 +1,256 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import {
+ CompletedRequest,
+ getLocal as getLocalHttpServer,
+ MockedEndpoint,
+ Mockttp,
+} from "mockttp";
+import type {
+ CallbackResponseMessageResult,
+ CallbackResponseResult,
+} from "mockttp/dist/rules/requests/request-handler-definitions";
+import { gunzipSync } from "zlib";
+import _debug from "debug";
+import {
+ CreateTestSuiteRunFromUploadRequest,
+ CreateTestSuiteRunInlineRequest,
+ TestSuiteManifest,
+ TestSuiteRunPendingSummary,
+} from "@unflakable/js-api";
+
+const debug = _debug("unflakable:test-common:mock-backend");
+
+export type UnmatchedEndpoints = {
+ unmatchedApiRequestEndpoint: MockedEndpoint;
+ unmatchedObjectStoreRequestEndpoint: MockedEndpoint;
+};
+
+export class MockBackend {
+ private readonly apiServer: Mockttp;
+ private readonly objectStoreServer: Mockttp;
+
+ constructor() {
+ this.apiServer = getLocalHttpServer({
+ // debug: true,
+ suggestChanges: false,
+ });
+ this.objectStoreServer = getLocalHttpServer({
+ // debug: true,
+ suggestChanges: false,
+ });
+ }
+
+ public get apiServerPort(): number {
+ return this.apiServer.port;
+ }
+
+ public addExpectations = async (
+ onError: (e: unknown) => void,
+ manifest: TestSuiteManifest | null,
+ verifyUploadResults: (request: CompletedRequest) => void,
+ runSummary: TestSuiteRunPendingSummary | null,
+ userAgentRegex: RegExp,
+ {
+ expectPluginToBeEnabled,
+ expectResultsToBeUploaded,
+ expectedApiKey,
+ expectedSuiteId,
+ }: {
+ expectPluginToBeEnabled: boolean;
+ expectResultsToBeUploaded: boolean;
+ expectedApiKey: string;
+ expectedSuiteId: string;
+ }
+ ): Promise => {
+ const onUnmatchedRequest = (
+ request: CompletedRequest
+ ): CallbackResponseResult => {
+ onError(
+ new Error(`Unexpected request ${request.method} ${request.path}`)
+ );
+ return { statusCode: 500 };
+ };
+
+ const unmatchedApiRequestEndpoint = await this.apiServer
+ .forUnmatchedRequest()
+ .thenCallback(onUnmatchedRequest);
+ const unmatchedObjectStoreRequestEndpoint = await this.objectStoreServer
+ .forUnmatchedRequest()
+ .thenCallback(onUnmatchedRequest);
+
+ if (!expectPluginToBeEnabled) {
+ return {
+ unmatchedApiRequestEndpoint,
+ unmatchedObjectStoreRequestEndpoint,
+ };
+ }
+
+ await this.apiServer
+ .forGet(`/api/v1/test-suites/${expectedSuiteId}/manifest`)
+ .times(manifest === null ? 3 : 1)
+ .withHeaders({
+ Authorization: `Bearer ${expectedApiKey}`,
+ })
+ .thenCallback((request): CallbackResponseResult => {
+ try {
+ expect(request.headers["user-agent"]).toMatch(userAgentRegex);
+
+ if (manifest === null) {
+ return "reset";
+ }
+
+ return {
+ statusCode: 200,
+ json: manifest,
+ };
+ } 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 this.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:${this.objectStoreServer.port}${uploadPath}${uploadQuery}`,
+ },
+ json: {
+ upload_id: "MOCK_UPLOAD_ID",
+ },
+ };
+ } catch (e) {
+ onError(e);
+ return {
+ statusCode: 500,
+ };
+ }
+ });
+
+ let runRequest: CreateTestSuiteRunInlineRequest | null = null;
+ await this.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(request);
+
+ return {
+ statusCode: 200,
+ };
+ } catch (e) {
+ onError(e);
+ return { statusCode: 500 };
+ }
+ });
+
+ await this.apiServer
+ .forPost(`/api/v1/test-suites/${expectedSuiteId}/runs`)
+ .times(runSummary === null ? 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 (runSummary === null) {
+ return "reset";
+ }
+
+ return {
+ json: runSummary,
+ statusCode: 201,
+ };
+ } catch (e) {
+ onError(e);
+ return {
+ statusCode: 500,
+ };
+ }
+ });
+ }
+
+ return {
+ unmatchedApiRequestEndpoint,
+ unmatchedObjectStoreRequestEndpoint,
+ };
+ };
+
+ public checkExpectations = async ({
+ unmatchedApiRequestEndpoint,
+ unmatchedObjectStoreRequestEndpoint,
+ }: UnmatchedEndpoints): Promise => {
+ expect(await this.apiServer.getPendingEndpoints()).toStrictEqual([
+ unmatchedApiRequestEndpoint,
+ ]);
+ expect(await this.objectStoreServer.getPendingEndpoints()).toStrictEqual([
+ unmatchedObjectStoreRequestEndpoint,
+ ]);
+ };
+
+ public start = async (): Promise => {
+ await this.apiServer.start();
+ debug(
+ `Listening for mock API requests on http://localhost:${this.apiServer.port}`
+ );
+
+ await this.objectStoreServer.start();
+ debug(
+ `Listening for mock S3 requests on http://localhost:${this.objectStoreServer.port}`
+ );
+ };
+
+ public stop = async (): Promise => {
+ debug(`Stopping mock API server`);
+ await this.apiServer.stop();
+
+ debug(`Stopping mock S3 server`);
+ return this.objectStoreServer.stop();
+ };
+}
diff --git a/packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts b/packages/test-common/src/mock-cosmiconfig.ts
similarity index 57%
rename from packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts
rename to packages/test-common/src/mock-cosmiconfig.ts
index 09da367..90049d3 100644
--- a/packages/cypress-plugin/test/integration-common/src/mock-cosmiconfig.ts
+++ b/packages/test-common/src/mock-cosmiconfig.ts
@@ -1,7 +1,6 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
-// Script loaded by Node.JS via --require that mocks cosmiconfig within the cypress-unflakable bin
-// script for testing.
+// Script loaded by Node.JS via --require that mocks cosmiconfig for testing.
import { registerCosmiconfigMock } from "./config";
diff --git a/packages/test-common/src/mock-git.ts b/packages/test-common/src/mock-git.ts
new file mode 100644
index 0000000..477ac4e
--- /dev/null
+++ b/packages/test-common/src/mock-git.ts
@@ -0,0 +1,7 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+// Script loaded by Node.JS via --require that mocks simple-git for testing.
+
+import { registerSimpleGitMock } from "./git";
+
+registerSimpleGitMock();
diff --git a/packages/test-common/src/spawn.ts b/packages/test-common/src/spawn.ts
new file mode 100644
index 0000000..9143bcf
--- /dev/null
+++ b/packages/test-common/src/spawn.ts
@@ -0,0 +1,155 @@
+// Copyright (c) 2023-2024 Developer Innovations, LLC
+
+import { TextDecoder } from "util";
+import treeKill from "tree-kill";
+import _debug from "debug";
+import { spawn } from "child_process";
+
+const debug = _debug("unflakable:test-common:spawn");
+
+// Async callbacks (e.g., mock API routes) can set this when an occurs during the test.
+export type AsyncTestError = { error: unknown | undefined };
+
+export const spawnTestWithTimeout = async (
+ nodeArgs: string[],
+ env: { [key in string]: string | undefined },
+ cwd: string,
+ timeout_ms: number,
+ verifyOutput: (stdoutLines: string[], stderrLines: string[]) => Promise,
+ expectedExitCode: number,
+ escapeStderrDebugOutput: boolean,
+ asyncTestError: AsyncTestError
+): Promise => {
+ debug(
+ `Spawning test:\n args = %o\n environment = %o\n cwd = %s`,
+ nodeArgs,
+ env,
+ cwd
+ );
+
+ const child = spawn("node", nodeArgs, {
+ cwd,
+ env,
+ });
+
+ const onOutput = (
+ name: string,
+ onLine: (line: string, now: Date) => 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.
+ child.on("exit", () => {
+ if (pending.s !== "") {
+ onLine(pending.s, new Date());
+ debugExt(escapeDebugOutput ? JSON.stringify(pending.s) : pending.s);
+ }
+ });
+
+ return (data: Buffer): void => {
+ const now = new Date();
+ // 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, now);
+ debugExt(
+ escapeDebugOutput ? JSON.stringify(lineWithPending) : lineWithPending
+ );
+ });
+
+ pending.s = lines[lines.length - 1];
+ };
+ };
+
+ const stdoutLines = [] as string[];
+ const stderrLines = [] as string[];
+ const combinedLines = [] as string[];
+
+ child.stderr.on(
+ "data",
+ onOutput(
+ "stderr",
+ (line, now) => {
+ stderrLines.push(line);
+ combinedLines.push(`${now.toISOString()} ${line}`);
+ },
+ // 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.
+ escapeStderrDebugOutput
+ )
+ );
+ child.stdout.on(
+ "data",
+ onOutput(
+ "stdout",
+ (line, now) => {
+ stdoutLines.push(line);
+ combinedLines.push(`${now.toISOString()} ${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 ${timeout_ms}ms; killing child process tree`
+ );
+ const timeoutError = new Error(
+ `Test timed out after ${timeout_ms}ms`
+ );
+ if (asyncTestError.error === undefined) {
+ asyncTestError.error = timeoutError;
+ }
+ treeKill(child.pid, "SIGKILL", () => reject(timeoutError));
+ }, timeout_ms);
+
+ child.on("error", (err) => {
+ clearTimeout(watchdog);
+ reject(err);
+ });
+ child.on("exit", (code, signal) => {
+ clearTimeout(watchdog);
+ resolve({ code, signal });
+ });
+ }
+ );
+
+ if (asyncTestError.error !== undefined) {
+ throw asyncTestError.error;
+ }
+
+ expect(signal).toBe(null);
+ expect(code).toBe(expectedExitCode);
+
+ await verifyOutput(stdoutLines, stderrLines);
+ } 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-common/src/tsconfig.json b/packages/test-common/src/tsconfig.json
similarity index 79%
rename from packages/cypress-plugin/test/integration-common/src/tsconfig.json
rename to packages/test-common/src/tsconfig.json
index 3aa8530..b51fb3a 100644
--- a/packages/cypress-plugin/test/integration-common/src/tsconfig.json
+++ b/packages/test-common/src/tsconfig.json
@@ -4,7 +4,8 @@
"declaration": true,
"declarationDir": "../dist",
// Required by Rollup (consumed by Rollup in the *-plugin packages).
- "module": "esnext"
+ "module": "esnext",
+ "types": ["jest", "node"]
},
"include": ["."]
}
diff --git a/packages/test-common/tsconfig.json b/packages/test-common/tsconfig.json
new file mode 100644
index 0000000..00ed11b
--- /dev/null
+++ b/packages/test-common/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ // Remove DOM types and support 2-argument Error constructor that takes a cause.
+ "lib": ["ES2022"]
+ },
+ "include": [".eslintrc.js", "rollup.config.mjs"]
+}
diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js
index 653facc..34454d6 100644
--- a/scripts/.eslintrc.js
+++ b/scripts/.eslintrc.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Developer Innovations, LLC
+// Copyright (c) 2023-2024 Developer Innovations, LLC
module.exports = {
extends: ["../.eslintrc-ts.js"],
diff --git a/scripts/set-jest-version.ts b/scripts/set-jest-version.ts
index 5b3e58a..66037ca 100644
--- a/scripts/set-jest-version.ts
+++ b/scripts/set-jest-version.ts
@@ -1,6 +1,6 @@
-// Copyright (c) 2022-2023 Developer Innovations, LLC
+// Copyright (c) 2022-2024 Developer Innovations, LLC
-import { spawnSync } from "child_process";
+import { execSync, spawnSync } from "child_process";
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as semver from "semver";
@@ -20,16 +20,28 @@ type PackageSpec = {
};
type Lockfile = { [key in string]: PackageSpec };
-const setYarnResolution = (descriptor: string, resolution: string): void => {
+const getYarnPath = (): string =>
+ execSync("yarn config get yarnPath").toString().trimEnd();
+
+const setYarnResolution = (
+ yarnPath: string,
+ descriptor: string,
+ resolution: string
+): void => {
debug(`Setting yarn resolution \`${descriptor}\` to ${resolution}`);
const outcome = spawnSync(
- "yarn",
- ["set", "resolution", descriptor, resolution],
+ "node",
+ // For some reason, yarn.cmd doesn't update the lockfile on Windows, but running node explicitly
+ // with the path to yarn does.
+ [yarnPath, "set", "resolution", descriptor, resolution],
{ stdio: "inherit" }
);
- if (outcome.status !== 0) {
+ if (outcome.error !== undefined) {
+ console.error("ERROR: Failed to run yarn: %o", outcome.error);
+ process.exit(1);
+ } else if (outcome.status !== 0) {
console.error("ERROR: Exiting due to yarn error");
process.exit(1);
}
@@ -65,6 +77,7 @@ const PACKAGE_VERSION_MAP = {
"25 - 27": null,
"29.0 - 29.3": "~29.0",
"29.4 - 29.5": "~29.4",
+ "29.6 - 29.7": "~29.6",
},
"@jest/source-map": {
"25.2 - 25.4": "~25.2",
@@ -74,6 +87,7 @@ const PACKAGE_VERSION_MAP = {
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
"29.4 - 29.5": "~29.4",
+ "29.6 - 29.7": "~29.6",
},
"@jest/test-result": {
"26.3 - 26.4": "~26.3",
@@ -84,6 +98,7 @@ const PACKAGE_VERSION_MAP = {
"@jest/types": {
"26.3 - 26.4": "~26.3",
"27.2 - 27.3": "~27.2",
+ "29.6 - 29.7": "~29.6",
},
"babel-jest": {
"26.3 - 26.4": "~26.3",
@@ -95,6 +110,7 @@ const PACKAGE_VERSION_MAP = {
"27.2 - 27.3": "~27.2",
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
+ "29.6 - 29.7": "~29.6",
},
"babel-preset-jest": {
"26.3 - 26.4": "~26.3",
@@ -102,11 +118,13 @@ const PACKAGE_VERSION_MAP = {
"27.2 - 27.3": "~27.2",
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
+ "29.6 - 29.7": "~29.6",
},
"jest-changed-files": {
"26.3 - 26.4": "~26.3",
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
+ "29.5 - 29.6": "~29.5",
},
"jest-docblock": {
"^25.3": "~25.3",
@@ -114,7 +132,7 @@ const PACKAGE_VERSION_MAP = {
"27.0 - 27.3": "~27.0",
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
- "29.4 - 29.5": "~29.4",
+ "29.4 - 29.6": "~29.4",
},
"jest-environment-jsdom": {
"26.3 - 26.4": "~26.3",
@@ -131,6 +149,7 @@ const PACKAGE_VERSION_MAP = {
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
"29.4 - 29.5": "~29.4",
+ "29.6 - 29.7": "~29.6",
},
"jest-haste-map": {
"26.3 - 26.4": "~26.3",
@@ -149,6 +168,7 @@ const PACKAGE_VERSION_MAP = {
"29.0 - 29.1": "~29.0",
"29.2 - 29.3": "~29.2",
"29.4 - 29.5": "~29.4",
+ "29.6 - 29.7": "~29.6",
},
"jest-serializer": {
"25.2 - 25.4": "~25.2",
@@ -219,6 +239,8 @@ const main = (): never => {
);
}
+ const yarnPath = getYarnPath();
+
const maxIterations = 10;
// The jest TS types aren't released very frequently and mostly correspond to major versions, so
@@ -228,6 +250,7 @@ const main = (): never => {
targetSemVerMinVersion.major <= 29
) {
setYarnResolution(
+ yarnPath,
"@types/jest@npm:25.1.0 - 29",
targetSemVerMinVersion.major.toString()
);
@@ -294,7 +317,7 @@ const main = (): never => {
done = false;
- setYarnResolution(descriptor, packageTargetSemVerRange.raw);
+ setYarnResolution(yarnPath, descriptor, packageTargetSemVerRange.raw);
});
}
});
diff --git a/yarn.lock b/yarn.lock
index 3a9fdd2..5eb8cd1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15,535 +15,362 @@ __metadata:
languageName: node
linkType: hard
-"@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"
+"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/code-frame@npm:7.22.5"
dependencies:
- "@babel/highlight": ^7.18.6
- checksum: e5390e6ec1ac58dcef01d4f18eaf1fd2f1325528661ff6d4a5de8979588b9f5a8e852a54a91b923846f7a5c681b217f0a45c2524eb9560553160cd963b7d592c
+ "@babel/highlight": ^7.22.5
+ checksum: cfe804f518f53faaf9a1d3e0f9f74127ab9a004912c3a16fda07fb6a633393ecb9918a053cb71804204c1b7ec3d49e1699604715e2cfb0c9f7bc4933d324ebb6
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.0":
- version: 7.22.3
- resolution: "@babel/compat-data@npm:7.22.3"
- checksum: eb001646f41459f42ccb0d39ee8bb3c3c495bc297234817044c0002689c625e3159a6678c53fd31bd98cf21f31472b73506f350fc6906e3bdfa49cb706e2af8d
+"@babel/compat-data@npm:^7.22.5, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/compat-data@npm:7.22.9"
+ checksum: bed77d9044ce948b4327b30dd0de0779fa9f3a7ed1f2d31638714ed00229fa71fc4d1617ae0eb1fad419338d3658d0e9a5a083297451e09e73e078d0347ff808
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.1
- resolution: "@babel/core@npm:7.22.1"
+"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/core@npm:7.22.9"
dependencies:
"@ampproject/remapping": ^2.2.0
- "@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
+ "@babel/code-frame": ^7.22.5
+ "@babel/generator": ^7.22.9
+ "@babel/helper-compilation-targets": ^7.22.9
+ "@babel/helper-module-transforms": ^7.22.9
+ "@babel/helpers": ^7.22.6
+ "@babel/parser": ^7.22.7
+ "@babel/template": ^7.22.5
+ "@babel/traverse": ^7.22.8
+ "@babel/types": ^7.22.5
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: bbe45e791f223a7e692d2ea6597a73f48050abd24b119c85c48ac6504c30ce63343a2ea3f79b5847bf4b409ddd8a68b6cdc4f0272ded1d2ef6f6b1e9663432f0
+ semver: ^6.3.1
+ checksum: 7bf069aeceb417902c4efdaefab1f7b94adb7dea694a9aed1bda2edf4135348a080820529b1a300c6f8605740a00ca00c19b2d5e74b5dd489d99d8c11d5e56d1
languageName: node
linkType: hard
-"@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"
+"@babel/generator@npm:^7.22.7, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.7.2":
+ version: 7.22.9
+ resolution: "@babel/generator@npm:7.22.9"
dependencies:
- "@babel/types": ^7.22.3
+ "@babel/types": ^7.22.5
"@jridgewell/gen-mapping": ^0.3.2
"@jridgewell/trace-mapping": ^0.3.17
jsesc: ^2.5.1
- checksum: ccb6426ca5b5a38f0d47a3ac9628e223d2aaaa489cbf90ffab41468795c22afe86855f68a58667f0f2673949f1810d4d5a57b826c17984eab3e28fdb34a909e6
+ checksum: 7c9d2c58b8d5ac5e047421a6ab03ec2ff5d9a5ff2c2212130a0055e063ac349e0b19d435537d6886c999771aef394832e4f54cd9fc810100a7f23d982f6af06b
languageName: node
linkType: hard
-"@babel/helper-annotate-as-pure@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/helper-annotate-as-pure@npm:7.18.6"
+"@babel/helper-annotate-as-pure@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-annotate-as-pure@npm:7.22.5"
dependencies:
- "@babel/types": ^7.18.6
- checksum: 88ccd15ced475ef2243fdd3b2916a29ea54c5db3cd0cfabf9d1d29ff6e63b7f7cd1c27264137d7a40ac2e978b9b9a542c332e78f40eb72abe737a7400788fc1b
+ "@babel/types": ^7.22.5
+ checksum: 53da330f1835c46f26b7bf4da31f7a496dee9fd8696cca12366b94ba19d97421ce519a74a837f687749318f94d1a37f8d1abcbf35e8ed22c32d16373b2f6198d
languageName: node
linkType: hard
-"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.18.6":
- version: 7.21.5
- resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.21.5"
+"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.5"
dependencies:
- "@babel/types": ^7.21.5
- checksum: 9a033d3d7a6409256272ea6fc03731511af9f936ee0b161ace05d171d7bd5adf455dc85f80437d92277462f6bd2af9af1f2d1967edc21ca4d5966ac0a09cf61d
+ "@babel/types": ^7.22.5
+ checksum: d753acac62399fc6dd354cf1b9441bde0c331c2fe792a4c14904c5e5eafc3cac79478f6aa038e8a51c1148b0af6710a2e619855e4b5d54497ac972eaffed5884
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.1":
- version: 7.22.1
- resolution: "@babel/helper-compilation-targets@npm:7.22.1"
+"@babel/helper-compilation-targets@npm:^7.22.5, @babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/helper-compilation-targets@npm:7.22.9"
dependencies:
- "@babel/compat-data": ^7.22.0
- "@babel/helper-validator-option": ^7.21.0
- browserslist: ^4.21.3
+ "@babel/compat-data": ^7.22.9
+ "@babel/helper-validator-option": ^7.22.5
+ browserslist: ^4.21.9
lru-cache: ^5.1.1
- semver: ^6.3.0
+ semver: ^6.3.1
peerDependencies:
"@babel/core": ^7.0.0
- checksum: a686a01bd3288cf95ca26faa27958d34c04e2501c4b0858c3a6558776dec20317b5635f33d64c5a635b6fbdfe462a85c30d4bfa0ae7e7ffe3467e4d06442d7c8
+ checksum: ea0006c6a93759025f4a35a25228ae260538c9f15023e8aac2a6d45ca68aef4cf86cfc429b19af9a402cbdd54d5de74ad3fbcf6baa7e48184dc079f1a791e178
languageName: node
linkType: hard
-"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0":
- version: 7.21.5
- resolution: "@babel/helper-create-class-features-plugin@npm:7.21.5"
+"@babel/helper-create-class-features-plugin@npm:^7.22.5, @babel/helper-create-class-features-plugin@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/helper-create-class-features-plugin@npm:7.22.9"
dependencies:
- "@babel/helper-annotate-as-pure": ^7.18.6
- "@babel/helper-environment-visitor": ^7.21.5
- "@babel/helper-function-name": ^7.21.0
- "@babel/helper-member-expression-to-functions": ^7.21.5
- "@babel/helper-optimise-call-expression": ^7.18.6
- "@babel/helper-replace-supers": ^7.21.5
- "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0
- "@babel/helper-split-export-declaration": ^7.18.6
- semver: ^6.3.0
+ "@babel/helper-annotate-as-pure": ^7.22.5
+ "@babel/helper-environment-visitor": ^7.22.5
+ "@babel/helper-function-name": ^7.22.5
+ "@babel/helper-member-expression-to-functions": ^7.22.5
+ "@babel/helper-optimise-call-expression": ^7.22.5
+ "@babel/helper-replace-supers": ^7.22.9
+ "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5
+ "@babel/helper-split-export-declaration": ^7.22.6
+ semver: ^6.3.1
peerDependencies:
"@babel/core": ^7.0.0
- checksum: cf1bcdd5cf2949927ba63002381cc7db22d1c8ef12b85aacc5c6361ae538522f947e57c59a787f5ee44c5413cf881a3d76224f5583d2c0575282c7c1f68df797
+ checksum: 6c2436d1a5a3f1ff24628d78fa8c6d3120c40285aa3eda7815b1adbf8c5951e0dd73d368cf845825888fa3dc2f207dadce53309825598d7c67953e5ed9dd51d2
languageName: node
linkType: hard
-"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.20.5":
- version: 7.21.5
- resolution: "@babel/helper-create-regexp-features-plugin@npm:7.21.5"
+"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.5":
+ version: 7.22.9
+ resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.9"
dependencies:
- "@babel/helper-annotate-as-pure": ^7.18.6
+ "@babel/helper-annotate-as-pure": ^7.22.5
regexpu-core: ^5.3.1
- semver: ^6.3.0
+ semver: ^6.3.1
peerDependencies:
"@babel/core": ^7.0.0
- checksum: c38cb01b242b0b2bb9783072e6ba4d4aa08c66ea39f9b74a45f31f95a6fe2ff3ba782d8ce09827c09939450d2d39a6db41c83051ef191482bfb67b63a5023e24
+ checksum: 87cb48a7ee898ab205374274364c3adc70b87b08c7bd07f51019ae4562c0170d7148e654d591f825dee14b5fe11666a0e7966872dfdbfa0d1b94b861ecf0e4e1
languageName: node
linkType: hard
-"@babel/helper-define-polyfill-provider@npm:^0.3.3":
- version: 0.3.3
- resolution: "@babel/helper-define-polyfill-provider@npm:0.3.3"
+"@babel/helper-define-polyfill-provider@npm:^0.4.2":
+ version: 0.4.2
+ resolution: "@babel/helper-define-polyfill-provider@npm:0.4.2"
dependencies:
- "@babel/helper-compilation-targets": ^7.17.7
- "@babel/helper-plugin-utils": ^7.16.7
+ "@babel/helper-compilation-targets": ^7.22.6
+ "@babel/helper-plugin-utils": ^7.22.5
debug: ^4.1.1
lodash.debounce: ^4.0.8
resolve: ^1.14.2
- semver: ^6.1.2
peerDependencies:
- "@babel/core": ^7.4.0-0
- checksum: 8e3fe75513302e34f6d92bd67b53890e8545e6c5bca8fe757b9979f09d68d7e259f6daea90dc9e01e332c4f8781bda31c5fe551c82a277f9bc0bec007aed497c
+ "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
+ checksum: 1f6dec0c5d0876d278fe15b71238eccc5f74c4e2efa2c78aaafa8bc2cc96336b8e68d94cd1a78497356c96e8b91b8c1f4452179820624d1702aee2f9832e6569
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.1":
- version: 7.22.1
- resolution: "@babel/helper-environment-visitor@npm:7.22.1"
- checksum: a6b4bb5505453bff95518d361ac1de393f0029aeb8b690c70540f4317934c53c43cc4afcda8c752ffa8c272e63ed6b929a56eca28e4978424177b24238b21bf9
+"@babel/helper-environment-visitor@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-environment-visitor@npm:7.22.5"
+ checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1
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":
- version: 7.21.0
- resolution: "@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"
dependencies:
- "@babel/template": ^7.20.7
- "@babel/types": ^7.21.0
- checksum: d63e63c3e0e3e8b3138fa47b0cd321148a300ef12b8ee951196994dcd2a492cc708aeda94c2c53759a5c9177fffaac0fd8778791286746f72a000976968daf4e
+ "@babel/template": ^7.22.5
+ "@babel/types": ^7.22.5
+ checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a
languageName: node
linkType: hard
-"@babel/helper-hoist-variables@npm:^7.18.6":
- version: 7.18.6
- resolution: "@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"
dependencies:
- "@babel/types": ^7.18.6
- checksum: fd9c35bb435fda802bf9ff7b6f2df06308a21277c6dec2120a35b09f9de68f68a33972e2c15505c1a1a04b36ec64c9ace97d4a9e26d6097b76b4396b7c5fa20f
+ "@babel/types": ^7.22.5
+ checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc
languageName: node
linkType: hard
-"@babel/helper-member-expression-to-functions@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/helper-member-expression-to-functions@npm:7.21.5"
+"@babel/helper-member-expression-to-functions@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-member-expression-to-functions@npm:7.22.5"
dependencies:
- "@babel/types": ^7.21.5
- checksum: c404b4a0271c640b7dc8c34af7b683c70a43200259e02330cfc02e79e6b271e9227f35554cd6ad015eabcfa1fea75b9d0b87b69f3d1e6c2af6edd224060b1732
+ "@babel/types": ^7.22.5
+ checksum: 4bd5791529c280c00743e8bdc669ef0d4cd1620d6e3d35e0d42b862f8262bc2364973e5968007f960780344c539a4b9cf92ab41f5b4f94560a9620f536de2a39
languageName: node
linkType: hard
-"@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"
+"@babel/helper-module-imports@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-module-imports@npm:7.22.5"
dependencies:
- "@babel/types": ^7.21.4
- checksum: bd330a2edaafeb281fbcd9357652f8d2666502567c0aad71db926e8499c773c9ea9c10dfaae30122452940326d90c8caff5c649ed8e1bf15b23f858758d3abc6
+ "@babel/types": ^7.22.5
+ checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad
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.1":
- version: 7.22.1
- resolution: "@babel/helper-module-transforms@npm:7.22.1"
+"@babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/helper-module-transforms@npm:7.22.9"
dependencies:
- "@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
+ "@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.6
+ "@babel/helper-validator-identifier": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 2751f77660518cf4ff027514d6f4794f04598c6393be7b04b8e46c6e21606e11c19f3f57ab6129a9c21bacdf8b3ffe3af87bb401d972f34af2d0ffde02ac3001
languageName: node
linkType: hard
-"@babel/helper-optimise-call-expression@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/helper-optimise-call-expression@npm:7.18.6"
+"@babel/helper-optimise-call-expression@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-optimise-call-expression@npm:7.22.5"
dependencies:
- "@babel/types": ^7.18.6
- checksum: e518fe8418571405e21644cfb39cf694f30b6c47b10b006609a92469ae8b8775cbff56f0b19732343e2ea910641091c5a2dc73b56ceba04e116a33b0f8bd2fbd
+ "@babel/types": ^7.22.5
+ checksum: c70ef6cc6b6ed32eeeec4482127e8be5451d0e5282d5495d5d569d39eb04d7f1d66ec99b327f45d1d5842a9ad8c22d48567e93fc502003a47de78d122e355f7c
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.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
+"@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.18.6, @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
languageName: node
linkType: hard
-"@babel/helper-remap-async-to-generator@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9"
+"@babel/helper-remap-async-to-generator@npm:^7.22.5":
+ version: 7.22.9
+ resolution: "@babel/helper-remap-async-to-generator@npm:7.22.9"
dependencies:
- "@babel/helper-annotate-as-pure": ^7.18.6
- "@babel/helper-environment-visitor": ^7.18.9
- "@babel/helper-wrap-function": ^7.18.9
- "@babel/types": ^7.18.9
+ "@babel/helper-annotate-as-pure": ^7.22.5
+ "@babel/helper-environment-visitor": ^7.22.5
+ "@babel/helper-wrap-function": ^7.22.9
peerDependencies:
"@babel/core": ^7.0.0
- checksum: 4be6076192308671b046245899b703ba090dbe7ad03e0bea897bb2944ae5b88e5e85853c9d1f83f643474b54c578d8ac0800b80341a86e8538264a725fbbefec
+ checksum: 05538079447829b13512157491cc77f9cf1ea7e1680e15cff0682c3ed9ee162de0c4862ece20a6d6b2df28177a1520bcfe45993fbeccf2747a81795a7c3f6290
languageName: node
linkType: hard
-"@babel/helper-replace-supers@npm:^7.18.6, @babel/helper-replace-supers@npm:^7.20.7, @babel/helper-replace-supers@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/helper-replace-supers@npm:7.21.5"
+"@babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/helper-replace-supers@npm:7.22.9"
dependencies:
- "@babel/helper-environment-visitor": ^7.21.5
- "@babel/helper-member-expression-to-functions": ^7.21.5
- "@babel/helper-optimise-call-expression": ^7.18.6
- "@babel/template": ^7.20.7
- "@babel/traverse": ^7.21.5
- "@babel/types": ^7.21.5
- checksum: 4fd343e6f90533743d8e8a1f42e50377b3d6b27f524a27eb97ff28f075e4e55cca2383adb1b0973de358b08022aef0fec4c8d69711e1da43bf9b887b5a893677
+ "@babel/helper-environment-visitor": ^7.22.5
+ "@babel/helper-member-expression-to-functions": ^7.22.5
+ "@babel/helper-optimise-call-expression": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: d41471f56ff2616459d35a5df1900d5f0756ae78b1027040365325ef332d66e08e3be02a9489756d870887585ff222403a228546e93dd7019e19e59c0c0fe586
languageName: node
linkType: hard
-"@babel/helper-simple-access@npm:^7.21.5":
- version: 7.21.5
- resolution: "@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"
dependencies:
- "@babel/types": ^7.21.5
- checksum: ad212beaa24be3864c8c95bee02f840222457ccf5419991e2d3e3e39b0f75b77e7e857e0bf4ed428b1cd97acefc87f3831bdb0b9696d5ad0557421f398334fc3
+ "@babel/types": ^7.22.5
+ checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2
languageName: node
linkType: hard
-"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0":
- version: 7.20.0
- resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0"
+"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5"
dependencies:
- "@babel/types": ^7.20.0
- checksum: 34da8c832d1c8a546e45d5c1d59755459ffe43629436707079989599b91e8c19e50e73af7a4bd09c95402d389266731b0d9c5f69e372d8ebd3a709c05c80d7dd
+ "@babel/types": ^7.22.5
+ checksum: 1012ef2295eb12dc073f2b9edf3425661e9b8432a3387e62a8bc27c42963f1f216ab3124228015c748770b2257b4f1fda882ca8fa34c0bf485e929ae5bc45244
languageName: node
linkType: hard
-"@babel/helper-split-export-declaration@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/helper-split-export-declaration@npm:7.18.6"
+"@babel/helper-split-export-declaration@npm:^7.22.6":
+ version: 7.22.6
+ resolution: "@babel/helper-split-export-declaration@npm:7.22.6"
dependencies:
- "@babel/types": ^7.18.6
- checksum: c6d3dede53878f6be1d869e03e9ffbbb36f4897c7cc1527dc96c56d127d834ffe4520a6f7e467f5b6f3c2843ea0e81a7819d66ae02f707f6ac057f3d57943a2b
+ "@babel/types": ^7.22.5
+ checksum: e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921
languageName: node
linkType: hard
-"@babel/helper-string-parser@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/helper-string-parser@npm:7.21.5"
- checksum: 36c0ded452f3858e67634b81960d4bde1d1cd2a56b82f4ba2926e97864816021c885f111a7cf81de88a0ed025f49d84a393256700e9acbca2d99462d648705d8
+"@babel/helper-string-parser@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-string-parser@npm:7.22.5"
+ checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467
languageName: node
linkType: hard
-"@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
+"@babel/helper-validator-identifier@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-validator-identifier@npm:7.22.5"
+ checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea
languageName: node
linkType: hard
-"@babel/helper-validator-option@npm:^7.21.0":
- version: 7.21.0
- resolution: "@babel/helper-validator-option@npm:7.21.0"
- checksum: 8ece4c78ffa5461fd8ab6b6e57cc51afad59df08192ed5d84b475af4a7193fc1cb794b59e3e7be64f3cdc4df7ac78bf3dbb20c129d7757ae078e6279ff8c2f07
+"@babel/helper-validator-option@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/helper-validator-option@npm:7.22.5"
+ checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3
languageName: node
linkType: hard
-"@babel/helper-wrap-function@npm:^7.18.9":
- version: 7.20.5
- resolution: "@babel/helper-wrap-function@npm:7.20.5"
+"@babel/helper-wrap-function@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/helper-wrap-function@npm:7.22.9"
dependencies:
- "@babel/helper-function-name": ^7.19.0
- "@babel/template": ^7.18.10
- "@babel/traverse": ^7.20.5
- "@babel/types": ^7.20.5
- checksum: 11a6fc28334368a193a9cb3ad16f29cd7603bab958433efc82ebe59fa6556c227faa24f07ce43983f7a85df826f71d441638442c4315e90a554fe0a70ca5005b
+ "@babel/helper-function-name": ^7.22.5
+ "@babel/template": ^7.22.5
+ "@babel/types": ^7.22.5
+ checksum: 037317dc06dac6593e388738ae1d3e43193bc1d31698f067c0ef3d4dc6f074dbed860ed42aa137b48a67aa7cb87336826c4bdc13189260481bcf67eb7256c789
languageName: node
linkType: hard
-"@babel/helpers@npm:^7.22.0":
- version: 7.22.3
- resolution: "@babel/helpers@npm:7.22.3"
+"@babel/helpers@npm:^7.22.6":
+ version: 7.22.6
+ resolution: "@babel/helpers@npm:7.22.6"
dependencies:
- "@babel/template": ^7.21.9
- "@babel/traverse": ^7.22.1
- "@babel/types": ^7.22.3
- checksum: 385289ee8b87cf9af448bbb9fcf747f6e67600db5f7f64eb4ad97761ee387819bf2212b6a757008286c6bfacf4f3fc0b6de88686f2e517a70fb59996bdfbd1e9
+ "@babel/template": ^7.22.5
+ "@babel/traverse": ^7.22.6
+ "@babel/types": ^7.22.5
+ checksum: 5c1f33241fe7bf7709868c2105134a0a86dca26a0fbd508af10a89312b1f77ca38ebae43e50be3b208613c5eacca1559618af4ca236f0abc55d294800faeff30
languageName: node
linkType: hard
-"@babel/highlight@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/highlight@npm:7.18.6"
+"@babel/highlight@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/highlight@npm:7.22.5"
dependencies:
- "@babel/helper-validator-identifier": ^7.18.6
+ "@babel/helper-validator-identifier": ^7.22.5
chalk: ^2.0.0
js-tokens: ^4.0.0
- checksum: 92d8ee61549de5ff5120e945e774728e5ccd57fd3b2ed6eace020ec744823d4a98e242be1453d21764a30a14769ecd62170fba28539b211799bbaf232bbb2789
+ checksum: f61ae6de6ee0ea8d9b5bcf2a532faec5ab0a1dc0f7c640e5047fc61630a0edb88b18d8c92eb06566d30da7a27db841aca11820ecd3ebe9ce514c9350fbed39c4
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.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"
+"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.5, @babel/parser@npm:^7.22.7":
+ version: 7.22.7
+ resolution: "@babel/parser@npm:7.22.7"
bin:
parser: ./bin/babel-parser.js
- checksum: 0ca6d3a2d9aae2504ba1bc494704b64a83140884f7379f609de69bd39b60adb58a4f8ec692fe53fef8657dd82705d01b7e6efb65e18296326bdd66f71d52d9a9
+ checksum: 02209ddbd445831ee8bf966fdf7c29d189ed4b14343a68eb2479d940e7e3846340d7cc6bd654a5f3d87d19dc84f49f50a58cf9363bee249dc5409ff3ba3dab54
languageName: node
linkType: hard
-"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6"
+"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0
- checksum: 845bd280c55a6a91d232cfa54eaf9708ec71e594676fe705794f494bb8b711d833b752b59d1a5c154695225880c23dbc9cab0e53af16fd57807976cd3ff41b8d
+ checksum: 1e353a060fb2cd8f1256d28cd768f16fb02513f905b9b6d656fb0242c96c341a196fa188b27c2701506a6e27515359fbcc1a5ca7fa8b9b530cf88fbd137baefc
languageName: node
linkType: hard
-"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.20.7":
- version: 7.20.7
- resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.20.7"
+"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0
- "@babel/plugin-proposal-optional-chaining": ^7.20.7
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5
+ "@babel/plugin-transform-optional-chaining": ^7.22.5
peerDependencies:
"@babel/core": ^7.13.0
- checksum: d610f532210bee5342f5b44a12395ccc6d904e675a297189bc1e401cc185beec09873da523466d7fec34ae1574f7a384235cba1ccc9fe7b89ba094167897c845
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-async-generator-functions@npm:^7.20.7":
- version: 7.20.7
- resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.20.7"
- dependencies:
- "@babel/helper-environment-visitor": ^7.18.9
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-remap-async-to-generator": ^7.18.9
- "@babel/plugin-syntax-async-generators": ^7.8.4
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 111109ee118c9e69982f08d5e119eab04190b36a0f40e22e873802d941956eee66d2aa5a15f5321e51e3f9aa70a91136451b987fe15185ef8cc547ac88937723
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-class-properties@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6"
- dependencies:
- "@babel/helper-create-class-features-plugin": ^7.18.6
- "@babel/helper-plugin-utils": ^7.18.6
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 49a78a2773ec0db56e915d9797e44fd079ab8a9b2e1716e0df07c92532f2c65d76aeda9543883916b8e0ff13606afeffa67c5b93d05b607bc87653ad18a91422
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-class-static-block@npm:^7.21.0":
- version: 7.21.0
- resolution: "@babel/plugin-proposal-class-static-block@npm:7.21.0"
- dependencies:
- "@babel/helper-create-class-features-plugin": ^7.21.0
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/plugin-syntax-class-static-block": ^7.14.5
- peerDependencies:
- "@babel/core": ^7.12.0
- checksum: 236c0ad089e7a7acab776cc1d355330193314bfcd62e94e78f2df35817c6144d7e0e0368976778afd6b7c13e70b5068fa84d7abbf967d4f182e60d03f9ef802b
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-dynamic-import@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-dynamic-import@npm:7.18.6"
- dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
- "@babel/plugin-syntax-dynamic-import": ^7.8.3
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 96b1c8a8ad8171d39e9ab106be33bde37ae09b22fb2c449afee9a5edf3c537933d79d963dcdc2694d10677cb96da739cdf1b53454e6a5deab9801f28a818bb2f
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-export-namespace-from@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/plugin-proposal-export-namespace-from@npm:7.18.9"
- dependencies:
- "@babel/helper-plugin-utils": ^7.18.9
- "@babel/plugin-syntax-export-namespace-from": ^7.8.3
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 84ff22bacc5d30918a849bfb7e0e90ae4c5b8d8b65f2ac881803d1cf9068dffbe53bd657b0e4bc4c20b4db301b1c85f1e74183cf29a0dd31e964bd4e97c363ef
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-json-strings@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-json-strings@npm:7.18.6"
- dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
- "@babel/plugin-syntax-json-strings": ^7.8.3
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 25ba0e6b9d6115174f51f7c6787e96214c90dd4026e266976b248a2ed417fe50fddae72843ffb3cbe324014a18632ce5648dfac77f089da858022b49fd608cb3
+ checksum: 16e7a5f3bf2f2ac0ca032a70bf0ebd7e886d84dbb712b55c0643c04c495f0f221fbcbca14b5f8f8027fa6c87a3dafae0934022ad2b409384af6c5c356495b7bd
languageName: node
linkType: hard
-"@babel/plugin-proposal-logical-assignment-operators@npm:^7.20.7":
- version: 7.20.7
- resolution: "@babel/plugin-proposal-logical-assignment-operators@npm:7.20.7"
- dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: cdd7b8136cc4db3f47714d5266f9e7b592a2ac5a94a5878787ce08890e97c8ab1ca8e94b27bfeba7b0f2b1549a026d9fc414ca2196de603df36fb32633bbdc19
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6"
- dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
- "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 949c9ddcdecdaec766ee610ef98f965f928ccc0361dd87cf9f88cf4896a6ccd62fce063d4494778e50da99dea63d270a1be574a62d6ab81cbe9d85884bf55a7d
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-numeric-separator@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-numeric-separator@npm:7.18.6"
- dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
- "@babel/plugin-syntax-numeric-separator": ^7.10.4
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: f370ea584c55bf4040e1f78c80b4eeb1ce2e6aaa74f87d1a48266493c33931d0b6222d8cee3a082383d6bb648ab8d6b7147a06f974d3296ef3bc39c7851683ec
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-object-rest-spread@npm:^7.20.7":
- version: 7.20.7
- resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.7"
- dependencies:
- "@babel/compat-data": ^7.20.5
- "@babel/helper-compilation-targets": ^7.20.7
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/plugin-syntax-object-rest-spread": ^7.8.3
- "@babel/plugin-transform-parameters": ^7.20.7
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 1329db17009964bc644484c660eab717cb3ca63ac0ab0f67c651a028d1bc2ead51dc4064caea283e46994f1b7221670a35cbc0b4beb6273f55e915494b5aa0b2
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-optional-catch-binding@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-optional-catch-binding@npm:7.18.6"
- dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
- "@babel/plugin-syntax-optional-catch-binding": ^7.8.3
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 7b5b39fb5d8d6d14faad6cb68ece5eeb2fd550fb66b5af7d7582402f974f5bc3684641f7c192a5a57e0f59acfae4aada6786be1eba030881ddc590666eff4d1e
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-optional-chaining@npm:^7.20.7, @babel/plugin-proposal-optional-chaining@npm:^7.21.0":
- version: 7.21.0
- resolution: "@babel/plugin-proposal-optional-chaining@npm:7.21.0"
- dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0
- "@babel/plugin-syntax-optional-chaining": ^7.8.3
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 11c5449e01b18bb8881e8e005a577fa7be2fe5688e2382c8822d51f8f7005342a301a46af7b273b1f5645f9a7b894c428eee8526342038a275ef6ba4c8d8d746
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-private-methods@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-proposal-private-methods@npm:7.18.6"
- dependencies:
- "@babel/helper-create-class-features-plugin": ^7.18.6
- "@babel/helper-plugin-utils": ^7.18.6
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 22d8502ee96bca99ad2c8393e8493e2b8d4507576dd054490fd8201a36824373440106f5b098b6d821b026c7e72b0424ff4aeca69ed5f42e48f029d3a156d5ad
- languageName: node
- linkType: hard
-
-"@babel/plugin-proposal-private-property-in-object@npm:^7.21.0":
- version: 7.21.0
- resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.21.0"
- dependencies:
- "@babel/helper-annotate-as-pure": ^7.18.6
- "@babel/helper-create-class-features-plugin": ^7.21.0
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/plugin-syntax-private-property-in-object": ^7.14.5
+"@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2":
+ version: 7.21.0-placeholder-for-preset-env.2
+ resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2"
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: add881a6a836635c41d2710551fdf777e2c07c0b691bf2baacc5d658dd64107479df1038680d6e67c468bfc6f36fb8920025d6bac2a1df0a81b867537d40ae78
+ checksum: d97745d098b835d55033ff3a7fb2b895b9c5295b08a5759e4f20df325aa385a3e0bc9bd5ad8f2ec554a44d4e6525acfc257b8c5848a1345cb40f26a30e277e91
languageName: node
linkType: hard
-"@babel/plugin-proposal-unicode-property-regex@npm:^7.18.6, @babel/plugin-proposal-unicode-property-regex@npm:^7.4.4":
+"@babel/plugin-proposal-unicode-property-regex@npm:^7.4.4":
version: 7.18.6
resolution: "@babel/plugin-proposal-unicode-property-regex@npm:7.18.6"
dependencies:
@@ -621,14 +448,25 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-import-assertions@npm:^7.20.0":
- version: 7.20.0
- resolution: "@babel/plugin-syntax-import-assertions@npm:7.20.0"
+"@babel/plugin-syntax-import-assertions@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-syntax-import-assertions@npm:7.22.5"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 2b8b5572db04a7bef1e6cd20debf447e4eef7cb012616f5eceb8fa3e23ce469b8f76ee74fd6d1e158ba17a8f58b0aec579d092fb67c5a30e83ccfbc5754916c1
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-syntax-import-attributes@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-syntax-import-attributes@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.19.0
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 6a86220e0aae40164cd3ffaf80e7c076a1be02a8f3480455dddbae05fda8140f429290027604df7a11b3f3f124866e8a6d69dbfa1dda61ee7377b920ad144d5b
+ checksum: 197b3c5ea2a9649347f033342cb222ab47f4645633695205c0250c6bf2af29e643753b8bb24a2db39948bef08e7c540babfd365591eb57fc110cb30b425ffc47
languageName: node
linkType: hard
@@ -654,14 +492,14 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-jsx@npm:^7.21.4, @babel/plugin-syntax-jsx@npm:^7.7.2":
- version: 7.21.4
- resolution: "@babel/plugin-syntax-jsx@npm:7.21.4"
+"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2":
+ version: 7.22.5
+ resolution: "@babel/plugin-syntax-jsx@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: bb7309402a1d4e155f32aa0cf216e1fa8324d6c4cfd248b03280028a015a10e46b6efd6565f515f8913918a3602b39255999c06046f7d4b8a5106be2165d724a
+ checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce
languageName: node
linkType: hard
@@ -753,617 +591,834 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-typescript@npm:^7.20.0, @babel/plugin-syntax-typescript@npm:^7.7.2":
- version: 7.21.4
- resolution: "@babel/plugin-syntax-typescript@npm:7.21.4"
+"@babel/plugin-syntax-typescript@npm:^7.22.5, @babel/plugin-syntax-typescript@npm:^7.7.2":
+ version: 7.22.5
+ resolution: "@babel/plugin-syntax-typescript@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: a59ce2477b7ae8c8945dc37dda292fef9ce46a6507b3d76b03ce7f3a6c9451a6567438b20a78ebcb3955d04095fd1ccd767075a863f79fcc30aa34dcfa441fe0
+ checksum: 8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a
languageName: node
linkType: hard
-"@babel/plugin-transform-arrow-functions@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/plugin-transform-arrow-functions@npm:7.21.5"
+"@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6":
+ version: 7.18.6
+ resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6"
dependencies:
- "@babel/helper-plugin-utils": ^7.21.5
+ "@babel/helper-create-regexp-features-plugin": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.18.6
peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: c7c281cdf37c33a584102d9fd1793e85c96d4d320cdfb7c43f1ce581323d057f13b53203994fcc7ee1f8dc1ff013498f258893aa855a06c6f830fcc4c33d6e44
+ "@babel/core": ^7.0.0
+ checksum: a651d700fe63ff0ddfd7186f4ebc24447ca734f114433139e3c027bc94a900d013cf1ef2e2db8430425ba542e39ae160c3b05f06b59fd4656273a3df97679e9c
languageName: node
linkType: hard
-"@babel/plugin-transform-async-to-generator@npm:^7.20.7":
- version: 7.20.7
- resolution: "@babel/plugin-transform-async-to-generator@npm:7.20.7"
+"@babel/plugin-transform-arrow-functions@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-arrow-functions@npm:7.22.5"
dependencies:
- "@babel/helper-module-imports": ^7.18.6
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-remap-async-to-generator": ^7.18.9
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: fe9ee8a5471b4317c1b9ea92410ace8126b52a600d7cfbfe1920dcac6fb0fad647d2e08beb4fd03c630eb54430e6c72db11e283e3eddc49615c68abd39430904
+ checksum: 35abb6c57062802c7ce8bd96b2ef2883e3124370c688bbd67609f7d2453802fb73944df8808f893b6c67de978eb2bcf87bbfe325e46d6f39b5fcb09ece11d01a
languageName: node
linkType: hard
-"@babel/plugin-transform-block-scoped-functions@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.18.6"
+"@babel/plugin-transform-async-generator-functions@npm:^7.22.7":
+ version: 7.22.7
+ resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.7"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-environment-visitor": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-remap-async-to-generator": ^7.22.5
+ "@babel/plugin-syntax-async-generators": ^7.8.4
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 0a0df61f94601e3666bf39f2cc26f5f7b22a94450fb93081edbed967bd752ce3f81d1227fefd3799f5ee2722171b5e28db61379234d1bb85b6ec689589f99d7e
+ checksum: 57cd2cce3fb696dadf00e88f168683df69e900b92dadeae07429243c43bc21d5ccdc0c2db61cf5c37bd0fbd893fc455466bef6babe4aa5b79d9cb8ba89f40ae7
languageName: node
linkType: hard
-"@babel/plugin-transform-block-scoping@npm:^7.21.0":
- version: 7.21.0
- resolution: "@babel/plugin-transform-block-scoping@npm:7.21.0"
+"@babel/plugin-transform-async-to-generator@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-async-to-generator@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-module-imports": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-remap-async-to-generator": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 15aacaadbecf96b53a750db1be4990b0d89c7f5bc3e1794b63b49fb219638c1fd25d452d15566d7e5ddf5b5f4e1a0a0055c35c1c7aee323c7b114bf49f66f4b0
+ checksum: b95f23f99dcb379a9f0a1c2a3bbea3f8dc0e1b16dc1ac8b484fe378370169290a7a63d520959a9ba1232837cf74a80e23f6facbe14fd42a3cda6d3c2d7168e62
languageName: node
linkType: hard
-"@babel/plugin-transform-classes@npm:^7.21.0":
- version: 7.21.0
- resolution: "@babel/plugin-transform-classes@npm:7.21.0"
+"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.22.5"
dependencies:
- "@babel/helper-annotate-as-pure": ^7.18.6
- "@babel/helper-compilation-targets": ^7.20.7
- "@babel/helper-environment-visitor": ^7.18.9
- "@babel/helper-function-name": ^7.21.0
- "@babel/helper-optimise-call-expression": ^7.18.6
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-replace-supers": ^7.20.7
- "@babel/helper-split-export-declaration": ^7.18.6
- globals: ^11.1.0
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 088ae152074bd0e90f64659169255bfe50393e637ec8765cb2a518848b11b0299e66b91003728fd0a41563a6fdc6b8d548ece698a314fd5447f5489c22e466b7
+ checksum: 416b1341858e8ca4e524dee66044735956ced5f478b2c3b9bc11ec2285b0c25d7dbb96d79887169eb938084c95d0a89338c8b2fe70d473bd9dc92e5d9db1732c
languageName: node
linkType: hard
-"@babel/plugin-transform-computed-properties@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/plugin-transform-computed-properties@npm:7.21.5"
+"@babel/plugin-transform-block-scoping@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-block-scoping@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.21.5
- "@babel/template": ^7.20.7
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: e819780ab30fc40d7802ffb75b397eff63ca4942a1873058f81c80f660189b78e158fa03fd3270775f0477c4c33cee3d8d40270e64404bbf24aa6cdccb197e7b
+ checksum: 26987002cfe6e24544e60fa35f07052b6557f590c1a1cc5cf35d6dc341d7fea163c1222a2d70d5d2692f0b9860d942fd3ba979848b2995d4debffa387b9b19ae
languageName: node
linkType: hard
-"@babel/plugin-transform-destructuring@npm:^7.21.3":
- version: 7.21.3
- resolution: "@babel/plugin-transform-destructuring@npm:7.21.3"
+"@babel/plugin-transform-class-properties@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-class-properties@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-create-class-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 43ebbe0bfa20287e34427be7c2200ce096c20913775ea75268fb47fe0e55f9510800587e6052c42fe6dffa0daaad95dd465c3e312fd1ef9785648384c45417ac
+ checksum: b830152dfc2ff2f647f0abe76e6251babdfbef54d18c4b2c73a6bf76b1a00050a5d998dac80dc901a48514e95604324943a9dd39317073fe0928b559e0e0c579
languageName: node
linkType: hard
-"@babel/plugin-transform-dotall-regex@npm:^7.18.6, @babel/plugin-transform-dotall-regex@npm:^7.4.4":
- version: 7.18.6
- resolution: "@babel/plugin-transform-dotall-regex@npm:7.18.6"
+"@babel/plugin-transform-class-static-block@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-class-static-block@npm:7.22.5"
dependencies:
- "@babel/helper-create-regexp-features-plugin": ^7.18.6
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-create-class-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-class-static-block": ^7.14.5
peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: cbe5d7063eb8f8cca24cd4827bc97f5641166509e58781a5f8aa47fb3d2d786ce4506a30fca2e01f61f18792783a5cb5d96bf5434c3dd1ad0de8c9cc625a53da
+ "@babel/core": ^7.12.0
+ checksum: bc48b92dbaf625a14f2bf62382384eef01e0515802426841636ae9146e27395d068c7a8a45e9e15699491b0a01d990f38f179cbc9dc89274a393f85648772f12
languageName: node
linkType: hard
-"@babel/plugin-transform-duplicate-keys@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/plugin-transform-duplicate-keys@npm:7.18.9"
+"@babel/plugin-transform-classes@npm:^7.22.6":
+ version: 7.22.6
+ resolution: "@babel/plugin-transform-classes@npm:7.22.6"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.9
+ "@babel/helper-annotate-as-pure": ^7.22.5
+ "@babel/helper-compilation-targets": ^7.22.6
+ "@babel/helper-environment-visitor": ^7.22.5
+ "@babel/helper-function-name": ^7.22.5
+ "@babel/helper-optimise-call-expression": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-replace-supers": ^7.22.5
+ "@babel/helper-split-export-declaration": ^7.22.6
+ globals: ^11.1.0
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 220bf4a9fec5c4d4a7b1de38810350260e8ea08481bf78332a464a21256a95f0df8cd56025f346238f09b04f8e86d4158fafc9f4af57abaef31637e3b58bd4fe
+ checksum: 8380e855c01033dbc7460d9acfbc1fc37c880350fa798c2de8c594ef818ade0e4c96173ec72f05f2a4549d8d37135e18cb62548352d51557b45a0fb4388d2f3f
languageName: node
linkType: hard
-"@babel/plugin-transform-exponentiation-operator@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.18.6"
+"@babel/plugin-transform-computed-properties@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-computed-properties@npm:7.22.5"
dependencies:
- "@babel/helper-builder-binary-assignment-operator-visitor": ^7.18.6
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/template": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 7f70222f6829c82a36005508d34ddbe6fd0974ae190683a8670dd6ff08669aaf51fef2209d7403f9bd543cb2d12b18458016c99a6ed0332ccedb3ea127b01229
+ checksum: c2a77a0f94ec71efbc569109ec14ea2aa925b333289272ced8b33c6108bdbb02caf01830ffc7e49486b62dec51911924d13f3a76f1149f40daace1898009e131
languageName: node
linkType: hard
-"@babel/plugin-transform-for-of@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/plugin-transform-for-of@npm:7.21.5"
+"@babel/plugin-transform-destructuring@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-destructuring@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.21.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: b6666b24e8ca1ffbf7452a0042149724e295965aad55070dc9ee992451d69d855fc9db832c1c5fb4d3dc532f4a18a2974d5f8524f5c2250dda888d05f6f3cadb
+ checksum: 76f6ea2aee1fcfa1c3791eb7a5b89703c6472650b993e8666fff0f1d6e9d737a84134edf89f63c92297f3e75064c1263219463b02dd9bc7434b6e5b9935e3f20
languageName: node
linkType: hard
-"@babel/plugin-transform-function-name@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/plugin-transform-function-name@npm:7.18.9"
+"@babel/plugin-transform-dotall-regex@npm:^7.22.5, @babel/plugin-transform-dotall-regex@npm:^7.4.4":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-dotall-regex@npm:7.22.5"
dependencies:
- "@babel/helper-compilation-targets": ^7.18.9
- "@babel/helper-function-name": ^7.18.9
- "@babel/helper-plugin-utils": ^7.18.9
+ "@babel/helper-create-regexp-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 62dd9c6cdc9714704efe15545e782ee52d74dc73916bf954b4d3bee088fb0ec9e3c8f52e751252433656c09f744b27b757fc06ed99bcde28e8a21600a1d8e597
+ checksum: 409b658d11e3082c8f69e9cdef2d96e4d6d11256f005772425fb230cc48fd05945edbfbcb709dab293a1a2f01f9c8a5bb7b4131e632b23264039d9f95864b453
languageName: node
linkType: hard
-"@babel/plugin-transform-literals@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/plugin-transform-literals@npm:7.18.9"
+"@babel/plugin-transform-duplicate-keys@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-duplicate-keys@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.9
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 3458dd2f1a47ac51d9d607aa18f3d321cbfa8560a985199185bed5a906bb0c61ba85575d386460bac9aed43fdd98940041fae5a67dff286f6f967707cff489f8
+ checksum: bb1280fbabaab6fab2ede585df34900712698210a3bd413f4df5bae6d8c24be36b496c92722ae676a7a67d060a4624f4d6c23b923485f906bfba8773c69f55b4
languageName: node
linkType: hard
-"@babel/plugin-transform-member-expression-literals@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-member-expression-literals@npm:7.18.6"
+"@babel/plugin-transform-dynamic-import@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-dynamic-import": ^7.8.3
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 35a3d04f6693bc6b298c05453d85ee6e41cc806538acb6928427e0e97ae06059f97d2f07d21495fcf5f70d3c13a242e2ecbd09d5c1fcb1b1a73ff528dcb0b695
+ checksum: 186a6d59f36eb3c5824739fc9c22ed0f4ca68e001662aa3a302634346a8b785cb9579b23b0c158f4570604d697d19598ca09b58c60a7fa2894da1163c4eb1907
languageName: node
linkType: hard
-"@babel/plugin-transform-modules-amd@npm:^7.20.11":
- version: 7.20.11
- resolution: "@babel/plugin-transform-modules-amd@npm:7.20.11"
+"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.22.5"
dependencies:
- "@babel/helper-module-transforms": ^7.20.11
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-builder-binary-assignment-operator-visitor": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 23665c1c20c8f11c89382b588fb9651c0756d130737a7625baeaadbd3b973bc5bfba1303bedffa8fb99db1e6d848afb01016e1df2b69b18303e946890c790001
+ checksum: f2d660c1b1d51ad5fec1cd5ad426a52187204068c4158f8c4aa977b31535c61b66898d532603eef21c15756827be8277f724c869b888d560f26d7fe848bb5eae
languageName: node
linkType: hard
-"@babel/plugin-transform-modules-commonjs@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/plugin-transform-modules-commonjs@npm:7.21.5"
+"@babel/plugin-transform-export-namespace-from@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.5"
dependencies:
- "@babel/helper-module-transforms": ^7.21.5
- "@babel/helper-plugin-utils": ^7.21.5
- "@babel/helper-simple-access": ^7.21.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-export-namespace-from": ^7.8.3
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: d9ff7a21baaa60c08a0c86c5e468bb4b2bd85caf51ba78712d8f45e9afa2498d50d6cdf349696e08aa820cafed65f19b70e5938613db9ebb095f7aba1127f282
+ checksum: 3d197b788758044983c96b9c49bed4b456055f35a388521a405968db0f6e2ffb6fd59110e3931f4dcc5e126ae9e5e00e154a0afb47a7ea359d8d0dea79f480d7
languageName: node
linkType: hard
-"@babel/plugin-transform-modules-systemjs@npm:^7.20.11":
- version: 7.20.11
- resolution: "@babel/plugin-transform-modules-systemjs@npm:7.20.11"
+"@babel/plugin-transform-for-of@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-for-of@npm:7.22.5"
dependencies:
- "@babel/helper-hoist-variables": ^7.18.6
- "@babel/helper-module-transforms": ^7.20.11
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-validator-identifier": ^7.19.1
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 4546c47587f88156d66c7eb7808e903cf4bb3f6ba6ac9bc8e3af2e29e92eb9f0b3f44d52043bfd24eb25fa7827fd7b6c8bfeac0cac7584e019b87e1ecbd0e673
+ checksum: d7b8d4db010bce7273674caa95c4e6abd909362866ce297e86a2ecaa9ae636e05d525415811db9b3c942155df7f3651d19b91dd6c41f142f7308a97c7cb06023
languageName: node
linkType: hard
-"@babel/plugin-transform-modules-umd@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-modules-umd@npm:7.18.6"
+"@babel/plugin-transform-function-name@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-function-name@npm:7.22.5"
dependencies:
- "@babel/helper-module-transforms": ^7.18.6
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-compilation-targets": ^7.22.5
+ "@babel/helper-function-name": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: c3b6796c6f4579f1ba5ab0cdcc73910c1e9c8e1e773c507c8bb4da33072b3ae5df73c6d68f9126dab6e99c24ea8571e1563f8710d7c421fac1cde1e434c20153
+ checksum: cff3b876357999cb8ae30e439c3ec6b0491a53b0aa6f722920a4675a6dd5b53af97a833051df4b34791fe5b3dd326ccf769d5c8e45b322aa50ee11a660b17845
languageName: node
linkType: hard
-"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.20.5":
- version: 7.20.5
- resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.20.5"
+"@babel/plugin-transform-json-strings@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-json-strings@npm:7.22.5"
dependencies:
- "@babel/helper-create-regexp-features-plugin": ^7.20.5
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-json-strings": ^7.8.3
peerDependencies:
- "@babel/core": ^7.0.0
- checksum: 528c95fb1087e212f17e1c6456df041b28a83c772b9c93d2e407c9d03b72182b0d9d126770c1d6e0b23aab052599ceaf25ed6a2c0627f4249be34a83f6fae853
+ "@babel/core": ^7.0.0-0
+ checksum: 4e00b902487a670b6c8948f33f9108133fd745cf9d1478aca515fb460b9b2f12e137988ebc1663630fb82070a870aed8b0c1aa4d007a841c18004619798f255c
languageName: node
linkType: hard
-"@babel/plugin-transform-new-target@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-new-target@npm:7.18.6"
+"@babel/plugin-transform-literals@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-literals@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: bd780e14f46af55d0ae8503b3cb81ca86dcc73ed782f177e74f498fff934754f9e9911df1f8f3bd123777eed7c1c1af4d66abab87c8daae5403e7719a6b845d1
+ checksum: ec37cc2ffb32667af935ab32fe28f00920ec8a1eb999aa6dc6602f2bebd8ba205a558aeedcdccdebf334381d5c57106c61f52332045730393e73410892a9735b
languageName: node
linkType: hard
-"@babel/plugin-transform-object-super@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-object-super@npm:7.18.6"
+"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
- "@babel/helper-replace-supers": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 0fcb04e15deea96ae047c21cb403607d49f06b23b4589055993365ebd7a7d7541334f06bf9642e90075e66efce6ebaf1eb0ef066fbbab802d21d714f1aac3aef
+ checksum: 18748e953c08f64885f18c224eac58df10a13eac4d845d16b5d9b6276907da7ca2530dfebe6ed41cdc5f8a75d9db3e36d8eb54ddce7cd0364af1cab09b435302
languageName: node
linkType: hard
-"@babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.21.3":
- version: 7.21.3
- resolution: "@babel/plugin-transform-parameters@npm:7.21.3"
+"@babel/plugin-transform-member-expression-literals@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-member-expression-literals@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: c92128d7b1fcf54e2cab186c196bbbf55a9a6de11a83328dc2602649c9dc6d16ef73712beecd776cd49bfdc624b5f56740f4a53568d3deb9505ec666bc869da3
+ checksum: ec4b0e07915ddd4fda0142fd104ee61015c208608a84cfa13643a95d18760b1dc1ceb6c6e0548898b8c49e5959a994e46367260176dbabc4467f729b21868504
languageName: node
linkType: hard
-"@babel/plugin-transform-property-literals@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-property-literals@npm:7.18.6"
+"@babel/plugin-transform-modules-amd@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-modules-amd@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-module-transforms": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 1c16e64de554703f4b547541de2edda6c01346dd3031d4d29e881aa7733785cd26d53611a4ccf5353f4d3e69097bb0111c0a93ace9e683edd94fea28c4484144
+ checksum: 7da4c4ebbbcf7d182abb59b2046b22d86eee340caf8a22a39ef6a727da2d8acfec1f714fcdcd5054110b280e4934f735e80a6848d192b6834c5d4459a014f04d
languageName: node
linkType: hard
-"@babel/plugin-transform-regenerator@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/plugin-transform-regenerator@npm:7.21.5"
+"@babel/plugin-transform-modules-commonjs@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.21.5
- regenerator-transform: ^0.15.1
+ "@babel/helper-module-transforms": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-simple-access": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 5291f6871276f57a6004f16d50ae9ad57f22a6aa2a183b8c84de8126f1066c6c9f9bbeadb282b5207fa9e7b0f57e40a8421d46cb5c60caf7e2848e98224d5639
+ checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa
languageName: node
linkType: hard
-"@babel/plugin-transform-reserved-words@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-reserved-words@npm:7.18.6"
+"@babel/plugin-transform-modules-systemjs@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-hoist-variables": ^7.22.5
+ "@babel/helper-module-transforms": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-validator-identifier": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 0738cdc30abdae07c8ec4b233b30c31f68b3ff0eaa40eddb45ae607c066127f5fa99ddad3c0177d8e2832e3a7d3ad115775c62b431ebd6189c40a951b867a80c
+ checksum: 04f4178589543396b3c24330a67a59c5e69af5e96119c9adda730c0f20122deaff54671ebbc72ad2df6495a5db8a758bd96942de95fba7ad427de9c80b1b38c8
languageName: node
linkType: hard
-"@babel/plugin-transform-shorthand-properties@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-shorthand-properties@npm:7.18.6"
+"@babel/plugin-transform-modules-umd@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-modules-umd@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-module-transforms": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: b8e4e8acc2700d1e0d7d5dbfd4fdfb935651913de6be36e6afb7e739d8f9ca539a5150075a0f9b79c88be25ddf45abb912fe7abf525f0b80f5b9d9860de685d7
+ checksum: 46622834c54c551b231963b867adbc80854881b3e516ff29984a8da989bd81665bd70e8cba6710345248e97166689310f544aee1a5773e262845a8f1b3e5b8b4
languageName: node
linkType: hard
-"@babel/plugin-transform-spread@npm:^7.20.7":
- version: 7.20.7
- resolution: "@babel/plugin-transform-spread@npm:7.20.7"
+"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0
+ "@babel/helper-create-regexp-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 8ea698a12da15718aac7489d4cde10beb8a3eea1f66167d11ab1e625033641e8b328157fd1a0b55dd6531933a160c01fc2e2e61132a385cece05f26429fd0cc2
+ "@babel/core": ^7.0.0
+ checksum: 3ee564ddee620c035b928fdc942c5d17e9c4b98329b76f9cefac65c111135d925eb94ed324064cd7556d4f5123beec79abea1d4b97d1c8a2a5c748887a2eb623
languageName: node
linkType: hard
-"@babel/plugin-transform-sticky-regex@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-sticky-regex@npm:7.18.6"
+"@babel/plugin-transform-new-target@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-new-target@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 68ea18884ae9723443ffa975eb736c8c0d751265859cd3955691253f7fee37d7a0f7efea96c8a062876af49a257a18ea0ed5fea0d95a7b3611ce40f7ee23aee3
+ checksum: 6b72112773487a881a1d6ffa680afde08bad699252020e86122180ee7a88854d5da3f15d9bca3331cf2e025df045604494a8208a2e63b486266b07c14e2ffbf3
languageName: node
linkType: hard
-"@babel/plugin-transform-template-literals@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/plugin-transform-template-literals@npm:7.18.9"
+"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.9
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 3d2fcd79b7c345917f69b92a85bdc3ddd68ce2c87dc70c7d61a8373546ccd1f5cb8adc8540b49dfba08e1b82bb7b3bbe23a19efdb2b9c994db2db42906ca9fb2
+ checksum: e6a059169d257fc61322d0708edae423072449b7c33de396261e68dee582aec5396789a1c22bce84e5bd88a169623c2e750b513fc222930979e6accd52a44bf2
languageName: node
linkType: hard
-"@babel/plugin-transform-typeof-symbol@npm:^7.18.9":
- version: 7.18.9
- resolution: "@babel/plugin-transform-typeof-symbol@npm:7.18.9"
+"@babel/plugin-transform-numeric-separator@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.18.9
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-numeric-separator": ^7.10.4
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: e754e0d8b8a028c52e10c148088606e3f7a9942c57bd648fc0438e5b4868db73c386a5ed47ab6d6f0594aae29ee5ffc2ffc0f7ebee7fae560a066d6dea811cd4
+ checksum: 9e7837d4eae04f211ebaa034fe5003d2927b6bf6d5b9dc09f2b1183c01482cdde5a75b8bd5c7ff195c2abc7b923339eb0b2a9d27cb78359d38248a3b2c2367c4
languageName: node
linkType: hard
-"@babel/plugin-transform-typescript@npm:^7.21.3":
- version: 7.21.3
- resolution: "@babel/plugin-transform-typescript@npm:7.21.3"
+"@babel/plugin-transform-object-rest-spread@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.5"
dependencies:
- "@babel/helper-annotate-as-pure": ^7.18.6
- "@babel/helper-create-class-features-plugin": ^7.21.0
- "@babel/helper-plugin-utils": ^7.20.2
- "@babel/plugin-syntax-typescript": ^7.20.0
+ "@babel/compat-data": ^7.22.5
+ "@babel/helper-compilation-targets": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-object-rest-spread": ^7.8.3
+ "@babel/plugin-transform-parameters": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: c16fd577bf43f633deb76fca2a8527d8ae25968c8efdf327c1955472c3e0257e62992473d1ad7f9ee95379ce2404699af405ea03346055adadd3478ad0ecd117
+ checksum: 3b5e091f0dc67108f2e41ed5a97e15bbe4381a19d9a7eea80b71c7de1d8169fd28784e1e41a3d2ad12709ab212e58fc481282a5bb65d591fae7b443048de3330
languageName: node
linkType: hard
-"@babel/plugin-transform-unicode-escapes@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/plugin-transform-unicode-escapes@npm:7.21.5"
+"@babel/plugin-transform-object-super@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-object-super@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.21.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-replace-supers": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 6504d642d0449a275191b624bd94d3e434ae154e610bf2f0e3c109068b287d2474f68e1da64b47f21d193cd67b27ee4643877d530187670565cac46e29fd257d
+ checksum: b71887877d74cb64dbccb5c0324fa67e31171e6a5311991f626650e44a4083e5436a1eaa89da78c0474fb095d4ec322d63ee778b202d33aa2e4194e1ed8e62d7
languageName: node
linkType: hard
-"@babel/plugin-transform-unicode-regex@npm:^7.18.6":
- version: 7.18.6
- resolution: "@babel/plugin-transform-unicode-regex@npm:7.18.6"
+"@babel/plugin-transform-optional-catch-binding@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.5"
dependencies:
- "@babel/helper-create-regexp-features-plugin": ^7.18.6
- "@babel/helper-plugin-utils": ^7.18.6
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-optional-catch-binding": ^7.8.3
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: d9e18d57536a2d317fb0b7c04f8f55347f3cfacb75e636b4c6fa2080ab13a3542771b5120e726b598b815891fc606d1472ac02b749c69fd527b03847f22dc25e
- languageName: node
- linkType: hard
-
-"@babel/preset-env@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/preset-env@npm:7.21.5"
- dependencies:
- "@babel/compat-data": ^7.21.5
- "@babel/helper-compilation-targets": ^7.21.5
- "@babel/helper-plugin-utils": ^7.21.5
- "@babel/helper-validator-option": ^7.21.0
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.20.7
- "@babel/plugin-proposal-async-generator-functions": ^7.20.7
- "@babel/plugin-proposal-class-properties": ^7.18.6
- "@babel/plugin-proposal-class-static-block": ^7.21.0
- "@babel/plugin-proposal-dynamic-import": ^7.18.6
- "@babel/plugin-proposal-export-namespace-from": ^7.18.9
- "@babel/plugin-proposal-json-strings": ^7.18.6
- "@babel/plugin-proposal-logical-assignment-operators": ^7.20.7
- "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6
- "@babel/plugin-proposal-numeric-separator": ^7.18.6
- "@babel/plugin-proposal-object-rest-spread": ^7.20.7
- "@babel/plugin-proposal-optional-catch-binding": ^7.18.6
- "@babel/plugin-proposal-optional-chaining": ^7.21.0
- "@babel/plugin-proposal-private-methods": ^7.18.6
- "@babel/plugin-proposal-private-property-in-object": ^7.21.0
- "@babel/plugin-proposal-unicode-property-regex": ^7.18.6
- "@babel/plugin-syntax-async-generators": ^7.8.4
- "@babel/plugin-syntax-class-properties": ^7.12.13
- "@babel/plugin-syntax-class-static-block": ^7.14.5
- "@babel/plugin-syntax-dynamic-import": ^7.8.3
- "@babel/plugin-syntax-export-namespace-from": ^7.8.3
- "@babel/plugin-syntax-import-assertions": ^7.20.0
- "@babel/plugin-syntax-import-meta": ^7.10.4
- "@babel/plugin-syntax-json-strings": ^7.8.3
- "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4
- "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3
- "@babel/plugin-syntax-numeric-separator": ^7.10.4
- "@babel/plugin-syntax-object-rest-spread": ^7.8.3
- "@babel/plugin-syntax-optional-catch-binding": ^7.8.3
+ checksum: b0e8b4233ff06b5c9d285257f49c5bd441f883189b24282e6200f9ebdf5db29aeeebbffae57fbbcd5df9f4387b3e66e5d322aaae5652a78e89685ddbae46bbd1
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-optional-chaining@npm:^7.22.5, @babel/plugin-transform-optional-chaining@npm:^7.22.6":
+ version: 7.22.6
+ resolution: "@babel/plugin-transform-optional-chaining@npm:7.22.6"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5
"@babel/plugin-syntax-optional-chaining": ^7.8.3
- "@babel/plugin-syntax-private-property-in-object": ^7.14.5
- "@babel/plugin-syntax-top-level-await": ^7.14.5
- "@babel/plugin-transform-arrow-functions": ^7.21.5
- "@babel/plugin-transform-async-to-generator": ^7.20.7
- "@babel/plugin-transform-block-scoped-functions": ^7.18.6
- "@babel/plugin-transform-block-scoping": ^7.21.0
- "@babel/plugin-transform-classes": ^7.21.0
- "@babel/plugin-transform-computed-properties": ^7.21.5
- "@babel/plugin-transform-destructuring": ^7.21.3
- "@babel/plugin-transform-dotall-regex": ^7.18.6
- "@babel/plugin-transform-duplicate-keys": ^7.18.9
- "@babel/plugin-transform-exponentiation-operator": ^7.18.6
- "@babel/plugin-transform-for-of": ^7.21.5
- "@babel/plugin-transform-function-name": ^7.18.9
- "@babel/plugin-transform-literals": ^7.18.9
- "@babel/plugin-transform-member-expression-literals": ^7.18.6
- "@babel/plugin-transform-modules-amd": ^7.20.11
- "@babel/plugin-transform-modules-commonjs": ^7.21.5
- "@babel/plugin-transform-modules-systemjs": ^7.20.11
- "@babel/plugin-transform-modules-umd": ^7.18.6
- "@babel/plugin-transform-named-capturing-groups-regex": ^7.20.5
- "@babel/plugin-transform-new-target": ^7.18.6
- "@babel/plugin-transform-object-super": ^7.18.6
- "@babel/plugin-transform-parameters": ^7.21.3
- "@babel/plugin-transform-property-literals": ^7.18.6
- "@babel/plugin-transform-regenerator": ^7.21.5
- "@babel/plugin-transform-reserved-words": ^7.18.6
- "@babel/plugin-transform-shorthand-properties": ^7.18.6
- "@babel/plugin-transform-spread": ^7.20.7
- "@babel/plugin-transform-sticky-regex": ^7.18.6
- "@babel/plugin-transform-template-literals": ^7.18.9
- "@babel/plugin-transform-typeof-symbol": ^7.18.9
- "@babel/plugin-transform-unicode-escapes": ^7.21.5
- "@babel/plugin-transform-unicode-regex": ^7.18.6
- "@babel/preset-modules": ^0.1.5
- "@babel/types": ^7.21.5
- babel-plugin-polyfill-corejs2: ^0.3.3
- babel-plugin-polyfill-corejs3: ^0.6.0
- babel-plugin-polyfill-regenerator: ^0.4.1
- core-js-compat: ^3.25.1
- semver: ^6.3.0
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 86e167f3a351c89f8cd1409262481ece6ddc085b76147e801530ce29d60b1cfda8b264b1efd1ae27b8181b073a923c7161f21e2ebc0a41d652d717b10cf1c829
+ checksum: 9713f7920ed04090c149fc5ec024dd1638e8b97aa4ae3753b93072d84103b8de380afb96d6cf03e53b285420db4f705f3ac13149c6fd54f322b61dc19e33c54f
languageName: node
linkType: hard
-"@babel/preset-modules@npm:^0.1.5":
- version: 0.1.5
- resolution: "@babel/preset-modules@npm:0.1.5"
+"@babel/plugin-transform-parameters@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-parameters@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.0.0
- "@babel/plugin-proposal-unicode-property-regex": ^7.4.4
- "@babel/plugin-transform-dotall-regex": ^7.4.4
- "@babel/types": ^7.4.4
- esutils: ^2.0.2
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 8430e0e9e9d520b53e22e8c4c6a5a080a12b63af6eabe559c2310b187bd62ae113f3da82ba33e9d1d0f3230930ca702843aae9dd226dec51f7d7114dc1f51c10
+ checksum: b44f89cf97daf23903776ba27c2ab13b439d80d8c8a95be5c476ab65023b1e0c0e94c28d3745f3b60a58edc4e590fa0cd4287a0293e51401ca7d29a2ddb13b8e
languageName: node
linkType: hard
-"@babel/preset-typescript@npm:^7.21.5":
- version: 7.21.5
- resolution: "@babel/preset-typescript@npm:7.21.5"
+"@babel/plugin-transform-private-methods@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-private-methods@npm:7.22.5"
dependencies:
- "@babel/helper-plugin-utils": ^7.21.5
- "@babel/helper-validator-option": ^7.21.0
- "@babel/plugin-syntax-jsx": ^7.21.4
- "@babel/plugin-transform-modules-commonjs": ^7.21.5
- "@babel/plugin-transform-typescript": ^7.21.3
+ "@babel/helper-create-class-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: e7b35c435139eec1d6bd9f57e8f3eb79bfc2da2c57a34ad9e9ea848ba4ecd72791cf4102df456604ab07c7f4518525b0764754b6dd5898036608b351e0792448
+ checksum: 321479b4fcb6d3b3ef622ab22fd24001e43d46e680e8e41324c033d5810c84646e470f81b44cbcbef5c22e99030784f7cac92f1829974da7a47a60a7139082c3
languageName: node
linkType: hard
-"@babel/regjsgen@npm:^0.8.0":
- version: 0.8.0
- resolution: "@babel/regjsgen@npm:0.8.0"
- checksum: 89c338fee774770e5a487382170711014d49a68eb281e74f2b5eac88f38300a4ad545516a7786a8dd5702e9cf009c94c2f582d200f077ac5decd74c56b973730
+"@babel/plugin-transform-private-property-in-object@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.5"
+ dependencies:
+ "@babel/helper-annotate-as-pure": ^7.22.5
+ "@babel/helper-create-class-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-private-property-in-object": ^7.14.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 9ac019fb2772f3af6278a7f4b8b14b0663accb3fd123d87142ceb2fbc57fd1afa07c945d1329029b026b9ee122096ef71a3f34f257a9e04cf4245b87298c38b4
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.8.4":
- version: 7.17.2
- resolution: "@babel/runtime@npm:7.17.2"
+"@babel/plugin-transform-property-literals@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-property-literals@npm:7.22.5"
dependencies:
- regenerator-runtime: ^0.13.4
- checksum: a48702d271ecc59c09c397856407afa29ff980ab537b3da58eeee1aeaa0f545402d340a1680c9af58aec94dfdcbccfb6abb211991b74686a86d03d3f6956cacd
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 796176a3176106f77fcb8cd04eb34a8475ce82d6d03a88db089531b8f0453a2fb8b0c6ec9a52c27948bc0ea478becec449893741fc546dfc3930ab927e3f9f2e
languageName: node
linkType: hard
-"@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"
+"@babel/plugin-transform-regenerator@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-regenerator@npm:7.22.5"
dependencies:
- "@babel/code-frame": ^7.21.4
- "@babel/parser": ^7.21.9
- "@babel/types": ^7.21.5
- checksum: 6ec2c60d4d53b2a9230ab82c399ba6525df87e9a4e01e4b111e071cbad283b1362e7c99a1bc50027073f44f2de36a495a89c27112c4e7efe7ef9c8d9c84de2ec
+ "@babel/helper-plugin-utils": ^7.22.5
+ regenerator-transform: ^0.15.1
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: f7c5ca5151321963df777cc02725d10d1ccc3b3b8323da0423aecd9ac6144cbdd2274af5281a5580db2fc2f8b234e318517b5d76b85669118906533a559f2b6a
languageName: node
linkType: hard
-"@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"
+"@babel/plugin-transform-reserved-words@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-reserved-words@npm:7.22.5"
dependencies:
- "@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: 9560ae22092d5a7c52849145dd3e5aed2ffb73d61255e70e19e3fbd06bcbafbbdecea28df40a42ee3b60b01e85a42224ec841df93e867547e329091cc2f2bb6f
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 3ffd7dbc425fe8132bfec118b9817572799cab1473113a635d25ab606c1f5a2341a636c04cf6b22df3813320365ed5a965b5eeb3192320a10e4cc2c137bd8bfc
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.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"
+"@babel/plugin-transform-shorthand-properties@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-shorthand-properties@npm:7.22.5"
dependencies:
- "@babel/helper-string-parser": ^7.21.5
- "@babel/helper-validator-identifier": ^7.19.1
- to-fast-properties: ^2.0.0
- checksum: ffe36bb4f4a99ad13c426a98c3b508d70736036cae4e471d9c862e3a579847ed4f480686af0fce2633f6f7c0f0d3bf02da73da36e7edd3fde0b2061951dcba9a
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: a5ac902c56ea8effa99f681340ee61bac21094588f7aef0bc01dff98246651702e677552fa6d10e548c4ac22a3ffad047dd2f8c8f0540b68316c2c203e56818b
languageName: node
linkType: hard
-"@bcoe/v8-coverage@npm:^0.2.3":
- version: 0.2.3
- resolution: "@bcoe/v8-coverage@npm:0.2.3"
- checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27
+"@babel/plugin-transform-spread@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-spread@npm:7.22.5"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 5587f0deb60b3dfc9b274e269031cc45ec75facccf1933ea2ea71ced9fd3ce98ed91bb36d6cd26817c14474b90ed998c5078415f0eab531caf301496ce24c95c
languageName: node
linkType: hard
-"@colors/colors@npm:1.5.0":
- version: 1.5.0
- resolution: "@colors/colors@npm:1.5.0"
- checksum: d64d5260bed1d5012ae3fc617d38d1afc0329fec05342f4e6b838f46998855ba56e0a73833f4a80fa8378c84810da254f76a8a19c39d038260dc06dc4e007425
+"@babel/plugin-transform-sticky-regex@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-sticky-regex@npm:7.22.5"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 63b2c575e3e7f96c32d52ed45ee098fb7d354b35c2223b8c8e76840b32cc529ee0c0ceb5742fd082e56e91e3d82842a367ce177e82b05039af3d602c9627a729
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"
+"@babel/plugin-transform-template-literals@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-template-literals@npm:7.22.5"
dependencies:
- "@jridgewell/trace-mapping": 0.3.9
- checksum: 5718f267085ed8edb3e7ef210137241775e607ee18b77d95aa5bd7514f47f5019aa2d82d96b3bf342ef7aa890a346fa1044532ff7cc3009e7d24fce3ce6200fa
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 27e9bb030654cb425381c69754be4abe6a7c75b45cd7f962cd8d604b841b2f0fb7b024f2efc1c25cc53f5b16d79d5e8cfc47cacbdaa983895b3aeefa3e7e24ff
languageName: node
linkType: hard
-"@cypress/request@npm:^2.88.10":
- version: 2.88.11
- resolution: "@cypress/request@npm:2.88.11"
+"@babel/plugin-transform-typeof-symbol@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-typeof-symbol@npm:7.22.5"
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
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 82a53a63ffc3010b689ca9a54e5f53b2718b9f4b4a9818f36f9b7dba234f38a01876680553d2716a645a61920b5e6e4aaf8d4a0064add379b27ca0b403049512
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-typescript@npm:^7.22.5":
+ version: 7.22.9
+ resolution: "@babel/plugin-transform-typescript@npm:7.22.9"
+ dependencies:
+ "@babel/helper-annotate-as-pure": ^7.22.5
+ "@babel/helper-create-class-features-plugin": ^7.22.9
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/plugin-syntax-typescript": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 6d1317a54d093b302599a4bee8ba9865d0de8b7b6ac1a0746c4316231d632f75b7f086e6e78acb9ac95ba12ba3b9da462dc9ca69370abb4603c4cc987f62e67e
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-unicode-escapes@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-unicode-escapes@npm:7.22.5"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: da5e85ab3bb33a75cbf6181bfd236b208dc934702fd304db127232f17b4e0f42c6d3f238de8589470b4190906967eea8ca27adf3ae9d8ee4de2a2eae906ed186
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.22.5"
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 2495e5f663cb388e3d888b4ba3df419ac436a5012144ac170b622ddfc221f9ea9bdba839fa2bc0185cb776b578030666406452ec7791cbf0e7a3d4c88ae9574c
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-unicode-regex@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-unicode-regex@npm:7.22.5"
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 6b5d1404c8c623b0ec9bd436c00d885a17d6a34f3f2597996343ddb9d94f6379705b21582dfd4cec2c47fd34068872e74ab6b9580116c0566b3f9447e2a7fa06
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.22.5"
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin": ^7.22.5
+ "@babel/helper-plugin-utils": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: c042070f980b139547f8b0179efbc049ac5930abec7fc26ed7a41d89a048d8ab17d362200e204b6f71c3c20d6991a0e74415e1a412a49adc8131c2a40c04822e
+ languageName: node
+ linkType: hard
+
+"@babel/preset-env@npm:^7.22.9":
+ version: 7.22.9
+ resolution: "@babel/preset-env@npm:7.22.9"
+ dependencies:
+ "@babel/compat-data": ^7.22.9
+ "@babel/helper-compilation-targets": ^7.22.9
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-validator-option": ^7.22.5
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.22.5
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.22.5
+ "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2
+ "@babel/plugin-syntax-async-generators": ^7.8.4
+ "@babel/plugin-syntax-class-properties": ^7.12.13
+ "@babel/plugin-syntax-class-static-block": ^7.14.5
+ "@babel/plugin-syntax-dynamic-import": ^7.8.3
+ "@babel/plugin-syntax-export-namespace-from": ^7.8.3
+ "@babel/plugin-syntax-import-assertions": ^7.22.5
+ "@babel/plugin-syntax-import-attributes": ^7.22.5
+ "@babel/plugin-syntax-import-meta": ^7.10.4
+ "@babel/plugin-syntax-json-strings": ^7.8.3
+ "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4
+ "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3
+ "@babel/plugin-syntax-numeric-separator": ^7.10.4
+ "@babel/plugin-syntax-object-rest-spread": ^7.8.3
+ "@babel/plugin-syntax-optional-catch-binding": ^7.8.3
+ "@babel/plugin-syntax-optional-chaining": ^7.8.3
+ "@babel/plugin-syntax-private-property-in-object": ^7.14.5
+ "@babel/plugin-syntax-top-level-await": ^7.14.5
+ "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6
+ "@babel/plugin-transform-arrow-functions": ^7.22.5
+ "@babel/plugin-transform-async-generator-functions": ^7.22.7
+ "@babel/plugin-transform-async-to-generator": ^7.22.5
+ "@babel/plugin-transform-block-scoped-functions": ^7.22.5
+ "@babel/plugin-transform-block-scoping": ^7.22.5
+ "@babel/plugin-transform-class-properties": ^7.22.5
+ "@babel/plugin-transform-class-static-block": ^7.22.5
+ "@babel/plugin-transform-classes": ^7.22.6
+ "@babel/plugin-transform-computed-properties": ^7.22.5
+ "@babel/plugin-transform-destructuring": ^7.22.5
+ "@babel/plugin-transform-dotall-regex": ^7.22.5
+ "@babel/plugin-transform-duplicate-keys": ^7.22.5
+ "@babel/plugin-transform-dynamic-import": ^7.22.5
+ "@babel/plugin-transform-exponentiation-operator": ^7.22.5
+ "@babel/plugin-transform-export-namespace-from": ^7.22.5
+ "@babel/plugin-transform-for-of": ^7.22.5
+ "@babel/plugin-transform-function-name": ^7.22.5
+ "@babel/plugin-transform-json-strings": ^7.22.5
+ "@babel/plugin-transform-literals": ^7.22.5
+ "@babel/plugin-transform-logical-assignment-operators": ^7.22.5
+ "@babel/plugin-transform-member-expression-literals": ^7.22.5
+ "@babel/plugin-transform-modules-amd": ^7.22.5
+ "@babel/plugin-transform-modules-commonjs": ^7.22.5
+ "@babel/plugin-transform-modules-systemjs": ^7.22.5
+ "@babel/plugin-transform-modules-umd": ^7.22.5
+ "@babel/plugin-transform-named-capturing-groups-regex": ^7.22.5
+ "@babel/plugin-transform-new-target": ^7.22.5
+ "@babel/plugin-transform-nullish-coalescing-operator": ^7.22.5
+ "@babel/plugin-transform-numeric-separator": ^7.22.5
+ "@babel/plugin-transform-object-rest-spread": ^7.22.5
+ "@babel/plugin-transform-object-super": ^7.22.5
+ "@babel/plugin-transform-optional-catch-binding": ^7.22.5
+ "@babel/plugin-transform-optional-chaining": ^7.22.6
+ "@babel/plugin-transform-parameters": ^7.22.5
+ "@babel/plugin-transform-private-methods": ^7.22.5
+ "@babel/plugin-transform-private-property-in-object": ^7.22.5
+ "@babel/plugin-transform-property-literals": ^7.22.5
+ "@babel/plugin-transform-regenerator": ^7.22.5
+ "@babel/plugin-transform-reserved-words": ^7.22.5
+ "@babel/plugin-transform-shorthand-properties": ^7.22.5
+ "@babel/plugin-transform-spread": ^7.22.5
+ "@babel/plugin-transform-sticky-regex": ^7.22.5
+ "@babel/plugin-transform-template-literals": ^7.22.5
+ "@babel/plugin-transform-typeof-symbol": ^7.22.5
+ "@babel/plugin-transform-unicode-escapes": ^7.22.5
+ "@babel/plugin-transform-unicode-property-regex": ^7.22.5
+ "@babel/plugin-transform-unicode-regex": ^7.22.5
+ "@babel/plugin-transform-unicode-sets-regex": ^7.22.5
+ "@babel/preset-modules": ^0.1.5
+ "@babel/types": ^7.22.5
+ babel-plugin-polyfill-corejs2: ^0.4.4
+ babel-plugin-polyfill-corejs3: ^0.8.2
+ babel-plugin-polyfill-regenerator: ^0.5.1
+ core-js-compat: ^3.31.0
+ semver: ^6.3.1
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 6caa2897bbda30c6932aed0a03827deb1337c57108050c9f97dc9a857e1533c7125b168b6d70b9d191965bf05f9f233f0ad20303080505dff7ce39740aaa759d
+ languageName: node
+ linkType: hard
+
+"@babel/preset-modules@npm:^0.1.5":
+ version: 0.1.6
+ resolution: "@babel/preset-modules@npm:0.1.6"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.0.0
+ "@babel/plugin-proposal-unicode-property-regex": ^7.4.4
+ "@babel/plugin-transform-dotall-regex": ^7.4.4
+ "@babel/types": ^7.4.4
+ esutils: ^2.0.2
+ peerDependencies:
+ "@babel/core": ^7.0.0-0 || ^8.0.0-0 <8.0.0
+ checksum: 9700992d2b9526e703ab49eb8c4cd0b26bec93594d57c6b808967619df1a387565e0e58829b65b5bd6d41049071ea0152c9195b39599515fddb3e52b09a55ff0
+ languageName: node
+ linkType: hard
+
+"@babel/preset-typescript@npm:^7.22.5":
+ version: 7.22.5
+ resolution: "@babel/preset-typescript@npm:7.22.5"
+ dependencies:
+ "@babel/helper-plugin-utils": ^7.22.5
+ "@babel/helper-validator-option": ^7.22.5
+ "@babel/plugin-syntax-jsx": ^7.22.5
+ "@babel/plugin-transform-modules-commonjs": ^7.22.5
+ "@babel/plugin-transform-typescript": ^7.22.5
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 7be1670cb4404797d3a473bd72d66eb2b3e0f2f8a672a5e40bdb0812cc66085ec84bcd7b896709764cabf042fdc6b7f2d4755ac7cce10515eb596ff61dab5154
+ languageName: node
+ linkType: hard
+
+"@babel/regjsgen@npm:^0.8.0":
+ version: 0.8.0
+ resolution: "@babel/regjsgen@npm:0.8.0"
+ checksum: 89c338fee774770e5a487382170711014d49a68eb281e74f2b5eac88f38300a4ad545516a7786a8dd5702e9cf009c94c2f582d200f077ac5decd74c56b973730
+ languageName: node
+ linkType: hard
+
+"@babel/runtime@npm:^7.8.4":
+ version: 7.22.6
+ resolution: "@babel/runtime@npm:7.22.6"
+ dependencies:
+ regenerator-runtime: ^0.13.11
+ checksum: e585338287c4514a713babf4fdb8fc2a67adcebab3e7723a739fc62c79cfda875b314c90fd25f827afb150d781af97bc16c85bfdbfa2889f06053879a1ddb597
+ languageName: node
+ linkType: hard
+
+"@babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3":
+ version: 7.22.5
+ resolution: "@babel/template@npm:7.22.5"
+ dependencies:
+ "@babel/code-frame": ^7.22.5
+ "@babel/parser": ^7.22.5
+ "@babel/types": ^7.22.5
+ checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3
+ languageName: node
+ linkType: hard
+
+"@babel/traverse@npm:^7.22.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.7.2":
+ version: 7.22.8
+ resolution: "@babel/traverse@npm:7.22.8"
+ dependencies:
+ "@babel/code-frame": ^7.22.5
+ "@babel/generator": ^7.22.7
+ "@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.6
+ "@babel/parser": ^7.22.7
+ "@babel/types": ^7.22.5
+ debug: ^4.1.0
+ globals: ^11.1.0
+ checksum: a381369bc3eedfd13ed5fef7b884657f1c29024ea7388198149f0edc34bd69ce3966e9f40188d15f56490a5e12ba250ccc485f2882b53d41b054fccefb233e33
+ languageName: node
+ linkType: hard
+
+"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @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"
+ dependencies:
+ "@babel/helper-string-parser": ^7.22.5
+ "@babel/helper-validator-identifier": ^7.22.5
+ to-fast-properties: ^2.0.0
+ checksum: c13a9c1dc7d2d1a241a2f8363540cb9af1d66e978e8984b400a20c4f38ba38ca29f06e26a0f2d49a70bad9e57615dac09c35accfddf1bb90d23cd3e0a0bab892
+ languageName: node
+ linkType: hard
+
+"@bcoe/v8-coverage@npm:^0.2.3":
+ version: 0.2.3
+ resolution: "@bcoe/v8-coverage@npm:0.2.3"
+ checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27
+ 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"
+ dependencies:
+ "@jridgewell/trace-mapping": 0.3.9
+ checksum: 5718f267085ed8edb3e7ef210137241775e607ee18b77d95aa5bd7514f47f5019aa2d82d96b3bf342ef7aa890a346fa1044532ff7cc3009e7d24fce3ce6200fa
+ languageName: node
+ linkType: hard
+
+"@cypress/request@npm:^2.88.10":
+ version: 2.88.12
+ resolution: "@cypress/request@npm:2.88.12"
+ 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
@@ -1371,10 +1426,10 @@ __metadata:
performance-now: ^2.1.0
qs: ~6.10.3
safe-buffer: ^5.1.2
- tough-cookie: ~2.5.0
+ tough-cookie: ^4.1.3
tunnel-agent: ^0.6.0
uuid: ^8.3.2
- checksum: e4b3f62e0c41c4ccca6c942828461d8ea717e752fd918d685e9f74e2ebcfa8b7942427f7ce971e502635c3bf3d40011476db84dc753d3dc360c6d08350da6f93
+ checksum: 2c6fbf7f3127d41bffca8374beaa8cf95450495a8a077b00309ea9d94dd2a4da450a77fe038e8ad26c97cdd7c39b65c53c850f8338ce9bc2dbe23ce2e2b48329
languageName: node
linkType: hard
@@ -1550,6 +1605,20 @@ __metadata:
languageName: node
linkType: hard
+"@isaacs/cliui@npm:^8.0.2":
+ version: 8.0.2
+ resolution: "@isaacs/cliui@npm:8.0.2"
+ dependencies:
+ string-width: ^5.1.2
+ string-width-cjs: "npm:string-width@^4.2.0"
+ strip-ansi: ^7.0.1
+ strip-ansi-cjs: "npm:strip-ansi@^6.0.1"
+ wrap-ansi: ^8.1.0
+ wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0"
+ checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb
+ languageName: node
+ linkType: hard
+
"@istanbuljs/load-nyc-config@npm:^1.0.0":
version: 1.1.0
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
@@ -1637,7 +1706,7 @@ __metadata:
languageName: node
linkType: hard
-"@jest/expect-utils@npm:^29.5.0":
+"@jest/expect-utils@npm:25.1.0 - 29, @jest/expect-utils@npm:^29.5.0":
version: 29.5.0
resolution: "@jest/expect-utils@npm:29.5.0"
dependencies:
@@ -1912,173 +1981,675 @@ __metadata:
languageName: node
linkType: hard
-"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
- version: 2.0.5
- resolution: "@nodelib/fs.stat@npm:2.0.5"
- checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0
+"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
+ version: 2.0.5
+ resolution: "@nodelib/fs.stat@npm:2.0.5"
+ checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0
+ languageName: node
+ linkType: hard
+
+"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8":
+ version: 1.2.8
+ resolution: "@nodelib/fs.walk@npm:1.2.8"
+ dependencies:
+ "@nodelib/fs.scandir": 2.1.5
+ fastq: ^1.6.0
+ checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53
+ languageName: node
+ linkType: hard
+
+"@npmcli/fs@npm:^1.0.0":
+ version: 1.1.1
+ resolution: "@npmcli/fs@npm:1.1.1"
+ dependencies:
+ "@gar/promisify": ^1.0.1
+ semver: ^7.3.5
+ checksum: f5ad92f157ed222e4e31c352333d0901df02c7c04311e42a81d8eb555d4ec4276ea9c635011757de20cc476755af33e91622838de573b17e52e2e7703f0a9965
+ languageName: node
+ linkType: hard
+
+"@npmcli/move-file@npm:^1.0.1":
+ version: 1.1.2
+ resolution: "@npmcli/move-file@npm:1.1.2"
+ dependencies:
+ mkdirp: ^1.0.4
+ rimraf: ^3.0.2
+ checksum: c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7
+ languageName: node
+ linkType: hard
+
+"@pkgjs/parseargs@npm:^0.11.0":
+ version: 0.11.0
+ resolution: "@pkgjs/parseargs@npm:0.11.0"
+ checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f
+ languageName: node
+ linkType: hard
+
+"@pkgr/utils@npm:^2.3.1":
+ version: 2.3.1
+ resolution: "@pkgr/utils@npm:2.3.1"
+ dependencies:
+ cross-spawn: ^7.0.3
+ is-glob: ^4.0.3
+ open: ^8.4.0
+ picocolors: ^1.0.0
+ tiny-glob: ^0.2.9
+ tslib: ^2.4.0
+ checksum: 118a1971120253740121a1db0a6658c21195b7da962acf9c124b507a3df707cfc97b0b84a16edcbd4352853b182e8337da9fc6e8e3d06c60d75ae4fb42321c75
+ languageName: node
+ linkType: hard
+
+"@rollup/plugin-commonjs@npm:^24.1.0":
+ version: 24.1.0
+ resolution: "@rollup/plugin-commonjs@npm:24.1.0"
+ dependencies:
+ "@rollup/pluginutils": ^5.0.1
+ commondir: ^1.0.1
+ estree-walker: ^2.0.2
+ glob: ^8.0.3
+ is-reference: 1.2.1
+ magic-string: ^0.27.0
+ peerDependencies:
+ rollup: ^2.68.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 42faafc9bc8e04d75c86bb50d693ebb9c5eee19bf9ab3c09780b872547d12ff5ea85cfec7da75f5176d0aa4b5233101f667f44b85b331450a7bb14c95180852e
+ languageName: node
+ linkType: hard
+
+"@rollup/plugin-json@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "@rollup/plugin-json@npm:6.0.0"
+ dependencies:
+ "@rollup/pluginutils": ^5.0.1
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 77cfc941edaf77a5307977704ffaba706d83bea66f265b2b68f14be2a0af6d08b0fb1b04fdd773146c84cc70938ff64b00ae946808fd6ac057058af824d78128
+ languageName: node
+ linkType: hard
+
+"@rollup/plugin-node-resolve@npm:^15.0.2":
+ version: 15.0.2
+ resolution: "@rollup/plugin-node-resolve@npm:15.0.2"
+ dependencies:
+ "@rollup/pluginutils": ^5.0.1
+ "@types/resolve": 1.20.2
+ deepmerge: ^4.2.2
+ is-builtin-module: ^3.2.1
+ is-module: ^1.0.0
+ resolve: ^1.22.1
+ peerDependencies:
+ rollup: ^2.78.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 328eafee06ff967a36441b55e77fbd0d4f599d256e5d1977800ee71915846c46bc1b6185df35c7b512ad2b4023b05b65a332be77b8b00b9d8a20f87d056b8166
+ languageName: node
+ linkType: hard
+
+"@rollup/plugin-typescript@npm:^11.1.1":
+ version: 11.1.1
+ resolution: "@rollup/plugin-typescript@npm:11.1.1"
+ dependencies:
+ "@rollup/pluginutils": ^5.0.1
+ resolve: ^1.22.1
+ peerDependencies:
+ rollup: ^2.14.0||^3.0.0
+ tslib: "*"
+ typescript: ">=3.7.0"
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ tslib:
+ optional: true
+ checksum: 0e82ef17ded026060bbc237ed2552e227d5b514cd58c1f624afeb65704a24048e38918e6242bcf29960612dc9925fbf1bb0625475ec691330cd144e4a84e3065
+ languageName: node
+ linkType: hard
+
+"@rollup/pluginutils@npm:^5.0.1":
+ version: 5.0.2
+ resolution: "@rollup/pluginutils@npm:5.0.2"
+ dependencies:
+ "@types/estree": ^1.0.0
+ estree-walker: ^2.0.2
+ picomatch: ^2.3.1
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: edea15e543bebc7dcac3b0ac8bc7b8e8e6dbd46e2864dbe5dd28072de1fbd5b0e10d545a610c0edaa178e8a7ac432e2a2a52e547ece1308471412caba47db8ce
+ languageName: node
+ linkType: hard
+
+"@sinclair/typebox@npm:^0.25.16":
+ version: 0.25.24
+ resolution: "@sinclair/typebox@npm:0.25.24"
+ checksum: 10219c58f40b8414c50b483b0550445e9710d4fe7b2c4dccb9b66533dd90ba8e024acc776026cebe81e87f06fa24b07fdd7bc30dd277eb9cc386ec50151a3026
+ languageName: node
+ linkType: hard
+
+"@sinonjs/commons@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "@sinonjs/commons@npm:3.0.0"
+ dependencies:
+ type-detect: 4.0.8
+ checksum: b4b5b73d4df4560fb8c0c7b38c7ad4aeabedd362f3373859d804c988c725889cde33550e4bcc7cd316a30f5152a2d1d43db71b6d0c38f5feef71fd8d016763f8
+ languageName: node
+ linkType: hard
+
+"@sinonjs/fake-timers@npm:^10.0.2":
+ version: 10.2.0
+ resolution: "@sinonjs/fake-timers@npm:10.2.0"
+ dependencies:
+ "@sinonjs/commons": ^3.0.0
+ checksum: 586c76e1dd90d03b0c4e754f2011325b38ac6055878c81c52434c900f36d9d245438c96ef69e08e28d9fbecf2335fb347b67850962d8b6e539dd7359d8c62802
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-has-node-buffer-support@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-has-node-buffer-support@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-is-buffer": ^0.0.x
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ bin:
+ has-node-buffer-support: bin/cli
+ checksum: 30fa31c3c675ce1e2b235327a5c074f7bc0a50d57eaf63d985b021073164ae1ef4be1bed842d5b56773218b0088f4fa1aa09fd488a450956a7dd088825b02864
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-has-own-property@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/assert-has-own-property@npm:0.0.7"
+ checksum: 4f1efbd2352214898792fea5bf83d190f9899a28d2eab52bca95de19370c112a385961390c423863be9977851f22003aee2268c1806138ad20d90bfa8d87eb95
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-has-symbol-support@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-has-symbol-support@npm:0.0.8"
+ dependencies:
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ bin:
+ has-symbol-support: bin/cli
+ checksum: 122d53efd4f8489a975486f1c79ec302102158f7bc92b3b13f936c8eae01189c2bf93483adb3942a310357425f9a4251959793e9a77ef310b2944af44396c8fc
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-has-tostringtag-support@npm:^0.0.x":
+ version: 0.0.9
+ resolution: "@stdlib/assert-has-tostringtag-support@npm:0.0.9"
+ dependencies:
+ "@stdlib/assert-has-symbol-support": ^0.0.x
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ bin:
+ has-tostringtag-support: bin/cli
+ checksum: e2d10af88f36ba0ae9e0a205761eb6c6497b4dba037fdd1302c345c419c3ff33dcc9e3162b83a98816a079ac26415e28bdfc160ae2822941d7bec41d516be2e4
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-array@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/assert-is-array@npm:0.0.7"
+ dependencies:
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: 17d64b5982fa6f1feb093e8c8a631adc780035070168b92ceafb8cd5162ad3f9a81952a4938dd6515a8caee20f24aca9112e4f219eb2f03789a8030aa434ad07
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-boolean@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-is-boolean@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-has-tostringtag-support": ^0.0.x
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: 501a78fa4a60816fe24de8870d2bccf1cdc2f6a1d2d4ae2b4d10ba6b55f667fc4a658504dc12fe89c697aa6f81020f14c9c8cb5512b0e24d33f88c705028b7b7
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-buffer@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-is-buffer@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-is-object-like": ^0.0.x
+ checksum: 2548c1d0a4c0240bef5677fb422c419b14dbc78a6499404eecd85afc332994660284c67adbf9f2de7ae429bed0b8683491528ba75e4baa3ec374748a569c1eaa
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-function@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-is-function@npm:0.0.8"
+ dependencies:
+ "@stdlib/utils-type-of": ^0.0.x
+ checksum: 57017ac110fa14ea61efd48b8b61c35141a5f6438312a846d38b30b7e22d8048e38c267bccf91f3c0e6d47f893e5ba75f93816d1dc529595955dfb2a22ddfd6e
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-object-like@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-is-object-like@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-tools-array-function": ^0.0.x
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ checksum: 45b1dbf8dc43abac2890afc21d64773bc2cc932696d7372ca5366402b7351314921293b14722454ab1a7a324449dfb924e519d82df188b37f9bfc8cc54ba863b
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-object@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-is-object@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-is-array": ^0.0.x
+ checksum: e50b56c32a6693f73f7d65a29726e76b4df8ebae601fd3508b3ef512a1988ff2ce9d436607555888979c715c13600a1a0ebb205fdb0235a30ca3de5580f164ae
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-plain-object@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/assert-is-plain-object@npm:0.0.7"
+ dependencies:
+ "@stdlib/assert-has-own-property": ^0.0.x
+ "@stdlib/assert-is-function": ^0.0.x
+ "@stdlib/assert-is-object": ^0.0.x
+ "@stdlib/utils-get-prototype-of": ^0.0.x
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: 3ec502a4954ef959dc478d4674f1fd4b0929bb5d420be0097db6a8eb899a1e65c23bdbdfb73e1b750f058058ac0634b1fc26b4275eb8ccffa196967a3da427d7
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-regexp-string@npm:^0.0.x":
+ version: 0.0.9
+ resolution: "@stdlib/assert-is-regexp-string@npm:0.0.9"
+ dependencies:
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ "@stdlib/process-read-stdin": ^0.0.x
+ "@stdlib/regexp-eol": ^0.0.x
+ "@stdlib/regexp-regexp": ^0.0.x
+ "@stdlib/streams-node-stdin": ^0.0.x
+ bin:
+ is-regexp-string: bin/cli
+ checksum: 10155625e04f3d79466993e080b2cf01742bf88a1adb9d383d1854b3a7464792ed8d59e810c7a35b5a8fbbfb63eb0317a540fd687cec2afdf59052b79a734911
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-regexp@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/assert-is-regexp@npm:0.0.7"
+ dependencies:
+ "@stdlib/assert-has-tostringtag-support": ^0.0.x
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: 6d881f57b1e295b0abf1f7698404cfa9aa8f0851099b5d1cb786b809742df266f17e4287af8932351a303825d475a6d6e22751a4f521b6e6504acab6d9aabd33
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-is-string@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/assert-is-string@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-has-tostringtag-support": ^0.0.x
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: c1bde204d24c2debf348202b432fa2376f0d53d8949a8a2f1cebc298e417495242267732bdb87df693ba46de3f2777895ed09d5271ba8ca83e107a7412e23c84
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/assert-tools-array-function@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/assert-tools-array-function@npm:0.0.7"
+ dependencies:
+ "@stdlib/assert-is-array": ^0.0.x
+ checksum: 2092c89eae72b5da2192fedecc1cddfd7ac553298425039d895cc858ee981df67f13560db60387b166354761cd74017bc5cc959e555d317c07358153bd9d79ef
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/buffer-ctor@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/buffer-ctor@npm:0.0.7"
+ dependencies:
+ "@stdlib/assert-has-node-buffer-support": ^0.0.x
+ checksum: 211b5eb199a6037381339bd0f7759f0dbeb912e8312d6ccb2a7c58fc77a4d311998efaecc1fce0e689e0483c213e9a24cfe3441e0130ca099f11abf5fb25cde7
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/buffer-from-string@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/buffer-from-string@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-is-function": ^0.0.x
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/buffer-ctor": ^0.0.x
+ "@stdlib/string-format": ^0.0.x
+ checksum: f436d106e816339f6ae8e438b5d36e8e522c50958663370bc3c028061aeae225026549f6de2fd50c9d34c08f1c581130dd8702b22a7002c5bdd8bd137d9ad863
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/cli-ctor@npm:^0.0.x":
+ version: 0.0.3
+ resolution: "@stdlib/cli-ctor@npm:0.0.3"
+ dependencies:
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ "@stdlib/utils-noop": ^0.0.x
+ minimist: ^1.2.0
+ checksum: c3b6e8b9d149b96a1e97df91e77fbc5245a92b504c277d993d2c08ff263ce46fc83dd211ee71e0e0641f700d7622b88265ce9a73b7628176f16626e8abe0e213
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/fs-read-file@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/fs-read-file@npm:0.0.8"
+ dependencies:
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ bin:
+ read-file: bin/cli
+ checksum: 10a1ead656b8a5d7f6f4975eb0d65c385f827e106452fb055caf9a471b672da33b6f282ea5a91cfb294a7d94152ab7133529ddaabdbfa63706253ba88c50bd0d
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/process-read-stdin@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/process-read-stdin@npm:0.0.7"
+ dependencies:
+ "@stdlib/assert-is-function": ^0.0.x
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/buffer-ctor": ^0.0.x
+ "@stdlib/buffer-from-string": ^0.0.x
+ "@stdlib/streams-node-stdin": ^0.0.x
+ "@stdlib/utils-next-tick": ^0.0.x
+ checksum: 5dd79b3e9939906af94025845f52f5107f9b371a3eabc131a3b456219b37f5df7f95ba04d526e37c0c8128d263f21d7371d98e77afc80fc06e926836e7150bf2
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/regexp-eol@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/regexp-eol@npm:0.0.7"
+ dependencies:
+ "@stdlib/assert-has-own-property": ^0.0.x
+ "@stdlib/assert-is-boolean": ^0.0.x
+ "@stdlib/assert-is-plain-object": ^0.0.x
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ checksum: c4cbe08a20192681395958ebcb4fdb3adaf243d229d8d681308903fb4587a990f3e8cea09ae101dd36e9d0fdff0e1206418001c99863c72a8d6e0670dd57ce65
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/regexp-extended-length-path@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/regexp-extended-length-path@npm:0.0.7"
+ dependencies:
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ checksum: 21a9595c94a1ce39a2b41c13346f50300ebc1a6822708336ac2433bf3dabfc312be863bb3e94e6e5aa42793df457d4aab429d6299351cddb369cac9238fbcc6f
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/regexp-function-name@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/regexp-function-name@npm:0.0.7"
+ dependencies:
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ checksum: 47ac8ca5b7cec2b0716de4919ac9c9c3b05af22aaa0955d458e58d330688ec3cd6742988c05703ea75373a9ffdec13e0961a8c977126dfc22ed61c0787cadbf3
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/regexp-regexp@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/regexp-regexp@npm:0.0.8"
+ dependencies:
+ "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x
+ checksum: 2a771dc4485fd7c05243627bb60b8de7395d53b7dac737952d16a55773b5912adc8a5cd4191b061e3739cd725662c8101c40c9900066e2faf04e51f11b8ff5d0
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/streams-node-stdin@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/streams-node-stdin@npm:0.0.7"
+ checksum: 750415b3c1cbb56592e9fbd9d7ccbb3b4e7f49be3f5ba95e1156efd0382ba23bf47eb3cf4088b5f6948a76c212af6b9e81ab78c24762d513b790bd8424700240
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/string-base-format-interpolate@npm:^0.0.x":
+ version: 0.0.4
+ resolution: "@stdlib/string-base-format-interpolate@npm:0.0.4"
+ checksum: 6eec4db6689fc7db21b85d70a944995721d5dcc6805f7a0c1c90070ed574e831252d252ce121abf4c76d2fdf0290ba04687cd867b23bd7f8229bd4e11b4df7fd
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/string-base-format-tokenize@npm:^0.0.x":
+ version: 0.0.4
+ resolution: "@stdlib/string-base-format-tokenize@npm:0.0.4"
+ checksum: 40a18396c16c75ecfc9b517119a276f5bdcc3f45ef6107ed6d3f4549e2be3b88a5eee38149dd5010a5f79bd091add273b162cbe2a95af90a86749744a0dc32dd
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/string-format@npm:^0.0.x":
+ version: 0.0.3
+ resolution: "@stdlib/string-format@npm:0.0.3"
+ dependencies:
+ "@stdlib/string-base-format-interpolate": ^0.0.x
+ "@stdlib/string-base-format-tokenize": ^0.0.x
+ checksum: b66be4fb1afa0d9b1ba3ee9fd00e0cf4d466e15e209f349d123e542292fa2489a1e2253dd170e87182ef7a51d103e2458df44f4ca3a697add6795525736982fe
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8":
- version: 1.2.8
- resolution: "@nodelib/fs.walk@npm:1.2.8"
+"@stdlib/string-lowercase@npm:^0.0.x":
+ version: 0.0.9
+ resolution: "@stdlib/string-lowercase@npm:0.0.9"
dependencies:
- "@nodelib/fs.scandir": 2.1.5
- fastq: ^1.6.0
- checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ "@stdlib/process-read-stdin": ^0.0.x
+ "@stdlib/streams-node-stdin": ^0.0.x
+ "@stdlib/string-format": ^0.0.x
+ bin:
+ lowercase: bin/cli
+ checksum: 278ababf4abf70eb08b58a79b9049a42ea867a51ab4391eca07afddc6ea643b7ceed8cbd5484f10c5602e1b7c89c7f98a68ed4a4f92c99b093919f10fe3a2d7d
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/string-replace@npm:^0.0.x":
+ version: 0.0.11
+ resolution: "@stdlib/string-replace@npm:0.0.11"
+ dependencies:
+ "@stdlib/assert-is-function": ^0.0.x
+ "@stdlib/assert-is-regexp": ^0.0.x
+ "@stdlib/assert-is-regexp-string": ^0.0.x
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ "@stdlib/process-read-stdin": ^0.0.x
+ "@stdlib/regexp-eol": ^0.0.x
+ "@stdlib/streams-node-stdin": ^0.0.x
+ "@stdlib/string-format": ^0.0.x
+ "@stdlib/utils-escape-regexp-string": ^0.0.x
+ "@stdlib/utils-regexp-from-string": ^0.0.x
+ bin:
+ replace: bin/cli
+ checksum: 63c28624c18caf0557dcfb3a4837d9188b390e8a7ac31b5bdc1f8299d84812bed7f83f49128b7faefc2a4b5d67ec46997243f2c524c6e013d9ba7f9811625d67
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@npmcli/fs@npm:^1.0.0":
- version: 1.1.1
- resolution: "@npmcli/fs@npm:1.1.1"
- dependencies:
- "@gar/promisify": ^1.0.1
- semver: ^7.3.5
- checksum: f5ad92f157ed222e4e31c352333d0901df02c7c04311e42a81d8eb555d4ec4276ea9c635011757de20cc476755af33e91622838de573b17e52e2e7703f0a9965
+"@stdlib/types@npm:^0.0.x":
+ version: 0.0.14
+ resolution: "@stdlib/types@npm:0.0.14"
+ checksum: 5680a655ddb3ad730f5c7eb2363a43e089f3e6a1b85b12546cab49f7749bb3baf293bd50fbfe55486f233f4227f1020b65eb461b754b94fb4a4bc2799647ec22
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@npmcli/move-file@npm:^1.0.1":
- version: 1.1.2
- resolution: "@npmcli/move-file@npm:1.1.2"
+"@stdlib/utils-constructor-name@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/utils-constructor-name@npm:0.0.8"
dependencies:
- mkdirp: ^1.0.4
- rimraf: ^3.0.2
- checksum: c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7
+ "@stdlib/assert-is-buffer": ^0.0.x
+ "@stdlib/regexp-function-name": ^0.0.x
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: a8ad0c7db0b34d0d6015f5a01907cdfedfe9419b11cc949bd3ec28f63f29f402e6c101217ab658d8c0e815eb5c8f0672bcf9dae9a3582f4b56bef5157c2fac4c
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@pkgr/utils@npm:^2.3.1":
- version: 2.3.1
- resolution: "@pkgr/utils@npm:2.3.1"
+"@stdlib/utils-convert-path@npm:^0.0.8":
+ version: 0.0.8
+ resolution: "@stdlib/utils-convert-path@npm:0.0.8"
+ dependencies:
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/cli-ctor": ^0.0.x
+ "@stdlib/fs-read-file": ^0.0.x
+ "@stdlib/process-read-stdin": ^0.0.x
+ "@stdlib/regexp-eol": ^0.0.x
+ "@stdlib/regexp-extended-length-path": ^0.0.x
+ "@stdlib/streams-node-stdin": ^0.0.x
+ "@stdlib/string-lowercase": ^0.0.x
+ "@stdlib/string-replace": ^0.0.x
+ bin:
+ convert-path: bin/cli
+ checksum: d8eb6dd6b2530b05af7a58c6be2b842c5abbfb02860ba7c9a4bb035c6092d3e6781708ec00d46af97c04598b0df18ce2a81cc4068ce48907d84ba975594e52a6
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
+ languageName: node
+ linkType: hard
+
+"@stdlib/utils-define-nonenumerable-read-only-property@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/utils-define-nonenumerable-read-only-property@npm:0.0.7"
dependencies:
- cross-spawn: ^7.0.3
- is-glob: ^4.0.3
- open: ^8.4.0
- picocolors: ^1.0.0
- tiny-glob: ^0.2.9
- tslib: ^2.4.0
- checksum: 118a1971120253740121a1db0a6658c21195b7da962acf9c124b507a3df707cfc97b0b84a16edcbd4352853b182e8337da9fc6e8e3d06c60d75ae4fb42321c75
+ "@stdlib/types": ^0.0.x
+ "@stdlib/utils-define-property": ^0.0.x
+ checksum: 0ec1480e58c25d64144cf0c88e241604a07a9faa613e0468d79e7597adfc109a57cb08afac1868180e6fe0b02cd3517b6fc2da80d03300e63e61fc0709cf8090
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@rollup/plugin-commonjs@npm:^24.1.0":
- version: 24.1.0
- resolution: "@rollup/plugin-commonjs@npm:24.1.0"
+"@stdlib/utils-define-property@npm:^0.0.x":
+ version: 0.0.9
+ resolution: "@stdlib/utils-define-property@npm:0.0.9"
dependencies:
- "@rollup/pluginutils": ^5.0.1
- commondir: ^1.0.1
- estree-walker: ^2.0.2
- glob: ^8.0.3
- is-reference: 1.2.1
- magic-string: ^0.27.0
- peerDependencies:
- rollup: ^2.68.0||^3.0.0
- peerDependenciesMeta:
- rollup:
- optional: true
- checksum: 42faafc9bc8e04d75c86bb50d693ebb9c5eee19bf9ab3c09780b872547d12ff5ea85cfec7da75f5176d0aa4b5233101f667f44b85b331450a7bb14c95180852e
+ "@stdlib/types": ^0.0.x
+ checksum: f9b7b20765ce7fcae7a0b57b90133be4259344f37faea929d873f46d5e4bb56c9da74fdbea26d945da94536e94b477ebd1a41708d2dbcdfe3d69208674175438
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@rollup/plugin-json@npm:^6.0.0":
- version: 6.0.0
- resolution: "@rollup/plugin-json@npm:6.0.0"
+"@stdlib/utils-escape-regexp-string@npm:^0.0.x":
+ version: 0.0.9
+ resolution: "@stdlib/utils-escape-regexp-string@npm:0.0.9"
dependencies:
- "@rollup/pluginutils": ^5.0.1
- peerDependencies:
- rollup: ^1.20.0||^2.0.0||^3.0.0
- peerDependenciesMeta:
- rollup:
- optional: true
- checksum: 77cfc941edaf77a5307977704ffaba706d83bea66f265b2b68f14be2a0af6d08b0fb1b04fdd773146c84cc70938ff64b00ae946808fd6ac057058af824d78128
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/string-format": ^0.0.x
+ checksum: ffe0d50b217fbaaf65bfcc852fe422cf1c21dca52aadf50130d7132d38f7a108081e71a3397ea8d32cf1f15a6b9a71f9dc05e8ce39b715b53faedcb1b517d21f
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@rollup/plugin-node-resolve@npm:^15.0.2":
- version: 15.0.2
- resolution: "@rollup/plugin-node-resolve@npm:15.0.2"
+"@stdlib/utils-get-prototype-of@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/utils-get-prototype-of@npm:0.0.7"
dependencies:
- "@rollup/pluginutils": ^5.0.1
- "@types/resolve": 1.20.2
- deepmerge: ^4.2.2
- is-builtin-module: ^3.2.1
- is-module: ^1.0.0
- resolve: ^1.22.1
- peerDependencies:
- rollup: ^2.78.0||^3.0.0
- peerDependenciesMeta:
- rollup:
- optional: true
- checksum: 328eafee06ff967a36441b55e77fbd0d4f599d256e5d1977800ee71915846c46bc1b6185df35c7b512ad2b4023b05b65a332be77b8b00b9d8a20f87d056b8166
+ "@stdlib/assert-is-function": ^0.0.x
+ "@stdlib/utils-native-class": ^0.0.x
+ checksum: 850f374a4b3bb49e6812827872e4e2e0cdbebefba62ab51d5888f1434f9b4c17ea56fde861899d6ca720e98260bc1f797fcd47d52651964e728474a8e1fd085a
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@rollup/plugin-typescript@npm:^11.1.1":
- version: 11.1.1
- resolution: "@rollup/plugin-typescript@npm:11.1.1"
+"@stdlib/utils-global@npm:^0.0.x":
+ version: 0.0.7
+ resolution: "@stdlib/utils-global@npm:0.0.7"
dependencies:
- "@rollup/pluginutils": ^5.0.1
- resolve: ^1.22.1
- peerDependencies:
- rollup: ^2.14.0||^3.0.0
- tslib: "*"
- typescript: ">=3.7.0"
- peerDependenciesMeta:
- rollup:
- optional: true
- tslib:
- optional: true
- checksum: 0e82ef17ded026060bbc237ed2552e227d5b514cd58c1f624afeb65704a24048e38918e6242bcf29960612dc9925fbf1bb0625475ec691330cd144e4a84e3065
+ "@stdlib/assert-is-boolean": ^0.0.x
+ checksum: 64f429ae756ad7c515696e537d76fe1ea278863c2fb6bc92dcb989b2f3a8d4f258d4c3f152df2f480fb1b5af6a960c8c8a948d6a37fbf6bae229940bcc5c076e
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@rollup/pluginutils@npm:^5.0.1":
- version: 5.0.2
- resolution: "@rollup/pluginutils@npm:5.0.2"
+"@stdlib/utils-native-class@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/utils-native-class@npm:0.0.8"
dependencies:
- "@types/estree": ^1.0.0
- estree-walker: ^2.0.2
- picomatch: ^2.3.1
- peerDependencies:
- rollup: ^1.20.0||^2.0.0||^3.0.0
- peerDependenciesMeta:
- rollup:
- optional: true
- checksum: edea15e543bebc7dcac3b0ac8bc7b8e8e6dbd46e2864dbe5dd28072de1fbd5b0e10d545a610c0edaa178e8a7ac432e2a2a52e547ece1308471412caba47db8ce
+ "@stdlib/assert-has-own-property": ^0.0.x
+ "@stdlib/assert-has-tostringtag-support": ^0.0.x
+ checksum: 4867eb4107d3924dfb60fa3351412c6790440e917087be6029ca89780b4a528aacaa672f588d482f5ca905842893cc6ae3cf6d0ee44ec378d1a578ae223fea37
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@sinclair/typebox@npm:^0.25.16":
- version: 0.25.24
- resolution: "@sinclair/typebox@npm:0.25.24"
- checksum: 10219c58f40b8414c50b483b0550445e9710d4fe7b2c4dccb9b66533dd90ba8e024acc776026cebe81e87f06fa24b07fdd7bc30dd277eb9cc386ec50151a3026
+"@stdlib/utils-next-tick@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/utils-next-tick@npm:0.0.8"
+ checksum: 0ef1d398e10ec35fa2c84168d9d85b110c6900405dd851071af36c4bdc42ad3f0239b501f8bde9cdfa4ee0d8540a0453e15838a6db2d3604d92fd94d6e412ec6
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@sinonjs/commons@npm:^3.0.0":
- version: 3.0.0
- resolution: "@sinonjs/commons@npm:3.0.0"
- dependencies:
- type-detect: 4.0.8
- checksum: b4b5b73d4df4560fb8c0c7b38c7ad4aeabedd362f3373859d804c988c725889cde33550e4bcc7cd316a30f5152a2d1d43db71b6d0c38f5feef71fd8d016763f8
+"@stdlib/utils-noop@npm:^0.0.x":
+ version: 0.0.14
+ resolution: "@stdlib/utils-noop@npm:0.0.14"
+ checksum: 907017cedeb7329b611dc6217a1c4c5a39070dc06e3bf5c895b11786d126eea0367d83315d48519395d5ac4af713155e0f1ccf5f2741ed03701b7a76355b975c
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@sinonjs/fake-timers@npm:^10.0.2":
- version: 10.2.0
- resolution: "@sinonjs/fake-timers@npm:10.2.0"
+"@stdlib/utils-regexp-from-string@npm:^0.0.x":
+ version: 0.0.9
+ resolution: "@stdlib/utils-regexp-from-string@npm:0.0.9"
dependencies:
- "@sinonjs/commons": ^3.0.0
- checksum: 586c76e1dd90d03b0c4e754f2011325b38ac6055878c81c52434c900f36d9d245438c96ef69e08e28d9fbecf2335fb347b67850962d8b6e539dd7359d8c62802
+ "@stdlib/assert-is-string": ^0.0.x
+ "@stdlib/regexp-regexp": ^0.0.x
+ "@stdlib/string-format": ^0.0.x
+ checksum: 78e2fd5e007744320f8b81ae753bcb58e7b702878f04fc90cb073beed79d1a35d753835eb4a17ec5a8b187b0a7587ac652f96309cb4c6d37119e615240c015d5
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
-"@tootallnate/once@npm:1":
- version: 1.1.2
- resolution: "@tootallnate/once@npm:1.1.2"
- checksum: e1fb1bbbc12089a0cb9433dc290f97bddd062deadb6178ce9bcb93bb7c1aecde5e60184bc7065aec42fe1663622a213493c48bbd4972d931aae48315f18e1be9
+"@stdlib/utils-type-of@npm:^0.0.x":
+ version: 0.0.8
+ resolution: "@stdlib/utils-type-of@npm:0.0.8"
+ dependencies:
+ "@stdlib/utils-constructor-name": ^0.0.x
+ "@stdlib/utils-global": ^0.0.x
+ checksum: de4e2a5c797010060107ae620de4b64f198f9b28a4e4a6910a6ae310b3a28594fbe47c7ab701ad2dc5d58076b5ed06e7fcf77e89ca609980aa0367c7bfcf4dcd
+ conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows)
languageName: node
linkType: hard
@@ -2089,6 +2660,13 @@ __metadata:
languageName: node
linkType: hard
+"@tootallnate/quickjs-emscripten@npm:^0.23.0":
+ version: 0.23.0
+ resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0"
+ checksum: c350a2947ffb80b22e14ff35099fd582d1340d65723384a0fd0515e905e2534459ad2f301a43279a37308a27c99273c932e64649abd57d0bb3ca8c557150eccc
+ languageName: node
+ linkType: hard
+
"@tsconfig/node10@npm:^1.0.7":
version: 1.0.8
resolution: "@tsconfig/node10@npm:1.0.8"
@@ -2279,12 +2857,12 @@ __metadata:
linkType: hard
"@types/jest@npm:25.1.0 - 29, @types/jest@npm:^29.5.2":
- version: 29.5.2
- resolution: "@types/jest@npm:29.5.2"
+ version: 29.5.3
+ resolution: "@types/jest@npm:29.5.3"
dependencies:
expect: ^29.0.0
pretty-format: ^29.0.0
- checksum: 7d205599ea3cccc262bad5cc173d3242d6bf8138c99458509230e4ecef07a52d6ddcde5a1dbd49ace655c0af51d2dbadef3748697292ea4d86da19d9e03e19c0
+ checksum: e36bb92e0b9e5ea7d6f8832baa42f087fc1697f6cd30ec309a07ea4c268e06ec460f1f0cfd2581daf5eff5763475190ec1ad8ac6520c49ccfe4f5c0a48bfa676
languageName: node
linkType: hard
@@ -2623,6 +3201,7 @@ __metadata:
"@rollup/plugin-json": ^6.0.0
"@rollup/plugin-node-resolve": ^15.0.2
"@rollup/plugin-typescript": ^11.1.1
+ "@stdlib/utils-convert-path": ^0.0.8
"@types/debug": ^4.1.7
"@types/deep-equal": ^1.0.1
"@types/es6-promisify": ^6.0.1
@@ -2637,12 +3216,14 @@ __metadata:
cli-table3: 0.5.1
cosmiconfig: ^7.0.1
cross-env: ^7.0.3
- cypress: 10 - 12
+ cypress: 11.2 - 13
cypress-multi-reporters: ^1.6.3
+ cypress-on-fix: ^1.0.2
dayjs: ^1.10.4
debug: ^4.3.3
deep-equal: ^2.0.5
es6-promisify: ^7.0.0
+ escape-string-regexp: ^4.0.0
jest: ^29.5.0
jest-environment-node: ^29.5.0
lodash: ^4.17.21
@@ -2650,15 +3231,18 @@ __metadata:
mocha: =7.0.1
ms: 2.1.1
path-browserify: ^1.0.1
+ rimraf: ^5.0.1
rollup: ^3.21.1
+ rollup-plugin-dts: ^5.3.0
simple-git: ^3.16.0
term-size: 2.1.0
tmp: ~0.2.1
+ ts-jest: ^29.1.0
typescript: ^4.9.5
widest-line: 3.1.0
yargs: ^17.7.2
peerDependencies:
- cypress: 10 - 12
+ cypress: 11.2 - 13
bin:
cypress-unflakable: ./dist/main.js
languageName: unknown
@@ -2689,16 +3273,18 @@ __metadata:
deep-equal: ^2.0.5
escape-string-regexp: ^4.0.0
exit: ^0.1.2
+ jest: 25.1.0 - 29
+ jest-circus: 25.1.0 - 29
+ jest-environment-node: 25.1.0 - 29
jest-runner: 25.1.0 - 29
jest-util: 25.1.0 - 29
+ rimraf: ^5.0.1
rollup: ^3.21.1
+ semver: ^7.5.4
simple-git: ^3.16.0
typescript: ^4.9.5
peerDependencies:
- "@jest/console": 25.1.0 - 29
- "@jest/reporters": 25.1.0 - 29
- jest-runner: 25.1.0 - 29
- jest-util: 25.1.0 - 29
+ jest: 25.1.0 - 29
languageName: unknown
linkType: soft
@@ -2710,6 +3296,7 @@ __metadata:
"@types/node-fetch": ^2.6.2
async-retry: ^1.3.3
node-fetch: ^2.6.7
+ rimraf: ^5.0.1
languageName: unknown
linkType: soft
@@ -2720,6 +3307,7 @@ __metadata:
"@unflakable/js-api": "workspace:^"
cosmiconfig: ^7.0.1
debug: ^4.3.3
+ rimraf: ^5.0.1
simple-git: ^3.16.0
languageName: unknown
linkType: soft
@@ -2924,14 +3512,14 @@ __metadata:
languageName: node
linkType: hard
-"acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0":
+"acorn-walk@npm:^8.1.1":
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.5.0, acorn@npm:^8.7.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0":
+"acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0":
version: 8.8.2
resolution: "acorn@npm:8.8.2"
bin:
@@ -2949,6 +3537,15 @@ __metadata:
languageName: node
linkType: hard
+"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "agent-base@npm:7.1.0"
+ dependencies:
+ debug: ^4.3.4
+ checksum: f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f
+ languageName: node
+ linkType: hard
+
"agentkeepalive@npm:^4.2.1":
version: 4.2.1
resolution: "agentkeepalive@npm:4.2.1"
@@ -3035,6 +3632,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-regex@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "ansi-regex@npm:6.0.1"
+ checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169
+ languageName: node
+ linkType: hard
+
"ansi-styles@npm:^3.2.0, ansi-styles@npm:^3.2.1":
version: 3.2.1
resolution: "ansi-styles@npm:3.2.1"
@@ -3060,6 +3664,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-styles@npm:^6.1.0":
+ version: 6.2.1
+ resolution: "ansi-styles@npm:6.2.1"
+ checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9
+ languageName: node
+ linkType: hard
+
"anymatch@npm:^3.0.3, anymatch@npm:~3.1.1":
version: 3.1.3
resolution: "anymatch@npm:3.1.3"
@@ -3207,7 +3818,7 @@ __metadata:
languageName: node
linkType: hard
-"ast-types@npm:^0.13.2":
+"ast-types@npm:^0.13.4":
version: 0.13.4
resolution: "ast-types@npm:0.13.4"
dependencies:
@@ -3325,39 +3936,39 @@ __metadata:
languageName: node
linkType: hard
-"babel-plugin-polyfill-corejs2@npm:^0.3.3":
- version: 0.3.3
- resolution: "babel-plugin-polyfill-corejs2@npm:0.3.3"
+"babel-plugin-polyfill-corejs2@npm:^0.4.4":
+ version: 0.4.5
+ resolution: "babel-plugin-polyfill-corejs2@npm:0.4.5"
dependencies:
- "@babel/compat-data": ^7.17.7
- "@babel/helper-define-polyfill-provider": ^0.3.3
- semver: ^6.1.1
+ "@babel/compat-data": ^7.22.6
+ "@babel/helper-define-polyfill-provider": ^0.4.2
+ semver: ^6.3.1
peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 7db3044993f3dddb3cc3d407bc82e640964a3bfe22de05d90e1f8f7a5cb71460011ab136d3c03c6c1ba428359ebf635688cd6205e28d0469bba221985f5c6179
+ "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
+ checksum: 33a8e06aa54e2858d211c743d179f0487b03222f9ca1bfd7c4865bca243fca942a3358cb75f6bb894ed476cbddede834811fbd6903ff589f055821146f053e1a
languageName: node
linkType: hard
-"babel-plugin-polyfill-corejs3@npm:^0.6.0":
- version: 0.6.0
- resolution: "babel-plugin-polyfill-corejs3@npm:0.6.0"
+"babel-plugin-polyfill-corejs3@npm:^0.8.2":
+ version: 0.8.3
+ resolution: "babel-plugin-polyfill-corejs3@npm:0.8.3"
dependencies:
- "@babel/helper-define-polyfill-provider": ^0.3.3
- core-js-compat: ^3.25.1
+ "@babel/helper-define-polyfill-provider": ^0.4.2
+ core-js-compat: ^3.31.0
peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 470bb8c59f7c0912bd77fe1b5a2e72f349b3f65bbdee1d60d6eb7e1f4a085c6f24b2dd5ab4ac6c2df6444a96b070ef6790eccc9edb6a2668c60d33133bfb62c6
+ "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
+ checksum: dcbb30e551702a82cfd4d2c375da2c317658e55f95e9edcda93b9bbfdcc8fb6e5344efcb144e04d3406859e7682afce7974c60ededd9f12072a48a83dd22a0da
languageName: node
linkType: hard
-"babel-plugin-polyfill-regenerator@npm:^0.4.1":
- version: 0.4.1
- resolution: "babel-plugin-polyfill-regenerator@npm:0.4.1"
+"babel-plugin-polyfill-regenerator@npm:^0.5.1":
+ version: 0.5.2
+ resolution: "babel-plugin-polyfill-regenerator@npm:0.5.2"
dependencies:
- "@babel/helper-define-polyfill-provider": ^0.3.3
+ "@babel/helper-define-polyfill-provider": ^0.4.2
peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: ab0355efbad17d29492503230387679dfb780b63b25408990d2e4cf421012dae61d6199ddc309f4d2409ce4e9d3002d187702700dd8f4f8770ebbba651ed066c
+ "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
+ checksum: d962200f604016a9a09bc9b4aaf60a3db7af876bb65bcefaeac04d44ac9d9ec4037cf24ce117760cc141d7046b6394c7eb0320ba9665cb4a2ee64df2be187c93
languageName: node
linkType: hard
@@ -3423,6 +4034,13 @@ __metadata:
languageName: node
linkType: hard
+"basic-ftp@npm:^5.0.2":
+ version: 5.0.3
+ resolution: "basic-ftp@npm:5.0.3"
+ checksum: 8b04e88eb85a64de9311721bb0707c9cd70453eefdd854cab85438e6f46fb6c597ddad57ed1acf0a9ede3c677b14e657f51051688a5f23d6f3ea7b5d9073b850
+ languageName: node
+ linkType: hard
+
"bcrypt-pbkdf@npm:^1.0.0":
version: 1.0.2
resolution: "bcrypt-pbkdf@npm:1.0.2"
@@ -3535,17 +4153,17 @@ __metadata:
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"
+"browserslist@npm:^4.14.5, browserslist@npm:^4.21.9":
+ version: 4.21.9
+ resolution: "browserslist@npm:4.21.9"
dependencies:
- caniuse-lite: ^1.0.30001449
- electron-to-chromium: ^1.4.284
- node-releases: ^2.0.8
- update-browserslist-db: ^1.0.10
+ caniuse-lite: ^1.0.30001503
+ electron-to-chromium: ^1.4.431
+ node-releases: ^2.0.12
+ update-browserslist-db: ^1.0.11
bin:
browserslist: cli.js
- checksum: 9755986b22e73a6a1497fd8797aedd88e04270be33ce66ed5d85a1c8a798292a65e222b0f251bafa1c2522261e237d73b08b58689d4920a607e5a53d56dc4706
+ checksum: 80d3820584e211484ad1b1a5cfdeca1dd00442f47be87e117e1dda34b628c87e18b81ae7986fa5977b3e6a03154f6d13cd763baa6b8bf5dd9dd19f4926603698
languageName: node
linkType: hard
@@ -3676,10 +4294,10 @@ __metadata:
languageName: node
linkType: hard
-"caniuse-lite@npm:^1.0.30001449":
- version: 1.0.30001465
- resolution: "caniuse-lite@npm:1.0.30001465"
- checksum: c991ecdfff378a22b268f9b1eb732d003c8ad89db3241a4cdec3b3ec3354aa966a44171cb806c90abe2e3f0573d67dc29a7dce2478b1f070b23747c392244c5d
+"caniuse-lite@npm:^1.0.30001503":
+ version: 1.0.30001517
+ resolution: "caniuse-lite@npm:1.0.30001517"
+ checksum: e4e87436ae1c4408cf4438aac22902b31eb03f3f5bad7f33bc518d12ffb35f3fd9395ccf7efc608ee046f90ce324ec6f7f26f8a8172b8c43c26a06ecee612a29
languageName: node
linkType: hard
@@ -4037,19 +4655,12 @@ __metadata:
languageName: node
linkType: hard
-"core-js-compat@npm:^3.25.1":
- version: 3.30.1
- resolution: "core-js-compat@npm:3.30.1"
+"core-js-compat@npm:^3.31.0":
+ version: 3.32.0
+ resolution: "core-js-compat@npm:3.32.0"
dependencies:
- browserslist: ^4.21.5
- checksum: e450a9771fc927ce982333929e1c4b32f180f641e4cfff9de6ed44b5930de19be7707cf74f45d1746ca69b8e8ac0698a555cb7244fbfbed6c38ca93844207bf7
- languageName: node
- linkType: hard
-
-"core-js@npm:^3.0.0":
- version: 3.21.1
- resolution: "core-js@npm:3.21.1"
- checksum: d68eddd831340ad5b24ac29c72fda022a43b17f194c4278b6b875a843283d316502cb4abd07f28631d6ebc4387f66aa06e2b1b3c8fd7e08096a751b5c63f6889
+ browserslist: ^4.21.9
+ checksum: e740b348dfd8dc25ac851ab625a1d5a63c012252bdd6d8ae92d1b2ebf46e6cf57ca6cbec4494cbacdd90d3f8ed822480c8a7106c990dbe9055ebdf5b79fbb92e
languageName: node
linkType: hard
@@ -4150,22 +4761,6 @@ __metadata:
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"
@@ -4173,8 +4768,7 @@ __metadata:
"@types/react": ^18.2.7
"@types/react-dom": ^18.2.4
"@unflakable/cypress-plugin": "workspace:^"
- cypress: 10 - 12
- cypress-integration-common: "workspace:^"
+ cypress: 11.2 - 13
mocha: =7.0.1
mocha-junit-reporter: ^2.2.0
process: ^0.11.10
@@ -4182,6 +4776,7 @@ __metadata:
react-dom: ^18.2.0
ts-loader: ^9.4.3
typescript: ^4.9.5
+ unflakable-test-common: "workspace:^"
webpack: ^5.84.1
languageName: unknown
linkType: soft
@@ -4193,16 +4788,18 @@ __metadata:
"@types/react": ^18.2.7
"@types/react-dom": ^18.2.4
"@unflakable/cypress-plugin": "workspace:^"
- cypress: 10 - 12
- cypress-integration-common: "workspace:^"
+ cypress: 11.2 - 13
cypress-multi-reporters: ^1.6.3
+ cypress-on-fix: ^1.0.2
mocha: =7.0.1
mocha-junit-reporter: ^2.2.0
process: ^0.11.10
react: ^18.2.0
react-dom: ^18.2.0
+ semver: ^7.5.4
ts-loader: ^9.4.3
typescript: ^4.9.5
+ unflakable-test-common: "workspace:^"
webpack: ^5.84.1
languageName: unknown
linkType: soft
@@ -4214,8 +4811,7 @@ __metadata:
"@types/react": ^18.2.7
"@types/react-dom": ^18.2.4
"@unflakable/cypress-plugin": "workspace:^"
- cypress: 10 - 12
- cypress-integration-common: "workspace:^"
+ cypress: 11.2 - 13
mocha: =7.0.1
mocha-junit-reporter: ^2.2.0
process: ^0.11.10
@@ -4223,6 +4819,7 @@ __metadata:
react-dom: ^18.2.0
ts-loader: ^9.4.3
typescript: ^4.9.5
+ unflakable-test-common: "workspace:^"
webpack: ^5.84.1
languageName: unknown
linkType: soft
@@ -4232,21 +4829,20 @@ __metadata:
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:^"
+ cypress: 11.2 - 13
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
+ mockttp: ^3.9.2
+ semver: ^7.5.4
ts-jest: ^29.1.0
typescript: ^4.9.5
+ unflakable-test-common: "workspace:^"
languageName: unknown
linkType: soft
@@ -4262,7 +4858,14 @@ __metadata:
languageName: node
linkType: hard
-"cypress@npm:10 - 12":
+"cypress-on-fix@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "cypress-on-fix@npm:1.0.2"
+ checksum: b35e0d49e4270237e7cbe95c21d458772d3df6bbb4423346c70f9417e61fdf061ad1d83aca76a854a378d001a68f50c17b8dd312fbe9c50b5d12e61fc317a785
+ languageName: node
+ linkType: hard
+
+"cypress@npm:11.2 - 13":
version: 12.14.0
resolution: "cypress@npm:12.14.0"
dependencies:
@@ -4323,10 +4926,10 @@ __metadata:
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
+"data-uri-to-buffer@npm:^5.0.1":
+ version: 5.0.1
+ resolution: "data-uri-to-buffer@npm:5.0.1"
+ checksum: 10958f89c0047b84bd86d572b6b77c9bf238ebe7b55a9a9ab04c90fbf5ab1881783b72e31dc0febdffd30ec914930244f2f728e3629bb8911d922baba129426f
languageName: node
linkType: hard
@@ -4413,7 +5016,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
@@ -4444,15 +5047,14 @@ __metadata:
languageName: node
linkType: hard
-"degenerator@npm:^3.0.2":
- version: 3.0.4
- resolution: "degenerator@npm:3.0.4"
+"degenerator@npm:^5.0.0":
+ version: 5.0.1
+ resolution: "degenerator@npm:5.0.1"
dependencies:
- ast-types: ^0.13.2
- escodegen: ^1.8.1
- esprima: ^4.0.0
- vm2: ^3.9.17
- checksum: 99c27c9456095e32c4f6e01091d2b5c249f246b574487c52bca571e1e586b02d4b74a0ea7f22f30cc953c914383d02e2038d7d476a22f2704a8c1e88b671007d
+ ast-types: ^0.13.4
+ escodegen: ^2.1.0
+ esprima: ^4.0.1
+ checksum: a64fa39cdf6c2edd75188157d32338ee9de7193d7dbb2aeb4acb1eb30fa4a15ed80ba8dae9bd4d7b085472cf174a5baf81adb761aaa8e326771392c922084152
languageName: node
linkType: hard
@@ -4567,6 +5169,13 @@ __metadata:
languageName: node
linkType: hard
+"eastasianwidth@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "eastasianwidth@npm:0.2.0"
+ checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed
+ languageName: node
+ linkType: hard
+
"ecc-jsbn@npm:~0.1.1":
version: 0.1.2
resolution: "ecc-jsbn@npm:0.1.2"
@@ -4584,10 +5193,10 @@ __metadata:
languageName: node
linkType: hard
-"electron-to-chromium@npm:^1.4.284":
- version: 1.4.328
- resolution: "electron-to-chromium@npm:1.4.328"
- checksum: 82c1617a77e40ac4ca5011749318a2fee8f8c75f8b517fcff7602219c85fd97a9fab2d5a1353ea10fb7f9c7d18acb90c9ed58c2292256f81e2ffa42ee66c4b0b
+"electron-to-chromium@npm:^1.4.431":
+ version: 1.4.476
+ resolution: "electron-to-chromium@npm:1.4.476"
+ checksum: 0b769c3b85bf4f28f58bf1c61caf14e7d5ef50e7e8250fc7594e81f4213e2c928f650a68edb8f0580e1f479b4548f34ca2096f661d44faf5de8141593d31a9b6
languageName: node
linkType: hard
@@ -4612,6 +5221,13 @@ __metadata:
languageName: node
linkType: hard
+"emoji-regex@npm:^9.2.2":
+ version: 9.2.2
+ resolution: "emoji-regex@npm:9.2.2"
+ checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601
+ languageName: node
+ linkType: hard
+
"encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
@@ -4824,14 +5440,13 @@ __metadata:
languageName: node
linkType: hard
-"escodegen@npm:^1.8.1":
- version: 1.14.3
- resolution: "escodegen@npm:1.14.3"
+"escodegen@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "escodegen@npm:2.1.0"
dependencies:
esprima: ^4.0.1
- estraverse: ^4.2.0
+ estraverse: ^5.2.0
esutils: ^2.0.2
- optionator: ^0.8.1
source-map: ~0.6.1
dependenciesMeta:
source-map:
@@ -4839,7 +5454,7 @@ __metadata:
bin:
escodegen: bin/escodegen.js
esgenerate: bin/esgenerate.js
- checksum: 381cdc4767ecdb221206bbbab021b467bbc2a6f5c9a99c9e6353040080bdd3dfe73d7604ad89a47aca6ea7d58bc635f6bd3fbc8da9a1998e9ddfa8372362ccd0
+ checksum: 096696407e161305cd05aebb95134ad176708bc5cb13d0dcc89a5fcbb959b8ed757e7f2591a5f8036f8f4952d4a724de0df14cd419e29212729fa6df5ce16bf6
languageName: node
linkType: hard
@@ -5054,7 +5669,7 @@ __metadata:
languageName: node
linkType: hard
-"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0":
+"estraverse@npm:^4.1.1":
version: 4.3.0
resolution: "estraverse@npm:4.3.0"
checksum: a6299491f9940bb246124a8d44b7b7a413a8336f5436f9837aaa9330209bd9ee8af7e91a654a3545aee9c54b3308e78ee360cef1d777d37cfef77d2fa33b5827
@@ -5160,7 +5775,7 @@ __metadata:
languageName: node
linkType: hard
-"expect@npm:^29.0.0, expect@npm:^29.5.0":
+"expect@npm:25.1.0 - 29, expect@npm:^29.0.0, expect@npm:^29.5.0":
version: 29.5.0
resolution: "expect@npm:29.5.0"
dependencies:
@@ -5291,7 +5906,7 @@ __metadata:
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
@@ -5325,43 +5940,6 @@ __metadata:
languageName: node
linkType: hard
-"fetch-mock-jest@npm:^1.5.1":
- version: 1.5.1
- resolution: "fetch-mock-jest@npm:1.5.1"
- dependencies:
- fetch-mock: ^9.11.0
- peerDependencies:
- node-fetch: "*"
- peerDependenciesMeta:
- node-fetch:
- optional: true
- checksum: 371b1c4a1fb1cf507ace0bf1a505ae0a9b0184ef0edfa6225c3d93866ba80fa89b47d412eb077315007e0a73028e2ed5be4b88b85e992e53a834ecbd44c4a3be
- languageName: node
- linkType: hard
-
-"fetch-mock@npm:^9.11.0":
- version: 9.11.0
- resolution: "fetch-mock@npm:9.11.0"
- dependencies:
- "@babel/core": ^7.0.0
- "@babel/runtime": ^7.0.0
- core-js: ^3.0.0
- debug: ^4.1.1
- glob-to-regexp: ^0.4.0
- is-subset: ^0.1.1
- lodash.isequal: ^4.5.0
- path-to-regexp: ^2.2.1
- querystring: ^0.2.0
- whatwg-url: ^6.5.0
- peerDependencies:
- node-fetch: "*"
- peerDependenciesMeta:
- node-fetch:
- optional: true
- checksum: debc4dd83bcda79b0aa71c38d08da6036906cdc49393343eb3426112314a7e57557255664f745d2e3f0b9b2a6e852bd3a564ae3f08332c27e422d3441bb865bd
- languageName: node
- linkType: hard
-
"figures@npm:^3.2.0":
version: 3.2.0
resolution: "figures@npm:3.2.0"
@@ -5380,13 +5958,6 @@ __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"
@@ -5465,14 +6036,12 @@ __metadata:
languageName: node
linkType: hard
-"flat@npm:^4.1.0":
- version: 4.1.1
- resolution: "flat@npm:4.1.1"
- dependencies:
- is-buffer: ~2.0.3
+"flat@npm:^5.0.2":
+ version: 5.0.2
+ resolution: "flat@npm:5.0.2"
bin:
flat: cli.js
- checksum: 398be12185eb0f3c59797c3670a8c35d07020b673363175676afbaf53d6b213660e060488554cf82c25504986e1a6059bdbcc5d562e87ca3e972e8a33148e3ae
+ checksum: 12a1536ac746db74881316a181499a78ef953632ddd28050b7a3a43c62ef5462e3357c8c29d76072bb635f147f7a9a1f0c02efef6b4be28f8db62ceb3d5c7f5d
languageName: node
linkType: hard
@@ -5492,6 +6061,16 @@ __metadata:
languageName: node
linkType: hard
+"foreground-child@npm:^3.1.0":
+ version: 3.1.1
+ resolution: "foreground-child@npm:3.1.1"
+ dependencies:
+ cross-spawn: ^7.0.0
+ signal-exit: ^4.0.1
+ checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5
+ languageName: node
+ linkType: hard
+
"forever-agent@npm:~0.6.1":
version: 0.6.1
resolution: "forever-agent@npm:0.6.1"
@@ -5612,16 +6191,6 @@ __metadata:
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"
@@ -5730,17 +6299,15 @@ __metadata:
languageName: node
linkType: hard
-"get-uri@npm:3":
- version: 3.0.2
- resolution: "get-uri@npm:3.0.2"
+"get-uri@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "get-uri@npm:6.0.1"
dependencies:
- "@tootallnate/once": 1
- data-uri-to-buffer: 3
- debug: 4
- file-uri-to-path: 2
+ basic-ftp: ^5.0.2
+ data-uri-to-buffer: ^5.0.1
+ debug: ^4.3.4
fs-extra: ^8.1.0
- ftp: ^0.3.10
- checksum: 5325b2906b08ca37529ca421cf52bc50376e75c6a945e0a8064e3f76b4bb67b8ab1e316a2fc7a307c8c606ab36d030720f39a57c97b027ff1134335e12102946
+ checksum: a8aec70e1c67386fbe67f66e344ecd671a19f4cfc8e0f0e14d070563af5123d540e77fbceb6e26566f29846fac864d2862699ab134d307f85c85e7d72ce23d14
languageName: node
linkType: hard
@@ -5780,7 +6347,7 @@ __metadata:
languageName: node
linkType: hard
-"glob-to-regexp@npm:^0.4.0, glob-to-regexp@npm:^0.4.1":
+"glob-to-regexp@npm:^0.4.1":
version: 0.4.1
resolution: "glob-to-regexp@npm:0.4.1"
checksum: e795f4e8f06d2a15e86f76e4d92751cf8bbfcf0157cea5c2f0f35678a8195a750b34096b1256e436f0cebc1883b5ff0888c47348443e69546a5a87f9e1eb1167
@@ -5801,6 +6368,21 @@ __metadata:
languageName: node
linkType: hard
+"glob@npm:^10.2.5":
+ version: 10.3.0
+ resolution: "glob@npm:10.3.0"
+ dependencies:
+ foreground-child: ^3.1.0
+ jackspeak: ^2.0.3
+ minimatch: ^9.0.1
+ minipass: ^5.0.0 || ^6.0.2
+ path-scurry: ^1.7.0
+ bin:
+ glob: dist/cjs/src/bin.js
+ checksum: 6fa4ac0a86ffec1c5715a2e6fbdd63e1e7f1c2c8f5db08cc3256cdfcb81093678e7c80a3d100b502a1b9d141369ecf87bc24fe2bcb72acec7b14626d358a4eb0
+ languageName: node
+ linkType: hard
+
"glob@npm:^7.1.3, glob@npm:^7.1.4":
version: 7.2.3
resolution: "glob@npm:7.2.3"
@@ -6087,18 +6669,7 @@ __metadata:
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
+ checksum: 9b0a3782665c52ce9dc658a0d1560bcb0214ba5699e4ea15aefb2a496e2ca83db03ebc42e1cce4ac1f413e4e0d2d736a3fd755772c556a9a06853ba2a0b7d920
languageName: node
linkType: hard
@@ -6113,6 +6684,16 @@ __metadata:
languageName: node
linkType: hard
+"http-proxy-agent@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "http-proxy-agent@npm:7.0.0"
+ dependencies:
+ agent-base: ^7.1.0
+ debug: ^4.3.4
+ checksum: 48d4fac997917e15f45094852b63b62a46d0c8a4f0b9c6c23ca26d27b8df8d178bed88389e604745e748bd9a01f5023e25093722777f0593c3f052009ff438b6
+ languageName: node
+ linkType: hard
+
"http-signature@npm:~1.3.6":
version: 1.3.6
resolution: "http-signature@npm:1.3.6"
@@ -6134,7 +6715,7 @@ __metadata:
languageName: node
linkType: hard
-"https-proxy-agent@npm:5, https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1":
+"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:
@@ -6144,6 +6725,16 @@ __metadata:
languageName: node
linkType: hard
+"https-proxy-agent@npm:^7.0.2":
+ version: 7.0.2
+ resolution: "https-proxy-agent@npm:7.0.2"
+ dependencies:
+ agent-base: ^7.0.2
+ debug: 4
+ checksum: 088969a0dd476ea7a0ed0a2cf1283013682b08f874c3bc6696c83fa061d2c157d29ef0ad3eb70a2046010bb7665573b2388d10fdcb3e410a66995e5248444292
+ languageName: node
+ linkType: hard
+
"human-signals@npm:^1.1.1":
version: 1.1.1
resolution: "human-signals@npm:1.1.1"
@@ -6252,7 +6843,7 @@ __metadata:
languageName: node
linkType: hard
-"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":
+"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:~2.0.3":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1
@@ -6277,10 +6868,10 @@ __metadata:
languageName: node
linkType: hard
-"ip@npm:^1.1.5":
- version: 1.1.5
- resolution: "ip@npm:1.1.5"
- checksum: 30133981f082a060a32644f6a7746e9ba7ac9e2bc07ecc8bbdda3ee8ca9bec1190724c390e45a1ee7695e7edfd2a8f7dda2c104ec5f7ac5068c00648504c7e5a
+"ip@npm:^1.1.8":
+ version: 1.1.8
+ resolution: "ip@npm:1.1.8"
+ checksum: a2ade53eb339fb0cbe9e69a44caab10d6e3784662285eb5d2677117ee4facc33a64679051c35e0dfdb1a3983a51ce2f5d2cb36446d52e10d01881789b76e28fb
languageName: node
linkType: hard
@@ -6361,13 +6952,6 @@ __metadata:
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"
@@ -6395,12 +6979,12 @@ __metadata:
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"
+"is-core-module@npm:^2.11.0, is-core-module@npm:^2.12.0":
+ version: 2.12.1
+ resolution: "is-core-module@npm:2.12.1"
dependencies:
has: ^1.0.3
- checksum: f96fd490c6b48eb4f6d10ba815c6ef13f410b0ba6f7eb8577af51697de523e5f2cd9de1c441b51d27251bf0e4aebc936545e33a5d26d5d51f28d25698d4a8bab
+ checksum: f04ea30533b5e62764e7b2e049d3157dc0abd95ef44275b32489ea2081176ac9746ffb1cdb107445cf1ff0e0dfcad522726ca27c27ece64dadf3795428b8e468
languageName: node
linkType: hard
@@ -6520,6 +7104,13 @@ __metadata:
languageName: node
linkType: hard
+"is-plain-obj@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "is-plain-obj@npm:1.1.0"
+ checksum: 0ee04807797aad50859652a7467481816cbb57e5cc97d813a7dcd8915da8195dc68c436010bf39d195226cde6a2d352f4b815f16f26b7bf486a5754290629931
+ languageName: node
+ linkType: hard
+
"is-reference@npm:1.2.1":
version: 1.2.1
resolution: "is-reference@npm:1.2.1"
@@ -6571,13 +7162,6 @@ __metadata:
languageName: node
linkType: hard
-"is-subset@npm:^0.1.1":
- version: 0.1.1
- resolution: "is-subset@npm:0.1.1"
- checksum: 97b8d7852af165269b7495095691a6ce6cf20bdfa1f846f97b4560ee190069686107af4e277fbd93aa0845c4d5db704391460ff6e9014aeb73264ba87893df44
- languageName: node
- linkType: hard
-
"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3":
version: 1.0.4
resolution: "is-symbol@npm:1.0.4"
@@ -6649,13 +7233,6 @@ __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"
@@ -6752,6 +7329,19 @@ __metadata:
languageName: node
linkType: hard
+"jackspeak@npm:^2.0.3":
+ version: 2.2.1
+ resolution: "jackspeak@npm:2.2.1"
+ dependencies:
+ "@isaacs/cliui": ^8.0.2
+ "@pkgjs/parseargs": ^0.11.0
+ dependenciesMeta:
+ "@pkgjs/parseargs":
+ optional: true
+ checksum: e29291c0d0f280a063fa18fbd1e891ab8c2d7519fd34052c0ebde38538a15c603140d60c2c7f432375ff7ee4c5f1c10daa8b2ae19a97c3d4affe308c8360c1df
+ languageName: node
+ linkType: hard
+
"jest-changed-files@npm:^29.5.0":
version: 29.5.0
resolution: "jest-changed-files@npm:29.5.0"
@@ -6762,7 +7352,7 @@ __metadata:
languageName: node
linkType: hard
-"jest-circus@npm:^29.5.0":
+"jest-circus@npm:25.1.0 - 29, jest-circus@npm:^29.5.0":
version: 29.5.0
resolution: "jest-circus@npm:29.5.0"
dependencies:
@@ -6790,7 +7380,7 @@ __metadata:
languageName: node
linkType: hard
-"jest-cli@npm:25.1.0 - 29, jest-cli@npm:^29.5.0":
+"jest-cli@npm:^29.5.0":
version: 29.5.0
resolution: "jest-cli@npm:29.5.0"
dependencies:
@@ -6910,7 +7500,7 @@ __metadata:
languageName: node
linkType: hard
-"jest-get-type@npm:^29.4.3":
+"jest-get-type@npm:25.1.0 - 29, jest-get-type@npm:^29.4.3":
version: 29.4.3
resolution: "jest-get-type@npm:29.4.3"
checksum: 6ac7f2dde1c65e292e4355b6c63b3a4897d7e92cb4c8afcf6d397f2682f8080e094c8b0b68205a74d269882ec06bf696a9de6cd3e1b7333531e5ed7b112605ce
@@ -6949,6 +7539,7 @@ __metadata:
jest: 25.1.0 - 29
jest-each: 25.1.0 - 29
jest-environment-node: 25.1.0 - 29
+ typescript: ^4.9.5
languageName: unknown
linkType: soft
@@ -6956,16 +7547,25 @@ __metadata:
version: 0.0.0-use.local
resolution: "jest-integration@workspace:packages/jest-plugin/test/integration"
dependencies:
+ "@babel/core": ^7.22.9
+ "@babel/preset-env": ^7.22.9
+ "@babel/preset-typescript": ^7.22.5
+ "@jest/expect-utils": 25.1.0 - 29
"@types/temp": ^0.9.1
"@unflakable/jest-plugin": "workspace:^"
"@unflakable/js-api": "workspace:^"
- cross-env: ^7.0.3
- deep-equal: ^2.0.5
- fetch-mock-jest: ^1.5.1
+ es6-promisify: ^7.0.0
+ escape-string-regexp: ^4.0.0
+ expect: 25.1.0 - 29
jest: 25.1.0 - 29
- jest-cli: 25.1.0 - 29
jest-environment-node: 25.1.0 - 29
- temp: ^0.9.4
+ jest-get-type: 25.1.0 - 29
+ jest-matcher-utils: 25.1.0 - 29
+ mockttp: ^3.9.2
+ semver: ^7.5.4
+ tmp: ^0.2.1
+ typescript: ^4.9.5
+ unflakable-test-common: "workspace:^"
languageName: unknown
linkType: soft
@@ -6979,7 +7579,7 @@ __metadata:
languageName: node
linkType: hard
-"jest-matcher-utils@npm:^29.5.0":
+"jest-matcher-utils@npm:25.1.0 - 29, jest-matcher-utils@npm:^29.5.0":
version: 29.5.0
resolution: "jest-matcher-utils@npm:29.5.0"
dependencies:
@@ -7438,16 +8038,6 @@ __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"
@@ -7518,13 +8108,6 @@ __metadata:
languageName: node
linkType: hard
-"lodash.isequal@npm:^4.5.0":
- version: 4.5.0
- resolution: "lodash.isequal@npm:4.5.0"
- checksum: da27515dc5230eb1140ba65ff8de3613649620e8656b19a6270afe4866b7bd461d9ba2ac8a48dcc57f7adac4ee80e1de9f965d89d4d81a0ad52bb3eec2609644
- languageName: node
- linkType: hard
-
"lodash.memoize@npm:4.x":
version: 4.1.2
resolution: "lodash.memoize@npm:4.1.2"
@@ -7546,14 +8129,7 @@ __metadata:
languageName: node
linkType: hard
-"lodash.sortby@npm:^4.7.0":
- version: 4.7.0
- resolution: "lodash.sortby@npm:4.7.0"
- checksum: db170c9396d29d11fe9a9f25668c4993e0c1331bcb941ddbd48fb76f492e732add7f2a47cfdf8e9d740fa59ac41bbfaf931d268bc72aab3ab49e9f89354d718c
- languageName: node
- linkType: hard
-
-"lodash@npm:^4.16.4, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.21":
+"lodash@npm:^4.16.4, lodash@npm:^4.17.14, lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
@@ -7627,6 +8203,13 @@ __metadata:
languageName: node
linkType: hard
+"lru-cache@npm:^9.1.1":
+ version: 9.1.2
+ resolution: "lru-cache@npm:9.1.2"
+ checksum: d3415634be3908909081fc4c56371a8d562d9081eba70543d86871b978702fffd0e9e362b83921b27a29ae2b37b90f55675aad770a54ac83bb3e4de5049d4b15
+ languageName: node
+ linkType: hard
+
"magic-string@npm:^0.27.0":
version: 0.27.0
resolution: "magic-string@npm:0.27.0"
@@ -7636,6 +8219,15 @@ __metadata:
languageName: node
linkType: hard
+"magic-string@npm:^0.30.0":
+ version: 0.30.0
+ resolution: "magic-string@npm:0.30.0"
+ dependencies:
+ "@jridgewell/sourcemap-codec": ^1.4.13
+ checksum: 7bdf22e27334d8a393858a16f5f840af63a7c05848c000fd714da5aa5eefa09a1bc01d8469362f25cc5c4a14ec01b46557b7fff8751365522acddf21e57c488d
+ languageName: node
+ linkType: hard
+
"make-dir@npm:^3.0.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
@@ -7774,11 +8366,11 @@ __metadata:
linkType: hard
"minimatch@npm:3.0.4":
- version: 3.0.4
- resolution: "minimatch@npm:3.0.4"
+ version: 3.0.8
+ resolution: "minimatch@npm:3.0.8"
dependencies:
brace-expansion: ^1.1.7
- checksum: 66ac295f8a7b59788000ea3749938b0970344c841750abd96694f80269b926ebcafad3deeb3f1da2522978b119e6ae3a5869b63b13a7859a456b3408bd18a078
+ checksum: 850cca179cad715133132693e6963b0db64ab0988c4d211415b087fc23a3e46321e2c5376a01bf5623d8782aba8bdf43c571e2e902e51fdce7175c7215c29f8b
languageName: node
linkType: hard
@@ -7800,10 +8392,12 @@ __metadata:
languageName: node
linkType: hard
-"minimist@npm:0.0.8":
- version: 0.0.8
- resolution: "minimist@npm:0.0.8"
- checksum: 042f8b626b1fa44dffc23bac55771425ac4ee9d267b56f9064c07713e516e1799f3ba933bb628d2475a210caf7dcdb98161611baa1f0daf49309a944cb4bc48f
+"minimatch@npm:^9.0.1":
+ version: 9.0.2
+ resolution: "minimatch@npm:9.0.2"
+ dependencies:
+ brace-expansion: ^2.0.1
+ checksum: 2eb12e2047a062fdb973fb51b9803f2455e3a00977858c038d66646d303a5a15bbcbc6ed5a2fc403bc869b1309f829ed3acd881d3246faf044ea7a494974b924
languageName: node
linkType: hard
@@ -7874,6 +8468,13 @@ __metadata:
languageName: node
linkType: hard
+"minipass@npm:^5.0.0 || ^6.0.2":
+ version: 6.0.2
+ resolution: "minipass@npm:6.0.2"
+ checksum: d140b91f4ab2e5ce5a9b6c468c0e82223504acc89114c1a120d4495188b81fedf8cade72a9f4793642b4e66672f990f1e0d902dd858485216a07cd3c8a62fac9
+ languageName: node
+ linkType: hard
+
"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2":
version: 2.1.2
resolution: "minizlib@npm:2.1.2"
@@ -7884,18 +8485,7 @@ __metadata:
languageName: node
linkType: hard
-"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":
+"mkdirp@npm:0.5.1, mkdirp@npm:^0.5.5":
version: 0.5.6
resolution: "mkdirp@npm:0.5.6"
dependencies:
@@ -7965,9 +8555,9 @@ __metadata:
languageName: node
linkType: hard
-"mockttp@npm:^3.7.5":
- version: 3.7.5
- resolution: "mockttp@npm:3.7.5"
+"mockttp@npm:^3.9.2":
+ version: 3.9.2
+ resolution: "mockttp@npm:3.9.2"
dependencies:
"@graphql-tools/schema": ^8.5.0
"@graphql-tools/utils": ^8.8.0
@@ -7998,19 +8588,19 @@ __metadata:
lru-cache: ^7.14.0
native-duplexpair: ^1.0.0
node-forge: ^1.2.1
- pac-proxy-agent: ^5.0.0
+ pac-proxy-agent: ^7.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
+ semver: ^7.5.3
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
+ checksum: aa11f01ef7765b96a8ad135f97eddd72e548e626bab4c9fcef7bf52633b1f49e5e4cd2f763356d4ae1ea8bb7f5f4d52110681e46913fc5800b0713a99c273316
languageName: node
linkType: hard
@@ -8142,10 +8732,10 @@ __metadata:
languageName: node
linkType: hard
-"node-releases@npm:^2.0.8":
- version: 2.0.10
- resolution: "node-releases@npm:2.0.10"
- checksum: d784ecde25696a15d449c4433077f5cce620ed30a1656c4abf31282bfc691a70d9618bae6868d247a67914d1be5cc4fde22f65a05f4398cdfb92e0fc83cadfbc
+"node-releases@npm:^2.0.12":
+ version: 2.0.13
+ resolution: "node-releases@npm:2.0.13"
+ checksum: 17ec8f315dba62710cae71a8dad3cd0288ba943d2ece43504b3b1aa8625bf138637798ab470b1d9035b0545996f63000a8a926e0f6d35d0996424f8b6d36dda3
languageName: node
linkType: hard
@@ -8314,20 +8904,6 @@ __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"
@@ -8410,31 +8986,30 @@ __metadata:
languageName: node
linkType: hard
-"pac-proxy-agent@npm:^5.0.0":
- version: 5.0.0
- resolution: "pac-proxy-agent@npm:5.0.0"
+"pac-proxy-agent@npm:^7.0.0":
+ version: 7.0.1
+ resolution: "pac-proxy-agent@npm:7.0.1"
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
+ "@tootallnate/quickjs-emscripten": ^0.23.0
+ agent-base: ^7.0.2
+ debug: ^4.3.4
+ get-uri: ^6.0.1
+ http-proxy-agent: ^7.0.0
+ https-proxy-agent: ^7.0.2
+ pac-resolver: ^7.0.0
+ socks-proxy-agent: ^8.0.2
+ checksum: 3d4aa48ec1c19db10158ecc1c4c9a9f77792294412d225ceb3dfa45d5a06950dca9755e2db0d9b69f12769119bea0adf2b24390d9c73c8d81df75e28245ae451
languageName: node
linkType: hard
-"pac-resolver@npm:^5.0.0":
- version: 5.0.1
- resolution: "pac-resolver@npm:5.0.1"
+"pac-resolver@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "pac-resolver@npm:7.0.0"
dependencies:
- degenerator: ^3.0.2
- ip: ^1.1.5
+ degenerator: ^5.0.0
+ ip: ^1.1.8
netmask: ^2.0.2
- checksum: e3bd8aada70d173cd4cec1ac810fb56161678b7a597060a740c4a31d9c5f8cd95687b2d0fd90b69c0cafe5ef787404074f38042ba08c8d378fed48973f58e493
+ checksum: fa3a898c09848e93e35f5e23443fea36ddb393a851c76a23664a5bf3fcbe58ff77a0bcdae1e4f01b9ea87ea493c52e14d97a0fe39f92474d14cd45559c6e3cde
languageName: node
linkType: hard
@@ -8515,6 +9090,16 @@ __metadata:
languageName: node
linkType: hard
+"path-scurry@npm:^1.7.0":
+ version: 1.9.2
+ resolution: "path-scurry@npm:1.9.2"
+ dependencies:
+ lru-cache: ^9.1.1
+ minipass: ^5.0.0 || ^6.0.2
+ checksum: 92888dfb68e285043c6d3291c8e971d5d2bc2f5082f4d7b5392896f34be47024c9d0a8b688dd7ae6d125acc424699195474927cb4f00049a9b1ec7c4256fa8e0
+ languageName: node
+ linkType: hard
+
"path-to-regexp@npm:0.1.7":
version: 0.1.7
resolution: "path-to-regexp@npm:0.1.7"
@@ -8522,13 +9107,6 @@ __metadata:
languageName: node
linkType: hard
-"path-to-regexp@npm:^2.2.1":
- version: 2.4.0
- resolution: "path-to-regexp@npm:2.4.0"
- checksum: 581175bf2968e51452f2b8c71f10e75c995693668b4ecf7d0b48962fbe0c56830661ca5dd5fd6d8e2f0cc9a045ce07e89af504ab133e1d21887c2712df85b1f4
- languageName: node
- linkType: hard
-
"path-type@npm:^4.0.0":
version: 4.0.0
resolution: "path-type@npm:4.0.0"
@@ -8612,13 +9190,6 @@ __metadata:
languageName: node
linkType: hard
-"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"
@@ -8704,7 +9275,7 @@ __metadata:
languageName: node
linkType: hard
-"psl@npm:^1.1.28":
+"psl@npm:^1.1.33":
version: 1.9.0
resolution: "psl@npm:1.9.0"
checksum: 20c4277f640c93d393130673f392618e9a8044c6c7bf61c53917a0fddb4952790f5f362c6c730a9c32b124813e173733f9895add8d26f566ed0ea0654b2e711d
@@ -8753,10 +9324,10 @@ __metadata:
languageName: node
linkType: hard
-"querystring@npm:^0.2.0":
- version: 0.2.1
- resolution: "querystring@npm:0.2.1"
- checksum: 7b83b45d641e75fd39cd6625ddfd44e7618e741c61e95281b57bbae8fde0afcc12cf851924559e5cc1ef9baa3b1e06e22b164ea1397d65dd94b801f678d9c8ce
+"querystringify@npm:^2.1.1":
+ version: 2.2.0
+ resolution: "querystringify@npm:2.2.0"
+ checksum: 5641ea231bad7ef6d64d9998faca95611ed4b11c2591a8cae741e178a974f6a8e0ebde008475259abe1621cb15e692404e6b6626e927f7b849d5c09392604b15
languageName: node
linkType: hard
@@ -8802,7 +9373,7 @@ __metadata:
languageName: node
linkType: hard
-"raw-body@npm:2.5.2, raw-body@npm:^2.2.0, raw-body@npm:^2.4.1":
+"raw-body@npm:2.5.2, raw-body@npm:^2.4.1":
version: 2.5.2
resolution: "raw-body@npm:2.5.2"
dependencies:
@@ -8851,18 +9422,6 @@ __metadata:
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"
@@ -8914,10 +9473,10 @@ __metadata:
languageName: node
linkType: hard
-"regenerator-runtime@npm:^0.13.4":
- version: 0.13.9
- resolution: "regenerator-runtime@npm:0.13.9"
- checksum: 65ed455fe5afd799e2897baf691ca21c2772e1a969d19bb0c4695757c2d96249eb74ee3553ea34a91062b2a676beedf630b4c1551cc6299afb937be1426ec55e
+"regenerator-runtime@npm:^0.13.11":
+ version: 0.13.11
+ resolution: "regenerator-runtime@npm:0.13.11"
+ checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4
languageName: node
linkType: hard
@@ -8989,6 +9548,13 @@ __metadata:
languageName: node
linkType: hard
+"requires-port@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "requires-port@npm:1.0.0"
+ checksum: eee0e303adffb69be55d1a214e415cf42b7441ae858c76dfc5353148644f6fd6e698926fc4643f510d5c126d12a705e7c8ed7e38061113bdf37547ab356797ff
+ languageName: node
+ linkType: hard
+
"resolve-alpn@npm:^1.2.0":
version: 1.2.1
resolution: "resolve-alpn@npm:1.2.1"
@@ -9027,28 +9593,28 @@ __metadata:
linkType: hard
"resolve@npm:^1.14.2, resolve@npm:^1.20.0, resolve@npm:^1.22.1":
- version: 1.22.1
- resolution: "resolve@npm:1.22.1"
+ version: 1.22.3
+ resolution: "resolve@npm:1.22.3"
dependencies:
- is-core-module: ^2.9.0
+ is-core-module: ^2.12.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
- checksum: 07af5fc1e81aa1d866cbc9e9460fbb67318a10fa3c4deadc35c3ad8a898ee9a71a86a65e4755ac3195e0ea0cfbe201eb323ebe655ce90526fd61917313a34e4e
+ checksum: fb834b81348428cb545ff1b828a72ea28feb5a97c026a1cf40aa1008352c72811ff4d4e71f2035273dc536dcfcae20c13604ba6283c612d70fa0b6e44519c374
languageName: node
linkType: hard
"resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin":
- version: 1.22.1
- resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d"
+ version: 1.22.3
+ resolution: "resolve@patch:resolve@npm%3A1.22.3#~builtin::version=1.22.3&hash=c3c19d"
dependencies:
- is-core-module: ^2.9.0
+ is-core-module: ^2.12.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
- checksum: 5656f4d0bedcf8eb52685c1abdf8fbe73a1603bb1160a24d716e27a57f6cecbe2432ff9c89c2bd57542c3a7b9d14b1882b73bfe2e9d7849c9a4c0b8b39f02b8b
+ checksum: ad59734723b596d0891321c951592ed9015a77ce84907f89c9d9307dd0c06e11a67906a3e628c4cae143d3e44898603478af0ddeb2bba3f229a9373efe342665
languageName: node
linkType: hard
@@ -9101,14 +9667,30 @@ __metadata:
languageName: node
linkType: hard
-"rimraf@npm:~2.6.2":
- version: 2.6.3
- resolution: "rimraf@npm:2.6.3"
+"rimraf@npm:^5.0.1":
+ version: 5.0.1
+ resolution: "rimraf@npm:5.0.1"
dependencies:
- glob: ^7.1.3
+ glob: ^10.2.5
bin:
- rimraf: ./bin.js
- checksum: 3ea587b981a19016297edb96d1ffe48af7e6af69660e3b371dbfc73722a73a0b0e9be5c88089fbeeb866c389c1098e07f64929c7414290504b855f54f901ab10
+ rimraf: dist/cjs/src/bin.js
+ checksum: bafce85391349a2d960847980bf9b5caa2a8887f481af630f1ea27e08288217293cec72d75e9a2ba35495c212789f66a7f3d23366ba6197026ab71c535126857
+ languageName: node
+ linkType: hard
+
+"rollup-plugin-dts@npm:^5.3.0":
+ version: 5.3.0
+ resolution: "rollup-plugin-dts@npm:5.3.0"
+ dependencies:
+ "@babel/code-frame": ^7.18.6
+ magic-string: ^0.30.0
+ peerDependencies:
+ rollup: ^3.0.0
+ typescript: ^4.1 || ^5.0
+ dependenciesMeta:
+ "@babel/code-frame":
+ optional: true
+ checksum: ba3a6065598586c52af60211877a234b8c829b8a7ecd6e6b426e52ec4dc2c8c8ac6e1fb9f47db6afd91858be0dcb09fd3d312c865e972fb72ae8c9ee00eb1d36
languageName: node
linkType: hard
@@ -9130,9 +9712,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
- "@babel/core": ^7.21.5
- "@babel/preset-env": ^7.21.5
- "@babel/preset-typescript": ^7.21.5
"@types/debug": ^4.1.7
"@types/js-yaml": ^4.0.5
"@types/node": ^14.18.43
@@ -9235,32 +9814,32 @@ __metadata:
languageName: node
linkType: hard
-"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:
- lru-cache: ^6.0.0
+"semver@npm:^5.7.0":
+ version: 5.7.2
+ resolution: "semver@npm:5.7.2"
bin:
- semver: bin/semver.js
- checksum: d16dbedad53c65b086f79524b9ef766bf38670b2395bdad5c957f824dcc566b624988013564f4812bcace3f9d405355c3635e2007396a39d1bffc71cfec4a2fc
+ semver: bin/semver
+ checksum: fb4ab5e0dd1c22ce0c937ea390b4a822147a9c53dbd2a9a0132f12fe382902beef4fbf12cf51bb955248d8d15874ce8cd89532569756384f994309825f10b686
languageName: node
linkType: hard
-"semver@npm:^5.7.0, semver@npm:^5.7.1":
- version: 5.7.1
- resolution: "semver@npm:5.7.1"
+"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1":
+ version: 6.3.1
+ resolution: "semver@npm:6.3.1"
bin:
- semver: ./bin/semver
- checksum: 57fd0acfd0bac382ee87cd52cd0aaa5af086a7dc8d60379dfe65fea491fb2489b6016400813930ecd61fd0952dae75c115287a1b16c234b1550887117744dfaf
+ semver: bin/semver.js
+ checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2
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"
+"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, semver@npm:^7.5.3, semver@npm:^7.5.4":
+ version: 7.5.4
+ resolution: "semver@npm:7.5.4"
+ dependencies:
+ lru-cache: ^6.0.0
bin:
- semver: ./bin/semver.js
- checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9
+ semver: bin/semver.js
+ checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3
languageName: node
linkType: hard
@@ -9354,6 +9933,13 @@ __metadata:
languageName: node
linkType: hard
+"signal-exit@npm:^4.0.1":
+ version: 4.0.2
+ resolution: "signal-exit@npm:4.0.2"
+ checksum: 41f5928431cc6e91087bf0343db786a6313dd7c6fd7e551dbc141c95bb5fb26663444fd9df8ea47c5d7fc202f60aa7468c3162a9365cbb0615fc5e1b1328fe31
+ languageName: node
+ linkType: hard
+
"simple-git@npm:^3.16.0":
version: 3.17.0
resolution: "simple-git@npm:3.17.0"
@@ -9415,17 +10001,6 @@ __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"
@@ -9448,7 +10023,18 @@ __metadata:
languageName: node
linkType: hard
-"socks@npm:^2.3.3, socks@npm:^2.6.1, socks@npm:^2.6.2":
+"socks-proxy-agent@npm:^8.0.2":
+ version: 8.0.2
+ resolution: "socks-proxy-agent@npm:8.0.2"
+ dependencies:
+ agent-base: ^7.0.2
+ debug: ^4.3.4
+ socks: ^2.7.1
+ checksum: 4fb165df08f1f380881dcd887b3cdfdc1aba3797c76c1e9f51d29048be6e494c5b06d68e7aea2e23df4572428f27a3ec22b3d7c75c570c5346507433899a4b6d
+ languageName: node
+ linkType: hard
+
+"socks@npm:^2.6.1, socks@npm:^2.6.2, socks@npm:^2.7.1":
version: 2.7.1
resolution: "socks@npm:2.7.1"
dependencies:
@@ -9562,7 +10148,7 @@ __metadata:
languageName: node
linkType: hard
-"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":
+"string-width-cjs@npm:string-width@^4.2.0, 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:
@@ -9594,6 +10180,17 @@ __metadata:
languageName: node
linkType: hard
+"string-width@npm:^5.0.1, string-width@npm:^5.1.2":
+ version: 5.1.2
+ resolution: "string-width@npm:5.1.2"
+ dependencies:
+ eastasianwidth: ^0.2.0
+ emoji-regex: ^9.2.2
+ strip-ansi: ^7.0.1
+ checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193
+ languageName: node
+ linkType: hard
+
"string.prototype.trim@npm:^1.2.7":
version: 1.2.7
resolution: "string.prototype.trim@npm:1.2.7"
@@ -9636,13 +10233,6 @@ __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"
@@ -9652,6 +10242,15 @@ __metadata:
languageName: node
linkType: hard
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "strip-ansi@npm:6.0.1"
+ dependencies:
+ ansi-regex: ^5.0.1
+ checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c
+ languageName: node
+ linkType: hard
+
"strip-ansi@npm:^4.0.0":
version: 4.0.0
resolution: "strip-ansi@npm:4.0.0"
@@ -9670,12 +10269,12 @@ __metadata:
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"
+"strip-ansi@npm:^7.0.1":
+ version: 7.1.0
+ resolution: "strip-ansi@npm:7.1.0"
dependencies:
- ansi-regex: ^5.0.1
- checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c
+ ansi-regex: ^6.0.1
+ checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d
languageName: node
linkType: hard
@@ -9795,16 +10394,6 @@ __metadata:
languageName: node
linkType: hard
-"temp@npm:^0.9.4":
- version: 0.9.4
- resolution: "temp@npm:0.9.4"
- dependencies:
- mkdirp: ^0.5.1
- rimraf: ~2.6.2
- checksum: 8709d4d63278bd309ca0e49e80a268308dea543a949e71acd427b3314cd9417da9a2cc73425dd9c21c6780334dbffd67e05e7be5aaa73e9affe8479afc6f20e3
- languageName: node
- linkType: hard
-
"term-size@npm:2.1.0":
version: 2.1.0
resolution: "term-size@npm:2.1.0"
@@ -9890,7 +10479,7 @@ __metadata:
languageName: node
linkType: hard
-"tmp@npm:~0.2.1":
+"tmp@npm:^0.2.1, tmp@npm:~0.2.1":
version: 0.2.1
resolution: "tmp@npm:0.2.1"
dependencies:
@@ -9936,22 +10525,15 @@ __metadata:
languageName: node
linkType: hard
-"tough-cookie@npm:~2.5.0":
- version: 2.5.0
- resolution: "tough-cookie@npm:2.5.0"
+"tough-cookie@npm:^4.1.3":
+ version: 4.1.3
+ resolution: "tough-cookie@npm:4.1.3"
dependencies:
- psl: ^1.1.28
+ psl: ^1.1.33
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"
- dependencies:
- punycode: ^2.1.0
- checksum: 96d4ed46bc161db75dbf9247a236ea0bfcaf5758baae6749e92afab0bc5a09cb59af21788ede7e55080f2bf02dce3e4a8f2a484cc45164e29f4b5e68f7cbcc1a
+ universalify: ^0.2.0
+ url-parse: ^1.5.3
+ checksum: c9226afff36492a52118432611af083d1d8493a53ff41ec4ea48e5b583aec744b989e4280bcf476c910ec1525a89a4a0f1cae81c08b18fb2ec3a9b3a72b91dcc
languageName: node
linkType: hard
@@ -9972,8 +10554,8 @@ __metadata:
linkType: hard
"ts-jest@npm:^29.1.0":
- version: 29.1.0
- resolution: "ts-jest@npm:29.1.0"
+ version: 29.1.1
+ resolution: "ts-jest@npm:29.1.1"
dependencies:
bs-logger: 0.x
fast-json-stable-stringify: 2.x
@@ -9981,7 +10563,7 @@ __metadata:
json5: ^2.2.3
lodash.memoize: 4.x
make-error: 1.x
- semver: 7.x
+ semver: ^7.5.3
yargs-parser: ^21.0.1
peerDependencies:
"@babel/core": ">=7.0.0-beta.0 <8"
@@ -10000,7 +10582,7 @@ __metadata:
optional: true
bin:
ts-jest: cli.js
- checksum: 535dc42ad523cbe1e387701fb2e448518419b515c082f09b25411f0b3dd0b854cf3e8141c316d6f4b99883aeb4a4f94159cbb1edfb06d7f77ea6229fadb2e1bf
+ checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402
languageName: node
linkType: hard
@@ -10119,15 +10701,6 @@ __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"
@@ -10209,6 +10782,29 @@ __metadata:
languageName: node
linkType: hard
+"unflakable-test-common@workspace:^, unflakable-test-common@workspace:packages/test-common":
+ version: 0.0.0-use.local
+ resolution: "unflakable-test-common@workspace:packages/test-common"
+ dependencies:
+ "@rollup/plugin-commonjs": ^24.1.0
+ "@rollup/plugin-node-resolve": ^15.0.2
+ "@rollup/plugin-typescript": ^11.1.1
+ "@types/jest": 25.1.0 - 29
+ "@unflakable/plugins-common": "workspace:^"
+ cosmiconfig: ^7.0.1
+ debug: ^4.3.3
+ deep-equal: ^2.0.5
+ expect: 25.1.0 - 29
+ mockttp: ^3.9.2
+ rimraf: ^5.0.1
+ rollup: ^3.21.1
+ rollup-plugin-dts: ^5.3.0
+ simple-git: ^3.16.0
+ tree-kill: ^1.2.2
+ typescript: ^4.9.5
+ languageName: unknown
+ linkType: soft
+
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
version: 2.0.0
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0"
@@ -10234,9 +10830,9 @@ __metadata:
linkType: hard
"unicode-property-aliases-ecmascript@npm:^2.0.0":
- version: 2.0.0
- resolution: "unicode-property-aliases-ecmascript@npm:2.0.0"
- checksum: dda4d39128cbbede2ac60fbb85493d979ec65913b8a486bf7cb7a375a2346fa48cbf9dc6f1ae23376e7e8e684c2b411434891e151e865a661b40a85407db51d0
+ version: 2.1.0
+ resolution: "unicode-property-aliases-ecmascript@npm:2.1.0"
+ checksum: 243524431893649b62cc674d877bd64ef292d6071dd2fd01ab4d5ad26efbc104ffcd064f93f8a06b7e4ec54c172bf03f6417921a0d8c3a9994161fe1f88f815b
languageName: node
linkType: hard
@@ -10265,6 +10861,13 @@ __metadata:
languageName: node
linkType: hard
+"universalify@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "universalify@npm:0.2.0"
+ checksum: e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5
+ languageName: node
+ linkType: hard
+
"universalify@npm:^2.0.0":
version: 2.0.0
resolution: "universalify@npm:2.0.0"
@@ -10286,17 +10889,17 @@ __metadata:
languageName: node
linkType: hard
-"update-browserslist-db@npm:^1.0.10":
- version: 1.0.10
- resolution: "update-browserslist-db@npm:1.0.10"
+"update-browserslist-db@npm:^1.0.11":
+ version: 1.0.11
+ resolution: "update-browserslist-db@npm:1.0.11"
dependencies:
escalade: ^3.1.1
picocolors: ^1.0.0
peerDependencies:
browserslist: ">= 4.21.0"
bin:
- browserslist-lint: cli.js
- checksum: 12db73b4f63029ac407b153732e7cd69a1ea8206c9100b482b7d12859cd3cd0bc59c602d7ae31e652706189f1acb90d42c53ab24a5ba563ed13aebdddc5561a0
+ update-browserslist-db: cli.js
+ checksum: b98327518f9a345c7cad5437afae4d2ae7d865f9779554baf2a200fdf4bac4969076b679b1115434bd6557376bdd37ca7583d0f9b8f8e302d7d4cc1e91b5f231
languageName: node
linkType: hard
@@ -10309,6 +10912,16 @@ __metadata:
languageName: node
linkType: hard
+"url-parse@npm:^1.5.3":
+ version: 1.5.10
+ resolution: "url-parse@npm:1.5.10"
+ dependencies:
+ querystringify: ^2.1.1
+ requires-port: ^1.0.0
+ checksum: fbdba6b1d83336aca2216bbdc38ba658d9cfb8fc7f665eb8b17852de638ff7d1a162c198a8e4ed66001ddbf6c9888d41e4798912c62b4fd777a31657989f7bdf
+ languageName: node
+ linkType: hard
+
"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
@@ -10375,18 +10988,6 @@ __metadata:
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"
@@ -10413,13 +11014,6 @@ __metadata:
languageName: node
linkType: hard
-"webidl-conversions@npm:^4.0.2":
- version: 4.0.2
- resolution: "webidl-conversions@npm:4.0.2"
- checksum: c93d8dfe908a0140a4ae9c0ebc87a33805b416a33ee638a605b551523eec94a9632165e54632f6d57a39c5f948c4bab10e0e066525e9a4b87a79f0d04fbca374
- languageName: node
- linkType: hard
-
"webpack-sources@npm:^3.2.3":
version: 3.2.3
resolution: "webpack-sources@npm:3.2.3"
@@ -10474,17 +11068,6 @@ __metadata:
languageName: node
linkType: hard
-"whatwg-url@npm:^6.5.0":
- version: 6.5.0
- resolution: "whatwg-url@npm:6.5.0"
- dependencies:
- lodash.sortby: ^4.7.0
- tr46: ^1.0.1
- webidl-conversions: ^4.0.2
- checksum: a10bd5e29f4382cd19789c2a7bbce25416e606b6fefc241c7fe34a2449de5bc5709c165bd13634eda433942d917ca7386a52841780b82dc37afa8141c31a8ebd
- languageName: node
- linkType: hard
-
"which-boxed-primitive@npm:^1.0.1, which-boxed-primitive@npm:^1.0.2":
version: 1.0.2
resolution: "which-boxed-primitive@npm:1.0.2"
@@ -10580,10 +11163,21 @@ __metadata:
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
+"word-wrap@npm:^1.2.3":
+ version: 1.2.5
+ resolution: "word-wrap@npm:1.2.5"
+ checksum: f93ba3586fc181f94afdaff3a6fef27920b4b6d9eaefed0f428f8e07adea2a7f54a5f2830ce59406c8416f033f86902b91eb824072354645eea687dff3691ccb
+ languageName: node
+ linkType: hard
+
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "wrap-ansi@npm:7.0.0"
+ dependencies:
+ ansi-styles: ^4.0.0
+ string-width: ^4.1.0
+ strip-ansi: ^6.0.0
+ checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b
languageName: node
linkType: hard
@@ -10609,14 +11203,14 @@ __metadata:
languageName: node
linkType: hard
-"wrap-ansi@npm:^7.0.0":
- version: 7.0.0
- resolution: "wrap-ansi@npm:7.0.0"
+"wrap-ansi@npm:^8.1.0":
+ version: 8.1.0
+ resolution: "wrap-ansi@npm:8.1.0"
dependencies:
- ansi-styles: ^4.0.0
- string-width: ^4.1.0
- strip-ansi: ^6.0.0
- checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b
+ ansi-styles: ^6.1.0
+ string-width: ^5.0.1
+ strip-ansi: ^7.0.1
+ checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238
languageName: node
linkType: hard
@@ -10659,13 +11253,6 @@ __metadata:
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"
@@ -10708,23 +11295,23 @@ __metadata:
languageName: node
linkType: hard
-"yargs-parser@npm:13.1.1":
- version: 13.1.1
- resolution: "yargs-parser@npm:13.1.1"
+"yargs-parser@npm:13.1.1, yargs-parser@npm:^13.1.1":
+ version: 13.1.2
+ resolution: "yargs-parser@npm:13.1.2"
dependencies:
camelcase: ^5.0.0
decamelize: ^1.2.0
- checksum: fa5fd27736aa423dc9a114d160dae94625f7faf19c252b8c91ac0197be9715d1dbc9b98fda893f75f182111fb6c3c0ce60c631b73859dd1a06bec07cddfb98f4
+ checksum: c8bb6f44d39a4acd94462e96d4e85469df865de6f4326e0ab1ac23ae4a835e5dd2ddfe588317ebf80c3a7e37e741bd5cb0dc8d92bcc5812baefb7df7c885e86b
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"
+"yargs-parser@npm:^15.0.1":
+ version: 15.0.3
+ resolution: "yargs-parser@npm:15.0.3"
dependencies:
camelcase: ^5.0.0
decamelize: ^1.2.0
- checksum: c8bb6f44d39a4acd94462e96d4e85469df865de6f4326e0ab1ac23ae4a835e5dd2ddfe588317ebf80c3a7e37e741bd5cb0dc8d92bcc5812baefb7df7c885e86b
+ checksum: 06611c1893fa9f1c25ae79df3c6e2edbac7c8d75257a4b55b8432cbc87ee03eda86bea0537f65b4b8a0d9684c83fa6e9ef61ef720a1e5cc8a9aa6893b54ee4c3
languageName: node
linkType: hard
@@ -10736,13 +11323,15 @@ __metadata:
linkType: hard
"yargs-unparser@npm:1.6.0":
- version: 1.6.0
- resolution: "yargs-unparser@npm:1.6.0"
+ version: 1.6.4
+ resolution: "yargs-unparser@npm:1.6.4"
dependencies:
- flat: ^4.1.0
- lodash: ^4.17.15
- yargs: ^13.3.0
- checksum: ca662bb94af53d816d47f2162f0a1d135783f09de9fd47645a5cb18dd25532b0b710432b680d2c065ff45de122ba4a96433c41595fa7bfcc08eb12e889db95c1
+ camelcase: ^5.3.1
+ decamelize: ^1.2.0
+ flat: ^5.0.2
+ is-plain-obj: ^1.1.0
+ yargs: ^14.2.3
+ checksum: 428695924f6dc3b660cab37e5f1bb46a7bc64bb81e583beaaf40155f2d33440e3776518528e98902d256ed68fe4cc74c54c0188481ce8dba6857bc1656b5ca06
languageName: node
linkType: hard
@@ -10764,11 +11353,12 @@ __metadata:
languageName: node
linkType: hard
-"yargs@npm:^13.3.0":
- version: 13.3.2
- resolution: "yargs@npm:13.3.2"
+"yargs@npm:^14.2.3":
+ version: 14.2.3
+ resolution: "yargs@npm:14.2.3"
dependencies:
cliui: ^5.0.0
+ decamelize: ^1.2.0
find-up: ^3.0.0
get-caller-file: ^2.0.1
require-directory: ^2.1.1
@@ -10777,8 +11367,8 @@ __metadata:
string-width: ^3.0.0
which-module: ^2.0.0
y18n: ^4.0.0
- yargs-parser: ^13.1.2
- checksum: 75c13e837eb2bb25717957ba58d277e864efc0cca7f945c98bdf6477e6ec2f9be6afa9ed8a876b251a21423500c148d7b91e88dee7adea6029bdec97af1ef3e8
+ yargs-parser: ^15.0.1
+ checksum: 684fcb1896e6c873c31c09c5c16445d6253dfe505aa879cff56d49425f5bca44f2ab8d7a1c949f3b932ae8654128425e89770e5e2f2c3d816e5816b9eb6efb6f
languageName: node
linkType: hard
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy