diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index ae1757ea8c..ba0e6f36de 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -47,3 +47,20 @@ jobs: name: ${{ matrix.browser }}-${{ matrix.os }}-test-results path: test-results + test_test_runner: + name: Test Runner + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - run: npm ci + env: + DEBUG: pw:install + - run: npm run build + - run: npm run ttest diff --git a/install-from-github.js b/install-from-github.js index 684aa45918..4d5ec2df59 100644 --- a/install-from-github.js +++ b/install-from-github.js @@ -18,6 +18,17 @@ // This file is only run when someone installs via the github repo const {execSync} = require('child_process'); +const path = require('path'); + +console.log(`Updating test runner...`); +try { + execSync('npm ci --save=false --fund=false --audit=false', { + stdio: ['inherit', 'inherit', 'inherit'], + cwd: path.join(__dirname, 'tests', 'config', 'test-runner'), + }); +} catch (e) { + process.exit(1); +} console.log(`Rebuilding installer...`); try { diff --git a/package-lock.json b/package-lock.json index 97b8eeb043..fdd8e58250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,41 @@ "version": "1.12.0-next", "license": "Apache-2.0", "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/core": "^7.14.0", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-dynamic-import": "^7.13.8", + "@babel/plugin-proposal-export-namespace-from": "^7.12.13", + "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-numeric-separator": "^7.12.13", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-private-property-in-object": "^7.14.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.14.0", + "@babel/preset-typescript": "^7.13.0", + "colors": "^1.4.0", "commander": "^6.1.0", "debug": "^4.1.1", + "expect": "^26.4.2", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.0", "jpeg-js": "^0.4.2", "mime": "^2.4.6", + "minimatch": "^3.0.3", + "ms": "^2.1.2", + "pirates": "^4.0.1", + "pixelmatch": "^5.2.1", "pngjs": "^5.0.0", "progress": "^2.0.3", "proper-lockfile": "^4.1.1", "proxy-from-env": "^1.1.0", "rimraf": "^3.0.2", + "source-map-support": "^0.4.18", "stack-utils": "^2.0.3", "ws": "^7.4.6", "yazl": "^2.5.1" @@ -28,10 +52,14 @@ "playwright": "lib/cli/cli.js" }, "devDependencies": { + "@types/babel__code-frame": "^7.0.2", + "@types/babel__core": "^7.1.14", "@types/debug": "^4.1.5", "@types/extract-zip": "^1.6.2", "@types/mime": "^2.0.3", + "@types/minimatch": "^3.0.3", "@types/node": "^10.17.28", + "@types/pixelmatch": "^5.2.1", "@types/pngjs": "^3.4.2", "@types/progress": "^2.0.3", "@types/proper-lockfile": "^4.1.1", @@ -40,8 +68,10 @@ "@types/react-dom": "^17.0.0", "@types/resize-observer-browser": "^0.1.4", "@types/rimraf": "^3.0.0", + "@types/source-map-support": "^0.4.2", "@types/webpack": "^4.41.25", "@types/ws": "7.2.6", + "@types/xml2js": "^0.4.5", "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", @@ -55,7 +85,6 @@ "eslint-plugin-notice": "^0.9.10", "eslint-plugin-react-hooks": "^4.2.0", "file-loader": "^6.1.0", - "folio": "=0.4.0-alpha28", "formidable": "^1.2.2", "html-webpack-plugin": "^4.4.1", "ncp": "^2.0.0", @@ -65,9 +94,10 @@ "socksv5": "0.0.6", "style-loader": "^1.2.1", "ts-loader": "^8.0.3", - "typescript": "^4.0.2", + "typescript": "=4.2.4", "webpack": "^4.44.2", "webpack-cli": "^3.3.12", + "xml2js": "^0.4.23", "yaml": "^1.10.0" }, "engines": { @@ -75,25 +105,22 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "dependencies": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.12.13" } }, "node_modules/@babel/compat-data": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz", - "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==", - "dev": true + "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==" }, "node_modules/@babel/core": { "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.14.3", @@ -115,20 +142,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.12.13" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -137,7 +154,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -146,7 +162,6 @@ "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", - "dev": true, "dependencies": { "@babel/types": "^7.14.2", "jsesc": "^2.5.1", @@ -157,7 +172,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -166,7 +180,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", - "dev": true, "dependencies": { "@babel/types": "^7.12.13" } @@ -175,7 +188,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz", "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.14.4", "@babel/helper-validator-option": "^7.12.17", @@ -187,7 +199,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -196,7 +207,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz", "integrity": "sha512-idr3pthFlDCpV+p/rMgGLGYIVtazeatrSOQk8YzO2pAepIjQhCN3myeihVg58ax2bbbGK9PUE1reFi7axOYIOw==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.12.13", "@babel/helper-function-name": "^7.14.2", @@ -210,7 +220,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz", "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==", - "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.12.13", "@babel/template": "^7.12.13", @@ -221,7 +230,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", - "dev": true, "dependencies": { "@babel/types": "^7.12.13" } @@ -230,7 +238,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, "dependencies": { "@babel/types": "^7.13.12" } @@ -239,7 +246,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", - "dev": true, "dependencies": { "@babel/types": "^7.13.12" } @@ -248,7 +254,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-replace-supers": "^7.13.12", @@ -264,7 +269,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", - "dev": true, "dependencies": { "@babel/types": "^7.12.13" } @@ -272,14 +276,12 @@ "node_modules/@babel/helper-plugin-utils": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==" }, "node_modules/@babel/helper-replace-supers": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz", "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==", - "dev": true, "dependencies": { "@babel/helper-member-expression-to-functions": "^7.13.12", "@babel/helper-optimise-call-expression": "^7.12.13", @@ -291,7 +293,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", - "dev": true, "dependencies": { "@babel/types": "^7.13.12" } @@ -300,7 +301,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", - "dev": true, "dependencies": { "@babel/types": "^7.12.1" } @@ -309,7 +309,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", - "dev": true, "dependencies": { "@babel/types": "^7.12.13" } @@ -317,20 +316,17 @@ "node_modules/@babel/helper-validator-identifier": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==" }, "node_modules/@babel/helper-validator-option": { "version": "7.12.17", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", - "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", - "dev": true + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" }, "node_modules/@babel/helpers": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", - "dev": true, "dependencies": { "@babel/template": "^7.12.13", "@babel/traverse": "^7.14.0", @@ -341,7 +337,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", @@ -352,7 +347,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz", "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -364,7 +358,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", - "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" @@ -374,7 +367,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.2.tgz", "integrity": "sha512-oxVQZIWFh91vuNEMKltqNsKLFWkOIyJc95k2Gv9lWVyDfPUQGSSlbDEgWuJUU1afGE9WwlzpucMZ3yDRHIItkA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -384,7 +376,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.2.tgz", "integrity": "sha512-sRxW3z3Zp3pFfLAgVEvzTFutTXax837oOatUIvSG9o5gRj9mKwm3br1Se5f4QalTQs9x4AzlA/HrCWbQIHASUQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -394,7 +385,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.2.tgz", "integrity": "sha512-1JAZtUrqYyGsS7IDmFeaem+/LJqujfLZ2weLR9ugB0ufUPjzf8cguyVT1g5im7f7RXxuLq1xUxEzvm68uYRtGg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -404,7 +394,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz", "integrity": "sha512-ebR0zU9OvI2N4qiAC38KIAK75KItpIPTpAtd2r4OZmMFeKbKJpUFLYP2EuDut82+BmYi8sz42B+TfTptJ9iG5Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -414,7 +403,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.2.tgz", "integrity": "sha512-DcTQY9syxu9BpU3Uo94fjCB3LN9/hgPS8oUL7KrSW3bA2ePrKZZPJcc5y0hoJAM9dft3pGfErtEUvxXQcfLxUg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -424,7 +412,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.2.tgz", "integrity": "sha512-qQByMRPwMZJainfig10BoaDldx/+VDtNcrA7qdNaEOAj6VXud+gfrkA8j4CRAU5HjnWREXqIpSpH30qZX1xivA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", @@ -435,7 +422,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", - "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" @@ -445,7 +431,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.0.tgz", "integrity": "sha512-59ANdmEwwRUkLjB7CRtwJxxwtjESw+X2IePItA+RGQh+oy5RmpCh/EvVVvh5XQc3yxsm5gtv0+i9oBZhaDNVTg==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.12.13", "@babel/helper-create-class-features-plugin": "^7.14.0", @@ -457,7 +442,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -466,7 +450,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -475,7 +458,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" } @@ -484,7 +466,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -493,7 +474,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -502,7 +482,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -511,7 +490,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -520,7 +498,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -529,7 +506,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -538,7 +514,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -547,7 +522,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.0.tgz", "integrity": "sha512-bda3xF8wGl5/5btF794utNOL0Jw+9jE5C1sLZcoK7c4uonE/y3iQiyG+KbkF3WBV/paX58VCpjhxLPkdj5Fe4w==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" } @@ -556,7 +530,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" } @@ -565,7 +538,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", - "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.14.0", "@babel/helper-plugin-utils": "^7.13.0", @@ -577,7 +549,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.4.tgz", "integrity": "sha512-WYdcGNEO7mCCZ2XzRlxwGj3PgeAr50ifkofOUC/+IN/GzKLB+biDPVBUAQN2C/dVZTvEXCp80kfQ1FFZPrwykQ==", - "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.14.4", "@babel/helper-plugin-utils": "^7.13.0", @@ -588,7 +559,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-validator-option": "^7.12.17", @@ -599,27 +569,16 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/parser": "^7.12.13", "@babel/types": "^7.12.13" } }, - "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.12.13" - } - }, "node_modules/@babel/traverse": { "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.14.2", @@ -631,20 +590,10 @@ "globals": "^11.1.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.12.13" - } - }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "engines": { "node": ">=4" } @@ -653,7 +602,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz", "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.14.0", "to-fast-properties": "^2.0.0" @@ -735,7 +683,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -751,7 +698,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -763,7 +709,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -776,7 +721,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -787,14 +731,12 @@ "node_modules/@jest/types/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/@jest/types/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -803,7 +745,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -873,6 +814,53 @@ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "dev": true }, + "node_modules/@types/babel__code-frame": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.2.tgz", + "integrity": "sha512-imO+jT/yjOKOAS5GQZ8SDtwiIloAGGr6OaZDKB0V5JVaSfGZLat5K5/ZRtyKW6R60XHV3RHYPTFfhYb+wDKyKg==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", + "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", + "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, "node_modules/@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -904,14 +892,12 @@ "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -920,7 +906,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", - "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } @@ -946,8 +931,16 @@ "node_modules/@types/node": { "version": "10.17.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "dev": true + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "node_modules/@types/pixelmatch": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.3.tgz", + "integrity": "sha512-p+nAQVYK/DUx7+s1Xyu9dqAg0gobf7VmJ+iDA4lljg1o4XRgQHr7R2h1NwFt3gdNOZiftxWB11+0TuZqXYf19w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/pngjs": { "version": "3.4.2", @@ -1045,11 +1038,19 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "node_modules/@types/source-map-support": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.4.2.tgz", + "integrity": "sha512-GbGWx39O8NdUHSChdrU0XeigBAgu1Teg3llwE0slSVcH2qISaQT70ftAiH+h4HIt3VIObFU34PSpXIKJuXCybQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", - "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", - "dev": true + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==" }, "node_modules/@types/tapable": { "version": "1.0.7", @@ -1109,11 +1110,19 @@ "@types/node": "*" } }, + "node_modules/@types/xml2js": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.8.tgz", + "integrity": "sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", - "dev": true, "dependencies": { "@types/yargs-parser": "*" } @@ -1121,8 +1130,7 @@ "node_modules/@types/yargs-parser": { "version": "20.2.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", - "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", - "dev": true + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" }, "node_modules/@types/yauzl": { "version": "2.9.1", @@ -1594,7 +1602,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, "engines": { "node": ">=8" } @@ -1603,7 +1610,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -1780,7 +1786,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, "dependencies": { "object.assign": "^4.1.0" } @@ -1930,7 +1935,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -2041,7 +2045,6 @@ "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, "dependencies": { "caniuse-lite": "^1.0.30001219", "colorette": "^1.2.2", @@ -2194,7 +2197,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -2228,14 +2230,12 @@ "node_modules/caniuse-lite": { "version": "1.0.30001230", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", - "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==", - "dev": true + "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==" }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2249,7 +2249,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -2258,7 +2257,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -2473,7 +2471,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -2481,14 +2478,12 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "node_modules/colorette": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" }, "node_modules/colors": { "version": "1.4.0", @@ -2584,7 +2579,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.1" } @@ -2891,7 +2885,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "dependencies": { "object-keys": "^1.0.12" }, @@ -2980,7 +2973,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, "engines": { "node": ">= 10.14.2" } @@ -3140,8 +3132,7 @@ "node_modules/electron-to-chromium": { "version": "1.3.741", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.741.tgz", - "integrity": "sha512-4i3T0cwnHo1O4Mnp9JniEco8bZiXoqbm3PhW5hv7uu8YLg35iajYrRnNyKFaN8/8SSTskU2hYqVTeYVPceSpUA==", - "dev": true + "integrity": "sha512-4i3T0cwnHo1O4Mnp9JniEco8bZiXoqbm3PhW5hv7uu8YLg35iajYrRnNyKFaN8/8SSTskU2hYqVTeYVPceSpUA==" }, "node_modules/electron/node_modules/@types/node": { "version": "12.20.13", @@ -3338,7 +3329,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -3456,6 +3446,15 @@ "node": ">=4" } }, + "node_modules/eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3708,7 +3707,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, "dependencies": { "@jest/types": "^26.6.2", "ansi-styles": "^4.0.0", @@ -3725,7 +3723,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3737,7 +3734,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3748,8 +3744,7 @@ "node_modules/expect/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/extend-shallow": { "version": "3.0.2", @@ -3993,7 +3988,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4198,75 +4192,6 @@ "readable-stream": "^2.3.6" } }, - "node_modules/folio": { - "version": "0.4.0-alpha28", - "resolved": "https://registry.npmjs.org/folio/-/folio-0.4.0-alpha28.tgz", - "integrity": "sha512-sbHdEDRXPkkhzHAyRy/tQKTWImNy38cICoii4ox9AGYFVWgF+i4l37AL2cVfJkUEvUqZpq+u4NkuV1cMelV5AA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/core": "^7.14.0", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-dynamic-import": "^7.13.8", - "@babel/plugin-proposal-export-namespace-from": "^7.12.13", - "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-numeric-separator": "^7.12.13", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-private-methods": "^7.13.0", - "@babel/plugin-proposal-private-property-in-object": "^7.14.0", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.14.0", - "@babel/preset-typescript": "^7.13.0", - "colors": "^1.4.0", - "commander": "^6.1.0", - "expect": "^26.4.2", - "jpeg-js": "^0.4.2", - "minimatch": "^3.0.3", - "ms": "^2.1.2", - "pirates": "^4.0.1", - "pixelmatch": "^5.2.1", - "rimraf": "^3.0.2", - "source-map-support": "^0.4.18", - "stack-utils": "^2.0.2" - }, - "bin": { - "folio": "cli.js" - }, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/folio/node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.12.13" - } - }, - "node_modules/folio/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/folio/node_modules/source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "dependencies": { - "source-map": "^0.5.6" - } - }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -4351,8 +4276,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -4364,7 +4288,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -4382,7 +4305,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -4641,7 +4563,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4659,7 +4580,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, "engines": { "node": ">=4" } @@ -4668,7 +4588,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5229,7 +5148,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -5338,7 +5256,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -5353,7 +5270,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5365,7 +5281,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5378,7 +5293,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5389,14 +5303,12 @@ "node_modules/jest-diff/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-diff/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5405,7 +5317,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5417,7 +5328,6 @@ "version": "26.3.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, "engines": { "node": ">= 10.14.2" } @@ -5426,7 +5336,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^26.6.2", @@ -5441,7 +5350,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5453,7 +5361,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5466,7 +5373,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5477,14 +5383,12 @@ "node_modules/jest-matcher-utils/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-matcher-utils/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5493,7 +5397,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5505,7 +5408,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -5525,7 +5427,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5537,7 +5438,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5550,7 +5450,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5561,14 +5460,12 @@ "node_modules/jest-message-util/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-message-util/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5577,7 +5474,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5589,7 +5485,6 @@ "version": "26.0.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true, "engines": { "node": ">= 10.14.2" } @@ -5602,8 +5497,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -5622,7 +5516,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -5665,7 +5558,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -5942,7 +5834,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, "dependencies": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -6016,8 +5907,7 @@ "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/mississippi": { "version": "3.0.0", @@ -6233,7 +6123,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6241,8 +6130,7 @@ "node_modules/node-releases": { "version": "1.1.72", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", - "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", - "dev": true + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" }, "node_modules/node-stream-zip": { "version": "1.13.4", @@ -6351,7 +6239,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -6372,7 +6259,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3", @@ -6647,7 +6533,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", - "dev": true, "engines": { "node": ">=8.6" } @@ -6666,7 +6551,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, "dependencies": { "node-modules-regexp": "^1.0.0" }, @@ -6678,7 +6562,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", - "dev": true, "dependencies": { "pngjs": "^4.0.1" }, @@ -6690,7 +6573,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==", - "dev": true, "engines": { "node": ">=8.0.0" } @@ -6861,7 +6743,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", @@ -6876,7 +6757,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6888,7 +6768,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6899,8 +6778,7 @@ "node_modules/pretty-format/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/process": { "version": "0.11.10", @@ -7088,8 +6966,7 @@ "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/readable-stream": { "version": "2.3.7", @@ -7391,8 +7268,7 @@ "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-regex": { "version": "1.1.0", @@ -7409,6 +7285,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, "node_modules/scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -7559,7 +7441,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -7842,13 +7723,19 @@ } }, "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "source-map": "^0.5.6" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" } }, "node_modules/source-map-url": { @@ -8177,6 +8064,16 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8215,7 +8112,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, "engines": { "node": ">=4" } @@ -8272,7 +8168,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -9444,6 +9339,28 @@ "node": ">=8.3.0" } }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9581,25 +9498,22 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.12.13" } }, "@babel/compat-data": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz", - "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==", - "dev": true + "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==" }, "@babel/core": { "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", - "dev": true, "requires": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.14.3", @@ -9618,26 +9532,15 @@ "source-map": "^0.5.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -9645,7 +9548,6 @@ "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", - "dev": true, "requires": { "@babel/types": "^7.14.2", "jsesc": "^2.5.1", @@ -9655,8 +9557,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -9664,7 +9565,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", - "dev": true, "requires": { "@babel/types": "^7.12.13" } @@ -9673,7 +9573,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz", "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==", - "dev": true, "requires": { "@babel/compat-data": "^7.14.4", "@babel/helper-validator-option": "^7.12.17", @@ -9684,8 +9583,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -9693,7 +9591,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz", "integrity": "sha512-idr3pthFlDCpV+p/rMgGLGYIVtazeatrSOQk8YzO2pAepIjQhCN3myeihVg58ax2bbbGK9PUE1reFi7axOYIOw==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.12.13", "@babel/helper-function-name": "^7.14.2", @@ -9707,7 +9604,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz", "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.12.13", "@babel/template": "^7.12.13", @@ -9718,7 +9614,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", - "dev": true, "requires": { "@babel/types": "^7.12.13" } @@ -9727,7 +9622,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, "requires": { "@babel/types": "^7.13.12" } @@ -9736,7 +9630,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", - "dev": true, "requires": { "@babel/types": "^7.13.12" } @@ -9745,7 +9638,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-replace-supers": "^7.13.12", @@ -9761,7 +9653,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", - "dev": true, "requires": { "@babel/types": "^7.12.13" } @@ -9769,14 +9660,12 @@ "@babel/helper-plugin-utils": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==" }, "@babel/helper-replace-supers": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz", "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==", - "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.13.12", "@babel/helper-optimise-call-expression": "^7.12.13", @@ -9788,7 +9677,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", - "dev": true, "requires": { "@babel/types": "^7.13.12" } @@ -9797,7 +9685,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", - "dev": true, "requires": { "@babel/types": "^7.12.1" } @@ -9806,7 +9693,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", - "dev": true, "requires": { "@babel/types": "^7.12.13" } @@ -9814,20 +9700,17 @@ "@babel/helper-validator-identifier": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==" }, "@babel/helper-validator-option": { "version": "7.12.17", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", - "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", - "dev": true + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" }, "@babel/helpers": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", - "dev": true, "requires": { "@babel/template": "^7.12.13", "@babel/traverse": "^7.14.0", @@ -9838,7 +9721,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", "chalk": "^2.0.0", @@ -9848,14 +9730,12 @@ "@babel/parser": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz", - "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==", - "dev": true + "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==" }, "@babel/plugin-proposal-class-properties": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", - "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" @@ -9865,7 +9745,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.2.tgz", "integrity": "sha512-oxVQZIWFh91vuNEMKltqNsKLFWkOIyJc95k2Gv9lWVyDfPUQGSSlbDEgWuJUU1afGE9WwlzpucMZ3yDRHIItkA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -9875,7 +9754,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.2.tgz", "integrity": "sha512-sRxW3z3Zp3pFfLAgVEvzTFutTXax837oOatUIvSG9o5gRj9mKwm3br1Se5f4QalTQs9x4AzlA/HrCWbQIHASUQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -9885,7 +9763,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.2.tgz", "integrity": "sha512-1JAZtUrqYyGsS7IDmFeaem+/LJqujfLZ2weLR9ugB0ufUPjzf8cguyVT1g5im7f7RXxuLq1xUxEzvm68uYRtGg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -9895,7 +9772,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz", "integrity": "sha512-ebR0zU9OvI2N4qiAC38KIAK75KItpIPTpAtd2r4OZmMFeKbKJpUFLYP2EuDut82+BmYi8sz42B+TfTptJ9iG5Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -9905,7 +9781,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.2.tgz", "integrity": "sha512-DcTQY9syxu9BpU3Uo94fjCB3LN9/hgPS8oUL7KrSW3bA2ePrKZZPJcc5y0hoJAM9dft3pGfErtEUvxXQcfLxUg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -9915,7 +9790,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.2.tgz", "integrity": "sha512-qQByMRPwMZJainfig10BoaDldx/+VDtNcrA7qdNaEOAj6VXud+gfrkA8j4CRAU5HjnWREXqIpSpH30qZX1xivA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", @@ -9926,7 +9800,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", - "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" @@ -9936,7 +9809,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.0.tgz", "integrity": "sha512-59ANdmEwwRUkLjB7CRtwJxxwtjESw+X2IePItA+RGQh+oy5RmpCh/EvVVvh5XQc3yxsm5gtv0+i9oBZhaDNVTg==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.12.13", "@babel/helper-create-class-features-plugin": "^7.14.0", @@ -9948,7 +9820,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -9957,7 +9828,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -9966,7 +9836,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" } @@ -9975,7 +9844,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -9984,7 +9852,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -9993,7 +9860,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -10002,7 +9868,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -10011,7 +9876,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -10020,7 +9884,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -10029,7 +9892,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -10038,7 +9900,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.0.tgz", "integrity": "sha512-bda3xF8wGl5/5btF794utNOL0Jw+9jE5C1sLZcoK7c4uonE/y3iQiyG+KbkF3WBV/paX58VCpjhxLPkdj5Fe4w==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0" } @@ -10047,7 +9908,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.12.13" } @@ -10056,7 +9916,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.14.0", "@babel/helper-plugin-utils": "^7.13.0", @@ -10068,7 +9927,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.4.tgz", "integrity": "sha512-WYdcGNEO7mCCZ2XzRlxwGj3PgeAr50ifkofOUC/+IN/GzKLB+biDPVBUAQN2C/dVZTvEXCp80kfQ1FFZPrwykQ==", - "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.14.4", "@babel/helper-plugin-utils": "^7.13.0", @@ -10079,7 +9937,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-validator-option": "^7.12.17", @@ -10090,29 +9947,16 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", - "dev": true, "requires": { "@babel/code-frame": "^7.12.13", "@babel/parser": "^7.12.13", "@babel/types": "^7.12.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - } } }, "@babel/traverse": { "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", - "dev": true, "requires": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.14.2", @@ -10124,20 +9968,10 @@ "globals": "^11.1.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" } } }, @@ -10145,7 +9979,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz", "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.0", "to-fast-properties": "^2.0.0" @@ -10214,7 +10047,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -10227,7 +10059,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -10236,7 +10067,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10246,7 +10076,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -10254,20 +10083,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -10321,6 +10147,53 @@ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "dev": true }, + "@types/babel__code-frame": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.2.tgz", + "integrity": "sha512-imO+jT/yjOKOAS5GQZ8SDtwiIloAGGr6OaZDKB0V5JVaSfGZLat5K5/ZRtyKW6R60XHV3RHYPTFfhYb+wDKyKg==", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", + "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", + "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -10352,14 +10225,12 @@ "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" }, "@types/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "*" } @@ -10368,7 +10239,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", - "dev": true, "requires": { "@types/istanbul-lib-report": "*" } @@ -10394,8 +10264,16 @@ "@types/node": { "version": "10.17.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "dev": true + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "@types/pixelmatch": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.3.tgz", + "integrity": "sha512-p+nAQVYK/DUx7+s1Xyu9dqAg0gobf7VmJ+iDA4lljg1o4XRgQHr7R2h1NwFt3gdNOZiftxWB11+0TuZqXYf19w==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/pngjs": { "version": "3.4.2", @@ -10493,11 +10371,19 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "@types/source-map-support": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.4.2.tgz", + "integrity": "sha512-GbGWx39O8NdUHSChdrU0XeigBAgu1Teg3llwE0slSVcH2qISaQT70ftAiH+h4HIt3VIObFU34PSpXIKJuXCybQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", - "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", - "dev": true + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==" }, "@types/tapable": { "version": "1.0.7", @@ -10556,11 +10442,19 @@ "@types/node": "*" } }, + "@types/xml2js": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.8.tgz", + "integrity": "sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", - "dev": true, "requires": { "@types/yargs-parser": "*" } @@ -10568,8 +10462,7 @@ "@types/yargs-parser": { "version": "20.2.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", - "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", - "dev": true + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" }, "@types/yauzl": { "version": "2.9.1", @@ -10991,14 +10884,12 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -11148,7 +11039,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, "requires": { "object.assign": "^4.1.0" } @@ -11279,7 +11169,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -11386,7 +11275,6 @@ "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30001219", "colorette": "^1.2.2", @@ -11522,7 +11410,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -11555,14 +11442,12 @@ "caniuse-lite": { "version": "1.0.30001230", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", - "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==", - "dev": true + "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==" }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -11572,14 +11457,12 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -11758,7 +11641,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -11766,14 +11648,12 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colorette": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" }, "colors": { "version": "1.4.0", @@ -11854,7 +11734,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -12105,7 +11984,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -12177,8 +12055,7 @@ "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==" }, "diffie-hellman": { "version": "5.0.3", @@ -12360,8 +12237,7 @@ "electron-to-chromium": { "version": "1.3.741", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.741.tgz", - "integrity": "sha512-4i3T0cwnHo1O4Mnp9JniEco8bZiXoqbm3PhW5hv7uu8YLg35iajYrRnNyKFaN8/8SSTskU2hYqVTeYVPceSpUA==", - "dev": true + "integrity": "sha512-4i3T0cwnHo1O4Mnp9JniEco8bZiXoqbm3PhW5hv7uu8YLg35iajYrRnNyKFaN8/8SSTskU2hYqVTeYVPceSpUA==" }, "elliptic": { "version": "6.5.4", @@ -12499,8 +12375,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "2.0.0", @@ -12552,6 +12427,15 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -12793,7 +12677,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, "requires": { "@jest/types": "^26.6.2", "ansi-styles": "^4.0.0", @@ -12807,7 +12690,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -12816,7 +12698,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -12824,8 +12705,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -13030,7 +12910,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -13204,68 +13083,6 @@ "readable-stream": "^2.3.6" } }, - "folio": { - "version": "0.4.0-alpha28", - "resolved": "https://registry.npmjs.org/folio/-/folio-0.4.0-alpha28.tgz", - "integrity": "sha512-sbHdEDRXPkkhzHAyRy/tQKTWImNy38cICoii4ox9AGYFVWgF+i4l37AL2cVfJkUEvUqZpq+u4NkuV1cMelV5AA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/core": "^7.14.0", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-dynamic-import": "^7.13.8", - "@babel/plugin-proposal-export-namespace-from": "^7.12.13", - "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-numeric-separator": "^7.12.13", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-private-methods": "^7.13.0", - "@babel/plugin-proposal-private-property-in-object": "^7.14.0", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.14.0", - "@babel/preset-typescript": "^7.13.0", - "colors": "^1.4.0", - "commander": "^6.1.0", - "expect": "^26.4.2", - "jpeg-js": "^0.4.2", - "minimatch": "^3.0.3", - "ms": "^2.1.2", - "pirates": "^4.0.1", - "pixelmatch": "^5.2.1", - "rimraf": "^3.0.2", - "source-map-support": "^0.4.18", - "stack-utils": "^2.0.2" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - } - } - }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -13335,8 +13152,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -13347,8 +13163,7 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-caller-file": { "version": "2.0.5", @@ -13360,7 +13175,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -13575,7 +13389,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -13589,14 +13402,12 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-value": { "version": "1.0.0", @@ -14052,8 +13863,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-number-object": { "version": "1.0.5", @@ -14135,7 +13945,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -14147,7 +13956,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -14156,7 +13964,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14166,7 +13973,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -14174,20 +13980,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -14197,14 +14000,12 @@ "jest-get-type": { "version": "26.3.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==" }, "jest-matcher-utils": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", - "dev": true, "requires": { "chalk": "^4.0.0", "jest-diff": "^26.6.2", @@ -14216,7 +14017,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -14225,7 +14025,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14235,7 +14034,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -14243,20 +14041,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -14267,7 +14062,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", - "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", @@ -14284,7 +14078,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -14293,7 +14086,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -14303,7 +14095,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -14311,20 +14102,17 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -14334,8 +14122,7 @@ "jest-regex-util": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==" }, "jpeg-js": { "version": "0.4.3", @@ -14345,8 +14132,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.1", @@ -14361,8 +14147,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-buffer": { "version": "3.0.0", @@ -14399,7 +14184,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -14625,7 +14409,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, "requires": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -14683,8 +14466,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mississippi": { "version": "3.0.0", @@ -14880,14 +14662,12 @@ "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" }, "node-releases": { "version": "1.1.72", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", - "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", - "dev": true + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" }, "node-stream-zip": { "version": "1.13.4", @@ -14973,8 +14753,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -14989,7 +14768,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, "requires": { "call-bind": "^1.0.0", "define-properties": "^1.1.3", @@ -15219,8 +14997,7 @@ "picomatch": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", - "dev": true + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==" }, "pify": { "version": "3.0.0", @@ -15233,7 +15010,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, "requires": { "node-modules-regexp": "^1.0.0" } @@ -15242,7 +15018,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", - "dev": true, "requires": { "pngjs": "^4.0.1" }, @@ -15250,8 +15025,7 @@ "pngjs": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", - "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==", - "dev": true + "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==" } } }, @@ -15370,7 +15144,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, "requires": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", @@ -15382,7 +15155,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -15391,7 +15163,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -15399,8 +15170,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -15576,8 +15346,7 @@ "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "readable-stream": { "version": "2.3.7", @@ -15824,8 +15593,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -15842,6 +15610,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, "scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -15966,8 +15740,7 @@ "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, "slice-ansi": { "version": "4.0.0", @@ -16194,13 +15967,18 @@ } }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "source-map": "^0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } } }, "source-map-url": { @@ -16453,6 +16231,16 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } } } }, @@ -16520,8 +16308,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -16565,7 +16352,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -17546,6 +17332,22 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 1d9bdcb7e3..b7d9529700 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,14 @@ "node": ">=12" }, "scripts": { - "ctest": "folio --config=tests/config/default.config.ts --project=chromium", - "ftest": "folio --config=tests/config/default.config.ts --project=firefox", - "wtest": "folio --config=tests/config/default.config.ts --project=webkit", - "atest": "folio --config=tests/config/android.config.ts", - "etest": "folio --config=tests/config/electron.config.ts", - "test": "folio --config=tests/config/default.config.ts", + "basetest": "node ./tests/config/test-runner/node_modules/@playwright/test/lib/cli/cli.js test", + "ctest": "npm run basetest -- --config=tests/config/default.config.ts --project=chromium", + "ftest": "npm run basetest -- --config=tests/config/default.config.ts --project=firefox", + "wtest": "npm run basetest -- --config=tests/config/default.config.ts --project=webkit", + "atest": "npm run basetest -- --config=tests/config/android.config.ts", + "etest": "npm run basetest -- --config=tests/config/electron.config.ts", + "ttest": "npm run basetest -- --config=tests/playwright-test/playwright-test.config.ts", + "test": "npm run basetest -- --config=tests/config/default.config.ts", "eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext ts . || eslint --ext ts .", "tsc": "tsc -p .", "tsc-installer": "tsc -p ./src/install/tsconfig.json", @@ -36,7 +38,32 @@ "bin": { "playwright": "./lib/cli/cli.js" }, + "DEPS-NOTE": "Any non-test dependency must be added to the build_package.js script as well", "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/core": "^7.14.0", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-dynamic-import": "^7.13.8", + "@babel/plugin-proposal-export-namespace-from": "^7.12.13", + "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-numeric-separator": "^7.12.13", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-private-property-in-object": "^7.14.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.14.0", + "@babel/preset-typescript": "^7.13.0", + "colors": "^1.4.0", + "expect": "^26.4.2", + "minimatch": "^3.0.3", + "ms": "^2.1.2", + "pirates": "^4.0.1", + "pixelmatch": "^5.2.1", + "source-map-support": "^0.4.18", "commander": "^6.1.0", "debug": "^4.1.1", "extract-zip": "^2.0.1", @@ -53,10 +80,14 @@ "yazl": "^2.5.1" }, "devDependencies": { + "@types/babel__code-frame": "^7.0.2", + "@types/babel__core": "^7.1.14", "@types/debug": "^4.1.5", "@types/extract-zip": "^1.6.2", "@types/mime": "^2.0.3", + "@types/minimatch": "^3.0.3", "@types/node": "^10.17.28", + "@types/pixelmatch": "^5.2.1", "@types/pngjs": "^3.4.2", "@types/progress": "^2.0.3", "@types/proper-lockfile": "^4.1.1", @@ -65,8 +96,10 @@ "@types/react-dom": "^17.0.0", "@types/resize-observer-browser": "^0.1.4", "@types/rimraf": "^3.0.0", + "@types/source-map-support": "^0.4.2", "@types/webpack": "^4.41.25", "@types/ws": "7.2.6", + "@types/xml2js": "^0.4.5", "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", @@ -80,7 +113,6 @@ "eslint-plugin-notice": "^0.9.10", "eslint-plugin-react-hooks": "^4.2.0", "file-loader": "^6.1.0", - "folio": "=0.4.0-alpha28", "formidable": "^1.2.2", "html-webpack-plugin": "^4.4.1", "ncp": "^2.0.0", @@ -90,9 +122,10 @@ "socksv5": "0.0.6", "style-loader": "^1.2.1", "ts-loader": "^8.0.3", - "typescript": "^4.0.2", + "typescript": "=4.2.4", "webpack": "^4.44.2", "webpack-cli": "^3.3.12", + "xml2js": "^0.4.23", "yaml": "^1.10.0" } } diff --git a/packages/build_package.js b/packages/build_package.js index b6bd3d2f44..e22057e700 100755 --- a/packages/build_package.js +++ b/packages/build_package.js @@ -61,10 +61,27 @@ const PACKAGES = { 'playwright-chromium': { description: 'A high-level API to automate Chromium', browsers: ['chromium', 'ffmpeg'], - files: [...PLAYWRIGHT_CORE_FILES], + files: PLAYWRIGHT_CORE_FILES, }, }; +const DEPENDENCIES = [ + 'commander', + 'debug', + 'extract-zip', + 'https-proxy-agent', + 'jpeg-js', + 'mime', + 'pngjs', + 'progress', + 'proper-lockfile', + 'proxy-from-env', + 'rimraf', + 'stack-utils', + 'ws', + 'yazl', +]; + // 1. Parse CLI arguments const args = process.argv.slice(2); if (args.some(arg => arg === '--help')) { @@ -121,9 +138,10 @@ if (!args.some(arg => arg === '--no-cleanup')) { // 4. Generate package.json const pwInternalJSON = require(path.join(ROOT_PATH, 'package.json')); - const dependencies = { ...pwInternalJSON.dependencies }; - if (packageName === 'playwright-test') - dependencies.folio = pwInternalJSON.devDependencies.folio; + const depNames = packageName === 'playwright-test' ? Object.keys(pwInternalJSON.dependencies) : DEPENDENCIES; + const dependencies = {}; + for (const dep of depNames) + dependencies[dep] = pwInternalJSON.dependencies[dep]; await writeToPackage('package.json', JSON.stringify({ name: package.name || packageName, version: pwInternalJSON.version, diff --git a/packages/playwright-test/index.js b/packages/playwright-test/index.js index dd7ff39165..cd046e0994 100644 --- a/packages/playwright-test/index.js +++ b/packages/playwright-test/index.js @@ -16,5 +16,5 @@ module.exports = { ...require('./lib/inprocess'), - ...require('./lib/cli/fixtures') + ...require('./lib/test/index') }; diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 3f9fb5038c..c1dc5ff734 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -35,16 +35,16 @@ import { BrowserContextOptions, LaunchOptions } from '../client/types'; import { spawn } from 'child_process'; import { installDeps } from '../install/installDeps'; import { allBrowserNames, BrowserName } from '../utils/registry'; -import { addTestCommand } from './testRunner'; import * as utils from '../utils/utils'; const SCRIPTS_DIRECTORY = path.join(__dirname, '..', '..', 'bin'); type BrowserChannel = 'chrome-beta'|'chrome'; const allBrowserChannels: Set = new Set(['chrome-beta', 'chrome']); +const packageJSON = require('../../package.json'); program - .version('Version ' + require('../../package.json').version) + .version('Version ' + packageJSON.version) .name(process.env.PW_CLI_NAME || 'npx playwright'); commandWithOpenOptions('open [url]', 'open page in browser specified via -b, --browser', []) @@ -226,8 +226,19 @@ program console.log(' $ show-trace trace/directory'); }); -if (!process.env.PW_CLI_TARGET_LANG) - addTestCommand(program); +if (!process.env.PW_CLI_TARGET_LANG) { + if (packageJSON.name === '@playwright/test' || process.env.PWTEST_CLI_ALLOW_TEST_COMMAND) { + require('../test/cli').addTestCommand(program); + } else { + const command = program.command('test'); + command.description('Run tests with Playwright Test. Available in @playwright/test package.'); + command.action(async (args, opts) => { + console.error('Please install @playwright/test package to use Playwright Test.'); + console.error(' npm install -D @playwright/test'); + process.exit(1); + }); + } +} if (process.argv[2] === 'run-driver') runDriver(); diff --git a/src/cli/testRunner.ts b/src/test/cli.ts similarity index 87% rename from src/cli/testRunner.ts rename to src/test/cli.ts index 433cdef6fc..a5d3a83dd7 100644 --- a/src/cli/testRunner.ts +++ b/src/test/cli.ts @@ -19,9 +19,8 @@ import * as commander from 'commander'; import * as fs from 'fs'; import * as path from 'path'; -import type { Config } from 'folio'; - -type RunnerType = typeof import('folio/out/runner').Runner; +import type { Config } from './types'; +import { Runner } from './runner'; const defaultTimeout = 30000; const defaultReporter = process.env.CI ? 'dot' : 'list'; @@ -37,14 +36,6 @@ const defaultConfig: Config = { }; export function addTestCommand(program: commander.CommanderStatic) { - let Runner: RunnerType; - try { - Runner = require('folio/out/runner').Runner as RunnerType; - } catch (e) { - addStubTestCommand(program); - return; - } - const command = program.command('test [test-filter...]'); command.description('Run tests with Playwright Test'); command.option('--browser ', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`); @@ -68,7 +59,7 @@ export function addTestCommand(program: commander.CommanderStatic) { command.option('-x', `Stop after the first failure`); command.action(async (args, opts) => { try { - await runTests(Runner, args, opts); + await runTests(args, opts); } catch (e) { console.error(e.toString()); process.exit(1); @@ -86,7 +77,7 @@ export function addTestCommand(program: commander.CommanderStatic) { }); } -async function runTests(Runner: RunnerType, args: string[], opts: { [key: string]: any }) { +async function runTests(args: string[], opts: { [key: string]: any }) { const browserOpt = opts.browser ? opts.browser.toLowerCase() : 'chromium'; if (!['all', 'chromium', 'firefox', 'webkit'].includes(browserOpt)) throw new Error(`Unsupported browser "${opts.browser}", must be one of "all", "chromium", "firefox" or "webkit"`); @@ -135,11 +126,6 @@ async function runTests(Runner: RunnerType, args: string[], opts: { [key: string throw new Error(`Configuration file not found. Run "npx playwright test --help" for more information.`); } - process.env.FOLIO_JUNIT_OUTPUT_NAME = process.env.PLAYWRIGHT_JUNIT_OUTPUT_NAME; - process.env.FOLIO_JUNIT_SUITE_ID = process.env.PLAYWRIGHT_JUNIT_SUITE_ID; - process.env.FOLIO_JUNIT_SUITE_NAME = process.env.PLAYWRIGHT_JUNIT_SUITE_NAME; - process.env.FOLIO_JSON_OUTPUT_NAME = process.env.PLAYWRIGHT_JSON_OUTPUT_NAME; - const result = await runner.run(!!opts.list, args.map(forceRegExp), opts.project || undefined); if (result === 'sigint') process.exit(130); @@ -172,13 +158,3 @@ function overridesFromOptions(options: { [key: string]: any }): Config { workers: options.workers ? parseInt(options.workers, 10) : undefined, }; } - -function addStubTestCommand(program: commander.CommanderStatic) { - const command = program.command('test'); - command.description('Run tests with Playwright Test. Available in @playwright/test package.'); - command.action(async (args, opts) => { - console.error('Please install @playwright/test package to use Playwright Test.'); - console.error(' npm install -D @playwright/test'); - process.exit(1); - }); -} diff --git a/src/test/dispatcher.ts b/src/test/dispatcher.ts new file mode 100644 index 0000000000..7aafc82985 --- /dev/null +++ b/src/test/dispatcher.ts @@ -0,0 +1,367 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import child_process from 'child_process'; +import path from 'path'; +import { EventEmitter } from 'events'; +import { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, WorkerInitParams } from './ipc'; +import type { TestResult, Reporter, TestStatus } from './reporter'; +import { Suite, Test } from './test'; +import { Loader } from './loader'; + +type DispatcherEntry = { + runPayload: RunPayload; + hash: string; + repeatEachIndex: number; + projectIndex: number; +}; + +export class Dispatcher { + private _workers = new Set(); + private _freeWorkers: Worker[] = []; + private _workerClaimers: (() => void)[] = []; + + private _testById = new Map(); + private _queue: DispatcherEntry[] = []; + private _stopCallback = () => {}; + readonly _loader: Loader; + private _suite: Suite; + private _reporter: Reporter; + private _hasWorkerErrors = false; + private _isStopped = false; + private _failureCount = 0; + + constructor(loader: Loader, suite: Suite, reporter: Reporter) { + this._loader = loader; + this._reporter = reporter; + + this._suite = suite; + for (const suite of this._suite.suites) { + for (const spec of suite._allSpecs()) { + for (const test of spec.tests) + this._testById.set(test._id, { test, result: test._appendTestResult() }); + } + } + + this._queue = this._filesSortedByWorkerHash(); + + // Shard tests. + const shard = this._loader.fullConfig().shard; + if (shard) { + let total = this._suite.totalTestCount(); + const shardSize = Math.ceil(total / shard.total); + const from = shardSize * shard.current; + const to = shardSize * (shard.current + 1); + let current = 0; + total = 0; + const filteredQueue: DispatcherEntry[] = []; + for (const entry of this._queue) { + if (current >= from && current < to) { + filteredQueue.push(entry); + total += entry.runPayload.entries.length; + } + current += entry.runPayload.entries.length; + } + this._queue = filteredQueue; + } + } + + _filesSortedByWorkerHash(): DispatcherEntry[] { + const entriesByWorkerHashAndFile = new Map>(); + for (const fileSuite of this._suite.suites) { + const file = fileSuite.file; + for (const spec of fileSuite._allSpecs()) { + for (const test of spec.tests) { + let entriesByFile = entriesByWorkerHashAndFile.get(test._workerHash); + if (!entriesByFile) { + entriesByFile = new Map(); + entriesByWorkerHashAndFile.set(test._workerHash, entriesByFile); + } + let entry = entriesByFile.get(file); + if (!entry) { + entry = { + runPayload: { + entries: [], + file, + }, + repeatEachIndex: test._repeatEachIndex, + projectIndex: test._projectIndex, + hash: test._workerHash, + }; + entriesByFile.set(file, entry); + } + entry.runPayload.entries.push({ + retry: this._testById.get(test._id)!.result.retry, + testId: test._id, + }); + } + } + } + + const result: DispatcherEntry[] = []; + for (const entriesByFile of entriesByWorkerHashAndFile.values()) { + for (const entry of entriesByFile.values()) + result.push(entry); + } + result.sort((a, b) => a.hash < b.hash ? -1 : (a.hash === b.hash ? 0 : 1)); + return result; + } + + async run() { + // Loop in case job schedules more jobs + while (this._queue.length && !this._isStopped) + await this._dispatchQueue(); + } + + async _dispatchQueue() { + const jobs = []; + while (this._queue.length) { + if (this._isStopped) + break; + const entry = this._queue.shift()!; + const requiredHash = entry.hash; + let worker = await this._obtainWorker(entry); + while (!this._isStopped && worker.hash && worker.hash !== requiredHash) { + worker.stop(); + worker = await this._obtainWorker(entry); + } + if (this._isStopped) + break; + jobs.push(this._runJob(worker, entry)); + } + await Promise.all(jobs); + } + + async _runJob(worker: Worker, entry: DispatcherEntry) { + worker.run(entry.runPayload); + let doneCallback = () => {}; + const result = new Promise(f => doneCallback = f); + worker.once('done', (params: DonePayload) => { + // We won't file remaining if: + // - there are no remaining + // - we are here not because something failed + // - no unrecoverable worker error + if (!params.remaining.length && !params.failedTestId && !params.fatalError) { + this._freeWorkers.push(worker); + this._notifyWorkerClaimer(); + doneCallback(); + return; + } + + // When worker encounters error, we will stop it and create a new one. + worker.stop(); + + let remaining = params.remaining; + const failedTestIds = new Set(); + + // In case of fatal error, report all remaining tests as failing with this error. + if (params.fatalError) { + for (const { testId } of remaining) { + const { test, result } = this._testById.get(testId)!; + this._reporter.onTestBegin?.(test); + result.error = params.fatalError; + this._reportTestEnd(test, result, 'failed'); + failedTestIds.add(testId); + } + // Since we pretent that all remaining tests failed, there is nothing else to run, + // except for possible retries. + remaining = []; + } + if (params.failedTestId) + failedTestIds.add(params.failedTestId); + + // Only retry expected failures, not passes and only if the test failed. + for (const testId of failedTestIds) { + const pair = this._testById.get(testId)!; + if (pair.test.expectedStatus === 'passed' && pair.test.results.length < pair.test.retries + 1) { + pair.result = pair.test._appendTestResult(); + remaining.unshift({ + retry: pair.result.retry, + testId: pair.test._id, + }); + } + } + + if (remaining.length) + this._queue.unshift({ ...entry, runPayload: { ...entry.runPayload, entries: remaining } }); + + // This job is over, we just scheduled another one. + doneCallback(); + }); + return result; + } + + async _obtainWorker(entry: DispatcherEntry) { + const claimWorker = (): Promise | null => { + // Use available worker. + if (this._freeWorkers.length) + return Promise.resolve(this._freeWorkers.pop()!); + // Create a new worker. + if (this._workers.size < this._loader.fullConfig().workers) + return this._createWorker(entry); + return null; + }; + + // Note: it is important to claim the worker synchronously, + // so that we won't miss a _notifyWorkerClaimer call while awaiting. + let worker = claimWorker(); + if (!worker) { + // Wait for available or stopped worker. + await new Promise(f => this._workerClaimers.push(f)); + worker = claimWorker(); + } + return worker!; + } + + async _notifyWorkerClaimer() { + if (this._isStopped || !this._workerClaimers.length) + return; + const callback = this._workerClaimers.shift()!; + callback(); + } + + _createWorker(entry: DispatcherEntry) { + const worker = new Worker(this); + worker.on('testBegin', (params: TestBeginPayload) => { + const { test, result: testRun } = this._testById.get(params.testId)!; + testRun.workerIndex = params.workerIndex; + this._reporter.onTestBegin(test); + }); + worker.on('testEnd', (params: TestEndPayload) => { + const { test, result } = this._testById.get(params.testId)!; + result.duration = params.duration; + result.error = params.error; + test.expectedStatus = params.expectedStatus; + test.annotations = params.annotations; + test.timeout = params.timeout; + if (params.expectedStatus === 'skipped' && params.status === 'skipped') + test.skipped = true; + this._reportTestEnd(test, result, params.status); + }); + worker.on('stdOut', (params: TestOutputPayload) => { + const chunk = chunkFromParams(params); + const pair = params.testId ? this._testById.get(params.testId) : undefined; + if (pair) + pair.result.stdout.push(chunk); + this._reporter.onStdOut(chunk, pair ? pair.test : undefined); + }); + worker.on('stdErr', (params: TestOutputPayload) => { + const chunk = chunkFromParams(params); + const pair = params.testId ? this._testById.get(params.testId) : undefined; + if (pair) + pair.result.stderr.push(chunk); + this._reporter.onStdErr(chunk, pair ? pair.test : undefined); + }); + worker.on('teardownError', ({error}) => { + this._hasWorkerErrors = true; + this._reporter.onError(error); + }); + worker.on('exit', () => { + this._workers.delete(worker); + this._notifyWorkerClaimer(); + if (this._stopCallback && !this._workers.size) + this._stopCallback(); + }); + this._workers.add(worker); + return worker.init(entry).then(() => worker); + } + + async stop() { + this._isStopped = true; + if (this._workers.size) { + const result = new Promise(f => this._stopCallback = f); + for (const worker of this._workers) + worker.stop(); + await result; + } + } + + private _reportTestEnd(test: Test, result: TestResult, status: TestStatus) { + if (this._isStopped) + return; + result.status = status; + if (result.status !== 'skipped' && result.status !== test.expectedStatus) + ++this._failureCount; + const maxFailures = this._loader.fullConfig().maxFailures; + if (!maxFailures || this._failureCount <= maxFailures) + this._reporter.onTestEnd(test, result); + if (maxFailures && this._failureCount === maxFailures) + this._isStopped = true; + } + + hasWorkerErrors(): boolean { + return this._hasWorkerErrors; + } +} + +let lastWorkerIndex = 0; + +class Worker extends EventEmitter { + process: child_process.ChildProcess; + runner: Dispatcher; + hash = ''; + index: number; + + constructor(runner: Dispatcher) { + super(); + this.runner = runner; + this.index = lastWorkerIndex++; + + this.process = child_process.fork(path.join(__dirname, 'worker.js'), { + detached: false, + env: { + FORCE_COLOR: process.stdout.isTTY ? '1' : '0', + DEBUG_COLORS: process.stdout.isTTY ? '1' : '0', + TEST_WORKER_INDEX: String(this.index), + ...process.env + }, + // Can't pipe since piping slows down termination for some reason. + stdio: ['ignore', 'ignore', process.env.PW_RUNNER_DEBUG ? 'inherit' : 'ignore', 'ipc'] + }); + this.process.on('exit', () => this.emit('exit')); + this.process.on('error', e => {}); // do not yell at a send to dead process. + this.process.on('message', (message: any) => { + const { method, params } = message; + this.emit(method, params); + }); + } + + async init(entry: DispatcherEntry) { + this.hash = entry.hash; + const params: WorkerInitParams = { + workerIndex: this.index, + repeatEachIndex: entry.repeatEachIndex, + projectIndex: entry.projectIndex, + loader: this.runner._loader.serialize(), + }; + this.process.send({ method: 'init', params }); + await new Promise(f => this.process.once('message', f)); // Ready ack + } + + run(runPayload: RunPayload) { + this.process.send({ method: 'run', params: runPayload }); + } + + stop() { + this.process.send({ method: 'stop' }); + } +} + +function chunkFromParams(params: TestOutputPayload): string | Buffer { + if (typeof params.text === 'string') + return params.text; + return Buffer.from(params.buffer!, 'base64'); +} diff --git a/src/test/expect.ts b/src/test/expect.ts new file mode 100644 index 0000000000..eaf33c9e7e --- /dev/null +++ b/src/test/expect.ts @@ -0,0 +1,40 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Expect } from './types'; +import expectLibrary from 'expect'; +import { currentTestInfo } from './globals'; +import { compare } from './golden'; + +export const expect: Expect = expectLibrary; + +function toMatchSnapshot(received: Buffer | string, nameOrOptions: string | { name: string, threshold?: number }, optOptions: { threshold?: number } = {}) { + let options: { name: string, threshold?: number }; + const testInfo = currentTestInfo(); + if (!testInfo) + throw new Error(`toMatchSnapshot() must be called during the test`); + if (typeof nameOrOptions === 'string') + options = { name: nameOrOptions, ...optOptions }; + else + options = { ...nameOrOptions }; + if (!options.name) + throw new Error(`toMatchSnapshot() requires a "name" parameter`); + + const { pass, message } = compare(received, options.name, testInfo.snapshotPath, testInfo.outputPath, testInfo.config.updateSnapshots, options); + return { pass, message: () => message }; +} + +expectLibrary.extend({ toMatchSnapshot }); diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts new file mode 100644 index 0000000000..dd018a3696 --- /dev/null +++ b/src/test/fixtures.ts @@ -0,0 +1,361 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { errorWithCallLocation, formatLocation, prependErrorMessage, wrapInPromise } from './util'; +import * as crypto from 'crypto'; +import { FixturesWithLocation, Location } from './types'; + +type FixtureScope = 'test' | 'worker'; +type FixtureRegistration = { + location: Location; + name: string; + scope: FixtureScope; + fn: Function | any; // Either a fixture function, or a fixture value. + auto: boolean; + deps: string[]; + id: string; + super?: FixtureRegistration; +}; + +class Fixture { + runner: FixtureRunner; + registration: FixtureRegistration; + usages: Set; + value: any; + _teardownFenceCallback!: (value?: unknown) => void; + _tearDownComplete!: Promise; + _setup = false; + _teardown = false; + + constructor(runner: FixtureRunner, registration: FixtureRegistration) { + this.runner = runner; + this.registration = registration; + this.usages = new Set(); + this.value = null; + } + + async setup(info: any) { + if (typeof this.registration.fn !== 'function') { + this._setup = true; + this.value = this.registration.fn; + return; + } + + const params: { [key: string]: any } = {}; + for (const name of this.registration.deps) { + const registration = this.runner.pool!.resolveDependency(this.registration, name); + if (!registration) + throw errorWithCallLocation(`Unknown fixture "${name}"`); + const dep = await this.runner.setupFixtureForRegistration(registration, info); + dep.usages.add(this); + params[name] = dep.value; + } + + let setupFenceFulfill = () => {}; + let setupFenceReject = (e: Error) => {}; + let called = false; + const setupFence = new Promise((f, r) => { setupFenceFulfill = f; setupFenceReject = r; }); + const teardownFence = new Promise(f => this._teardownFenceCallback = f); + this._tearDownComplete = wrapInPromise(this.registration.fn(params, async (value: any) => { + if (called) + throw errorWithCallLocation(`Cannot provide fixture value for the second time`); + called = true; + this.value = value; + setupFenceFulfill(); + return await teardownFence; + }, info)).catch((e: any) => { + if (!this._setup) + setupFenceReject(e); + else + throw e; + }); + await setupFence; + this._setup = true; + } + + async teardown() { + if (this._teardown) + return; + this._teardown = true; + if (typeof this.registration.fn !== 'function') + return; + for (const fixture of this.usages) + await fixture.teardown(); + this.usages.clear(); + if (this._setup) { + this._teardownFenceCallback(); + await this._tearDownComplete; + } + this.runner.instanceForId.delete(this.registration.id); + } +} + +export class FixturePool { + readonly digest: string; + readonly registrations: Map; + + constructor(fixturesList: FixturesWithLocation[], parentPool?: FixturePool) { + this.registrations = new Map(parentPool ? parentPool.registrations : []); + + for (const { fixtures, location } of fixturesList) { + try { + for (const entry of Object.entries(fixtures)) { + const name = entry[0]; + let value = entry[1]; + let options: { auto: boolean, scope: FixtureScope } | undefined; + if (Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1])) { + options = { + auto: !!value[1].auto, + scope: value[1].scope || 'test' + }; + value = value[0]; + } + const fn = value as (Function | any); + + const previous = this.registrations.get(name); + if (previous && options) { + if (previous.scope !== options.scope) + throw errorWithLocations(`Fixture "${name}" has already been registered as a { scope: '${previous.scope}' } fixture.`, { location, name}, previous); + if (previous.auto !== options.auto) + throw errorWithLocations(`Fixture "${name}" has already been registered as a { auto: '${previous.scope}' } fixture.`, { location, name }, previous); + } else if (previous) { + options = { auto: previous.auto, scope: previous.scope }; + } else if (!options) { + options = { auto: false, scope: 'test' }; + } + + const deps = fixtureParameterNames(fn, location); + const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, deps, super: previous }; + registrationId(registration); + this.registrations.set(name, registration); + } + } catch (e) { + prependErrorMessage(e, `Error processing fixtures at ${formatLocation(location)}:\n`); + throw e; + } + } + + this.digest = this.validate(); + } + + private validate() { + const markers = new Map(); + const stack: FixtureRegistration[] = []; + const visit = (registration: FixtureRegistration) => { + markers.set(registration, 'visiting'); + stack.push(registration); + for (const name of registration.deps) { + const dep = this.resolveDependency(registration, name); + if (!dep) { + if (name === registration.name) + throw errorWithLocations(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration); + else + throw errorWithLocations(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration); + } + if (registration.scope === 'worker' && dep.scope === 'test') + throw errorWithLocations(`Worker fixture "${registration.name}" cannot depend on a test fixture "${name}".`, registration, dep); + if (!markers.has(dep)) { + visit(dep); + } else if (markers.get(dep) === 'visiting') { + const index = stack.indexOf(dep); + const regs = stack.slice(index, stack.length); + const names = regs.map(r => `"${r.name}"`); + throw errorWithLocations(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle.`, ...regs); + } + } + markers.set(registration, 'visited'); + stack.pop(); + }; + + const hash = crypto.createHash('sha1'); + const names = Array.from(this.registrations.keys()).sort(); + for (const name of names) { + const registration = this.registrations.get(name)!; + visit(registration); + if (registration.scope === 'worker') + hash.update(registration.id + ';'); + } + return hash.digest('hex'); + } + + validateFunction(fn: Function, prefix: string, allowTestFixtures: boolean, location: Location) { + const visit = (registration: FixtureRegistration) => { + for (const name of registration.deps) + visit(this.resolveDependency(registration, name)!); + }; + for (const name of fixtureParameterNames(fn, location)) { + const registration = this.registrations.get(name); + if (!registration) + throw errorWithLocations(`${prefix} has unknown parameter "${name}".`, { location, name: prefix, quoted: false }); + if (!allowTestFixtures && registration.scope === 'test') + throw errorWithLocations(`${prefix} cannot depend on a test fixture "${name}".`, { location, name: prefix, quoted: false }, registration); + visit(registration); + } + } + + resolveDependency(registration: FixtureRegistration, name: string): FixtureRegistration | undefined { + if (name === registration.name) + return registration.super; + return this.registrations.get(name); + } +} + +export class FixtureRunner { + private testScopeClean = true; + pool: FixturePool | undefined; + instanceForId = new Map(); + + setPool(pool: FixturePool) { + if (!this.testScopeClean) + throw new Error('Did not teardown test scope'); + if (this.pool && pool.digest !== this.pool.digest) + throw new Error('Digests do not match'); + this.pool = pool; + } + + async teardownScope(scope: string) { + for (const [, fixture] of this.instanceForId) { + if (fixture.registration.scope === scope) + await fixture.teardown(); + } + if (scope === 'test') + this.testScopeClean = true; + } + + async resolveParametersAndRunHookOrTest(fn: Function, scope: FixtureScope, info: any) { + // Install all automatic fixtures. + for (const registration of this.pool!.registrations.values()) { + const shouldSkip = scope === 'worker' && registration.scope === 'test'; + if (registration.auto && !shouldSkip) + await this.setupFixtureForRegistration(registration, info); + } + + // Install used fixtures. + const names = fixtureParameterNames(fn, { file: '', line: 1, column: 1 }); + const params: { [key: string]: any } = {}; + for (const name of names) { + const registration = this.pool!.registrations.get(name); + if (!registration) + throw errorWithCallLocation('Unknown fixture: ' + name); + const fixture = await this.setupFixtureForRegistration(registration, info); + params[name] = fixture.value; + } + + return fn(params, info); + } + + async setupFixtureForRegistration(registration: FixtureRegistration, info: any): Promise { + if (registration.scope === 'test') + this.testScopeClean = false; + + let fixture = this.instanceForId.get(registration.id); + if (fixture) + return fixture; + + fixture = new Fixture(this, registration); + this.instanceForId.set(registration.id, fixture); + await fixture.setup(info); + return fixture; + } +} + +export function inheritFixtureParameterNames(from: Function, to: Function, location: Location) { + if (!(to as any)[signatureSymbol]) + (to as any)[signatureSymbol] = innerFixtureParameterNames(from, location); +} + +const signatureSymbol = Symbol('signature'); + +function fixtureParameterNames(fn: Function | any, location: Location): string[] { + if (typeof fn !== 'function') + return []; + if (!fn[signatureSymbol]) + fn[signatureSymbol] = innerFixtureParameterNames(fn, location); + return fn[signatureSymbol]; +} + +function innerFixtureParameterNames(fn: Function, location: Location): string[] { + const text = fn.toString(); + const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/); + if (!match) + return []; + const trimmedParams = match[1].trim(); + if (!trimmedParams) + return []; + const [firstParam] = splitByComma(trimmedParams); + if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}') + throw errorWithLocations('First argument must use the object destructuring pattern: ' + firstParam, { location }); + const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => { + const colon = prop.indexOf(':'); + return colon === -1 ? prop : prop.substring(0, colon).trim(); + }); + return props; +} + +function splitByComma(s: string) { + const result: string[] = []; + const stack: string[] = []; + let start = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === '{' || s[i] === '[') { + stack.push(s[i] === '{' ? '}' : ']'); + } else if (s[i] === stack[stack.length - 1]) { + stack.pop(); + } else if (!stack.length && s[i] === ',') { + const token = s.substring(start, i).trim(); + if (token) + result.push(token); + start = i + 1; + } + } + const lastToken = s.substring(start).trim(); + if (lastToken) + result.push(lastToken); + return result; +} + +// name + superId, fn -> id +const registrationIdMap = new Map>(); +let lastId = 0; + +function registrationId(registration: FixtureRegistration): string { + if (registration.id) + return registration.id; + const key = registration.name + '@@@' + (registration.super ? registrationId(registration.super) : ''); + let map = registrationIdMap.get(key); + if (!map) { + map = new Map(); + registrationIdMap.set(key, map); + } + if (!map.has(registration.fn)) + map.set(registration.fn, String(lastId++)); + registration.id = map.get(registration.fn)!; + return registration.id; +} + +function errorWithLocations(message: string, ...defined: { location: Location, name?: string, quoted?: boolean }[]): Error { + for (const { name, location, quoted } of defined) { + let prefix = ''; + if (name && quoted === false) + prefix = name + ' '; + else if (name) + prefix = `"${name}" `; + message += `\n ${prefix}defined at ${formatLocation(location)}`; + } + const error = new Error(message); + error.stack = 'Error: ' + message + '\n'; + return error; +} diff --git a/src/test/globals.ts b/src/test/globals.ts new file mode 100644 index 0000000000..d2577ee5f5 --- /dev/null +++ b/src/test/globals.ts @@ -0,0 +1,34 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TestInfo } from './types'; +import { Suite } from './test'; + +let currentTestInfoValue: TestInfo | null = null; +export function setCurrentTestInfo(testInfo: TestInfo | null) { + currentTestInfoValue = testInfo; +} +export function currentTestInfo(): TestInfo | null { + return currentTestInfoValue; +} + +let currentFileSuite: Suite | undefined; +export function setCurrentlyLoadingFileSuite(suite: Suite | undefined) { + currentFileSuite = suite; +} +export function currentlyLoadingFileSuite() { + return currentFileSuite; +} diff --git a/src/test/golden.ts b/src/test/golden.ts new file mode 100644 index 0000000000..ada8df22ea --- /dev/null +++ b/src/test/golden.ts @@ -0,0 +1,179 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import colors from 'colors/safe'; +import fs from 'fs'; +import path from 'path'; +import jpeg from 'jpeg-js'; +import pixelmatch from 'pixelmatch'; +import { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } from '../third_party/diff_match_patch'; +import { UpdateSnapshots } from './types'; + +// Note: we require the pngjs version of pixelmatch to avoid version mismatches. +const { PNG } = require(require.resolve('pngjs', { paths: [require.resolve('pixelmatch')] })); + +const extensionToMimeType: { [key: string]: string } = { + 'dat': 'application/octet-string', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'png': 'image/png', + 'txt': 'text/plain', +}; + +const GoldenComparators: { [key: string]: any } = { + 'application/octet-string': compareBuffers, + 'image/png': compareImages, + 'image/jpeg': compareImages, + 'text/plain': compareText, +}; + +function compareBuffers(actualBuffer: Buffer | string, expectedBuffer: Buffer, mimeType: string): { diff?: object; errorMessage?: string; } | null { + if (!actualBuffer || !(actualBuffer instanceof Buffer)) + return { errorMessage: 'Actual result should be Buffer.' }; + if (Buffer.compare(actualBuffer, expectedBuffer)) + return { errorMessage: 'Buffers differ' }; + return null; +} + +function compareImages(actualBuffer: Buffer | string, expectedBuffer: Buffer, mimeType: string, options = {}): { diff?: object; errorMessage?: string; } | null { + if (!actualBuffer || !(actualBuffer instanceof Buffer)) + return { errorMessage: 'Actual result should be Buffer.' }; + + const actual = mimeType === 'image/png' ? PNG.sync.read(actualBuffer) : jpeg.decode(actualBuffer); + const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer); + if (expected.width !== actual.width || expected.height !== actual.height) { + return { + errorMessage: `Sizes differ; expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. ` + }; + } + const diff = new PNG({width: expected.width, height: expected.height}); + const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { threshold: 0.2, ...options }); + return count > 0 ? { diff: PNG.sync.write(diff) } : null; +} + +function compareText(actual: Buffer | string, expectedBuffer: Buffer): { diff?: object; errorMessage?: string; diffExtension?: string; } | null { + if (typeof actual !== 'string') + return { errorMessage: 'Actual result should be string' }; + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) + return null; + const dmp = new diff_match_patch(); + const d = dmp.diff_main(expected, actual); + dmp.diff_cleanupSemantic(d); + return { + errorMessage: diff_prettyTerminal(d) + }; +} + +export function compare(actual: Buffer | string, name: string, snapshotPath: (name: string) => string, outputPath: (name: string) => string, updateSnapshots: UpdateSnapshots, options?: { threshold?: number }): { pass: boolean; message?: string; } { + const snapshotFile = snapshotPath(name); + if (!fs.existsSync(snapshotFile)) { + const writingActual = updateSnapshots === 'all' || updateSnapshots === 'missing'; + if (writingActual) { + fs.mkdirSync(path.dirname(snapshotFile), { recursive: true }); + fs.writeFileSync(snapshotFile, actual); + } + const message = snapshotFile + ' is missing in snapshots' + (writingActual ? ', writing actual.' : '.'); + if (updateSnapshots === 'all') { + console.log(message); + return { pass: true, message }; + } + return { pass: false, message }; + } + const expected = fs.readFileSync(snapshotFile); + const extension = path.extname(snapshotFile).substring(1); + const mimeType = extensionToMimeType[extension] || 'application/octet-string'; + const comparator = GoldenComparators[mimeType]; + if (!comparator) { + return { + pass: false, + message: 'Failed to find comparator with type ' + mimeType + ': ' + snapshotFile, + }; + } + + const result = comparator(actual, expected, mimeType, options); + if (!result) + return { pass: true }; + + if (updateSnapshots === 'all') { + fs.mkdirSync(path.dirname(snapshotFile), { recursive: true }); + fs.writeFileSync(snapshotFile, actual); + console.log(snapshotFile + ' does not match, writing actual.'); + return { + pass: true, + message: snapshotFile + ' running with --update-snapshots, writing actual.' + }; + } + const outputFile = outputPath(name); + const expectedPath = addSuffix(outputFile, '-expected'); + const actualPath = addSuffix(outputFile, '-actual'); + const diffPath = addSuffix(outputFile, '-diff'); + fs.writeFileSync(expectedPath, expected); + fs.writeFileSync(actualPath, actual); + if (result.diff) + fs.writeFileSync(diffPath, result.diff); + + const output = [ + colors.red(`Snapshot comparison failed:`), + ]; + if (result.errorMessage) { + output.push(''); + output.push(indent(result.errorMessage, ' ')); + } + output.push(''); + output.push(`Expected: ${colors.yellow(expectedPath)}`); + output.push(`Received: ${colors.yellow(actualPath)}`); + if (result.diff) + output.push(` Diff: ${colors.yellow(diffPath)}`); + + return { + pass: false, + message: output.join('\n'), + }; +} + +function indent(lines: string, tab: string) { + return lines.replace(/^(?=.+$)/gm, tab); +} + +function addSuffix(filePath: string, suffix: string, customExtension?: string): string { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +} + +function diff_prettyTerminal(diffs: diff_match_patch.Diff[]) { + const html = []; + for (let x = 0; x < diffs.length; x++) { + const op = diffs[x][0]; // Operation (insert, delete, equal) + const data = diffs[x][1]; // Text of change. + const text = data; + switch (op) { + case DIFF_INSERT: + html[x] = colors.green(text); + break; + case DIFF_DELETE: + html[x] = colors.strikethrough(colors.red(text)); + break; + case DIFF_EQUAL: + html[x] = text; + break; + } + } + return html.join(''); +} diff --git a/src/cli/fixtures.ts b/src/test/index.ts similarity index 96% rename from src/cli/fixtures.ts rename to src/test/index.ts index 3b487dc665..607cc8a684 100644 --- a/src/cli/fixtures.ts +++ b/src/test/index.ts @@ -15,12 +15,12 @@ */ import * as fs from 'fs'; -import * as folio from 'folio'; +import { test as base } from './internal'; import type { LaunchOptions, BrowserContextOptions, Page } from '../../types/types'; import type { PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '../../types/test'; -export * from 'folio'; -export const test = folio.test.extend({ +export * from './internal'; +export const test = base.extend({ defaultBrowserType: [ 'chromium', { scope: 'worker' } ], browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ], playwright: [ require('../inprocess'), { scope: 'worker' } ], @@ -149,5 +149,3 @@ export const test = folio.test.extend = rootTestType.test; +export default test; diff --git a/src/test/ipc.ts b/src/test/ipc.ts new file mode 100644 index 0000000000..cb3d17c8cb --- /dev/null +++ b/src/test/ipc.ts @@ -0,0 +1,67 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TestError } from './reporter'; +import type { Config, TestStatus } from './types'; + +export type SerializedLoaderData = { + defaultConfig: Config; + overrides: Config; + configFile: { file: string } | { rootDir: string }; +}; +export type WorkerInitParams = { + workerIndex: number; + repeatEachIndex: number; + projectIndex: number; + loader: SerializedLoaderData; +}; + +export type TestBeginPayload = { + testId: string; + workerIndex: number, +}; + +export type TestEndPayload = { + testId: string; + duration: number; + status: TestStatus; + error?: TestError; + expectedStatus: TestStatus; + annotations: { type: string, description?: string }[]; + timeout: number; +}; + +export type TestEntry = { + testId: string; + retry: number; +}; + +export type RunPayload = { + file: string; + entries: TestEntry[]; +}; + +export type DonePayload = { + failedTestId?: string; + fatalError?: any; + remaining: TestEntry[]; +}; + +export type TestOutputPayload = { + testId?: string; + text?: string; + buffer?: string; +}; diff --git a/src/test/loader.ts b/src/test/loader.ts new file mode 100644 index 0000000000..04bb202934 --- /dev/null +++ b/src/test/loader.ts @@ -0,0 +1,397 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { installTransform } from './transform'; +import type { FullConfig, Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types'; +import { errorWithCallLocation, isRegExp, mergeObjects, prependErrorMessage } from './util'; +import { setCurrentlyLoadingFileSuite } from './globals'; +import { Suite } from './test'; +import { SerializedLoaderData } from './ipc'; +import * as path from 'path'; +import { ProjectImpl } from './project'; +import { Reporter } from './reporter'; + +export class Loader { + private _defaultConfig: Config; + private _configOverrides: Config; + private _fullConfig: FullConfig; + private _config: Config = {}; + private _configFile: string | undefined; + private _projects: ProjectImpl[] = []; + private _fileSuites = new Map(); + + constructor(defaultConfig: Config, configOverrides: Config) { + this._defaultConfig = defaultConfig; + this._configOverrides = configOverrides; + this._fullConfig = baseFullConfig; + } + + static deserialize(data: SerializedLoaderData): Loader { + const loader = new Loader(data.defaultConfig, data.overrides); + if ('file' in data.configFile) + loader.loadConfigFile(data.configFile.file); + else + loader.loadEmptyConfig(data.configFile.rootDir); + return loader; + } + + loadConfigFile(file: string): Config { + if (this._configFile) + throw new Error('Cannot load two config files'); + const revertBabelRequire = installTransform(); + try { + let config = require(file); + if (config && typeof config === 'object' && ('default' in config)) + config = config['default']; + this._config = config; + const rawConfig = { ...config }; + this._processConfigObject(path.dirname(file)); + this._configFile = file; + return rawConfig; + } catch (e) { + prependErrorMessage(e, `Error while reading ${file}:\n`); + throw e; + } finally { + revertBabelRequire(); + } + } + + loadEmptyConfig(rootDir: string) { + this._config = {}; + this._processConfigObject(rootDir); + } + + private _processConfigObject(rootDir: string) { + validateConfig(this._config); + + const configUse = mergeObjects(this._defaultConfig.use, this._config.use); + this._config = mergeObjects(mergeObjects(this._defaultConfig, this._config), { use: configUse }); + + if (('testDir' in this._config) && this._config.testDir !== undefined && !path.isAbsolute(this._config.testDir)) + this._config.testDir = path.resolve(rootDir, this._config.testDir); + const projects: Project[] = ('projects' in this._config) && this._config.projects !== undefined ? this._config.projects : [this._config]; + + this._fullConfig.rootDir = this._config.testDir || rootDir; + this._fullConfig.forbidOnly = takeFirst(this._configOverrides.forbidOnly, this._config.forbidOnly, baseFullConfig.forbidOnly); + this._fullConfig.globalSetup = takeFirst(this._configOverrides.globalSetup, this._config.globalSetup, baseFullConfig.globalSetup); + this._fullConfig.globalTeardown = takeFirst(this._configOverrides.globalTeardown, this._config.globalTeardown, baseFullConfig.globalTeardown); + this._fullConfig.globalTimeout = takeFirst(this._configOverrides.globalTimeout, this._configOverrides.globalTimeout, this._config.globalTimeout, baseFullConfig.globalTimeout); + this._fullConfig.grep = takeFirst(this._configOverrides.grep, this._config.grep, baseFullConfig.grep); + this._fullConfig.maxFailures = takeFirst(this._configOverrides.maxFailures, this._config.maxFailures, baseFullConfig.maxFailures); + this._fullConfig.preserveOutput = takeFirst(this._configOverrides.preserveOutput, this._config.preserveOutput, baseFullConfig.preserveOutput); + this._fullConfig.reporter = takeFirst(toReporters(this._configOverrides.reporter), toReporters(this._config.reporter), baseFullConfig.reporter); + this._fullConfig.quiet = takeFirst(this._configOverrides.quiet, this._config.quiet, baseFullConfig.quiet); + this._fullConfig.shard = takeFirst(this._configOverrides.shard, this._config.shard, baseFullConfig.shard); + this._fullConfig.updateSnapshots = takeFirst(this._configOverrides.updateSnapshots, this._config.updateSnapshots, baseFullConfig.updateSnapshots); + this._fullConfig.workers = takeFirst(this._configOverrides.workers, this._config.workers, baseFullConfig.workers); + + for (const project of projects) + this._addProject(project, this._fullConfig.rootDir); + this._fullConfig.projects = this._projects.map(p => p.config); + } + + loadTestFile(file: string) { + if (this._fileSuites.has(file)) + return this._fileSuites.get(file)!; + const revertBabelRequire = installTransform(); + try { + const suite = new Suite(''); + suite.file = file; + setCurrentlyLoadingFileSuite(suite); + require(file); + this._fileSuites.set(file, suite); + return suite; + } catch (e) { + prependErrorMessage(e, `Error while reading ${file}:\n`); + throw e; + } finally { + revertBabelRequire(); + setCurrentlyLoadingFileSuite(undefined); + } + } + + loadGlobalHook(file: string, name: string): (config: FullConfig) => any { + const revertBabelRequire = installTransform(); + try { + let hook = require(file); + if (hook && typeof hook === 'object' && ('default' in hook)) + hook = hook['default']; + if (typeof hook !== 'function') + throw errorWithCallLocation(`${name} file must export a single function.`); + return hook; + } catch (e) { + prependErrorMessage(e, `Error while reading ${file}:\n`); + throw e; + } finally { + revertBabelRequire(); + } + } + + loadReporter(file: string): new (arg?: any) => Reporter { + const revertBabelRequire = installTransform(); + try { + let func = require(path.resolve(this._fullConfig.rootDir, file)); + if (func && typeof func === 'object' && ('default' in func)) + func = func['default']; + if (typeof func !== 'function') + throw errorWithCallLocation(`Reporter file "${file}" must export a single class.`); + return func; + } catch (e) { + prependErrorMessage(e, `Error while reading ${file}:\n`); + throw e; + } finally { + revertBabelRequire(); + } + } + + fullConfig(): FullConfig { + return this._fullConfig; + } + + projects() { + return this._projects; + } + + fileSuites() { + return this._fileSuites; + } + + serialize(): SerializedLoaderData { + return { + defaultConfig: this._defaultConfig, + configFile: this._configFile ? { file: this._configFile } : { rootDir: this._fullConfig.rootDir }, + overrides: this._configOverrides, + }; + } + + private _addProject(projectConfig: Project, rootDir: string) { + let testDir = takeFirst(projectConfig.testDir, rootDir); + if (!path.isAbsolute(testDir)) + testDir = path.resolve(rootDir, testDir); + + const fullProject: FullProject = { + define: takeFirst(this._configOverrides.define, projectConfig.define, this._config.define, []), + outputDir: takeFirst(this._configOverrides.outputDir, projectConfig.outputDir, this._config.outputDir, path.resolve(process.cwd(), 'test-results')), + repeatEach: takeFirst(this._configOverrides.repeatEach, projectConfig.repeatEach, this._config.repeatEach, 1), + retries: takeFirst(this._configOverrides.retries, projectConfig.retries, this._config.retries, 0), + metadata: takeFirst(this._configOverrides.metadata, projectConfig.metadata, this._config.metadata, undefined), + name: takeFirst(this._configOverrides.name, projectConfig.name, this._config.name, ''), + testDir, + testIgnore: takeFirst(this._configOverrides.testIgnore, projectConfig.testIgnore, this._config.testIgnore, []), + testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, this._config.testMatch, '**/?(*.)+(spec|test).[jt]s'), + timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, this._config.timeout, 10000), + use: mergeObjects(mergeObjects(this._config.use, projectConfig.use), this._configOverrides.use), + }; + this._projects.push(new ProjectImpl(fullProject, this._projects.length)); + } +} + +function takeFirst(...args: (T | undefined)[]): T { + for (const arg of args) { + if (arg !== undefined) + return arg; + } + return undefined as any as T; +} + +function toReporters(reporters: 'dot' | 'line' | 'list' | 'junit' | 'json' | 'null' | ReporterDescription[] | undefined): ReporterDescription[] | undefined { + if (!reporters) + return; + if (typeof reporters === 'string') + return [ [reporters] ]; + return reporters; +} + +function validateConfig(config: Config) { + if (typeof config !== 'object' || !config) + throw new Error(`Configuration file must export a single object`); + + validateProject(config, 'config'); + + if ('forbidOnly' in config && config.forbidOnly !== undefined) { + if (typeof config.forbidOnly !== 'boolean') + throw new Error(`config.forbidOnly must be a boolean`); + } + + if ('globalSetup' in config && config.globalSetup !== undefined) { + if (typeof config.globalSetup !== 'string') + throw new Error(`config.globalSetup must be a string`); + } + + if ('globalTeardown' in config && config.globalTeardown !== undefined) { + if (typeof config.globalTeardown !== 'string') + throw new Error(`config.globalTeardown must be a string`); + } + + if ('globalTimeout' in config && config.globalTimeout !== undefined) { + if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0) + throw new Error(`config.globalTimeout must be a non-negative number`); + } + + if ('grep' in config && config.grep !== undefined) { + if (Array.isArray(config.grep)) { + config.grep.forEach((item, index) => { + if (!isRegExp(item)) + throw new Error(`config.grep[${index}] must be a RegExp`); + }); + } else if (!isRegExp(config.grep)) { + throw new Error(`config.grep must be a RegExp`); + } + } + + if ('maxFailures' in config && config.maxFailures !== undefined) { + if (typeof config.maxFailures !== 'number' || config.maxFailures < 0) + throw new Error(`config.maxFailures must be a non-negative number`); + } + + if ('preserveOutput' in config && config.preserveOutput !== undefined) { + if (typeof config.preserveOutput !== 'string' || !['always', 'never', 'failures-only'].includes(config.preserveOutput)) + throw new Error(`config.preserveOutput must be one of "always", "never" or "failures-only"`); + } + + if ('projects' in config && config.projects !== undefined) { + if (!Array.isArray(config.projects)) + throw new Error(`config.projects must be an array`); + config.projects.forEach((project, index) => { + validateProject(project, `config.projects[${index}]`); + }); + } + + if ('quiet' in config && config.quiet !== undefined) { + if (typeof config.quiet !== 'boolean') + throw new Error(`config.quiet must be a boolean`); + } + + if ('reporter' in config && config.reporter !== undefined) { + if (Array.isArray(config.reporter)) { + config.reporter.forEach((item, index) => { + if (!Array.isArray(item) || item.length <= 0 || item.length > 2 || typeof item[0] !== 'string') + throw new Error(`config.reporter[${index}] must be a tuple [name, optionalArgument]`); + }); + } else { + const builtinReporters = ['dot', 'line', 'list', 'junit', 'json', 'null']; + if (typeof config.reporter !== 'string' || !builtinReporters.includes(config.reporter)) + throw new Error(`config.reporter must be one of ${builtinReporters.map(name => `"${name}"`).join(', ')}`); + } + } + + if ('shard' in config && config.shard !== undefined && config.shard !== null) { + if (!config.shard || typeof config.shard !== 'object') + throw new Error(`config.shard must be an object`); + if (!('total' in config.shard) || typeof config.shard.total !== 'number' || config.shard.total < 1) + throw new Error(`config.shard.total must be a positive number`); + if (!('current' in config.shard) || typeof config.shard.current !== 'number' || config.shard.current < 1 || config.shard.current > config.shard.total) + throw new Error(`config.shard.current must be a positive number, not greater than config.shard.total`); + } + + if ('updateSnapshots' in config && config.updateSnapshots !== undefined) { + if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots)) + throw new Error(`config.updateSnapshots must be one of "all", "none" or "missing"`); + } + + if ('workers' in config && config.workers !== undefined) { + if (typeof config.workers !== 'number' || config.workers <= 0) + throw new Error(`config.workers must be a positive number`); + } +} + +function validateProject(project: Project, title: string) { + if (typeof project !== 'object' || !project) + throw new Error(`${title} must be an object`); + + if ('define' in project && project.define !== undefined) { + if (Array.isArray(project.define)) { + project.define.forEach((item, index) => { + validateDefine(item, `${title}.define[${index}]`); + }); + } else { + validateDefine(project.define, `${title}.define`); + } + } + + if ('name' in project && project.name !== undefined) { + if (typeof project.name !== 'string') + throw new Error(`${title}.name must be a string`); + } + + if ('outputDir' in project && project.outputDir !== undefined) { + if (typeof project.outputDir !== 'string') + throw new Error(`${title}.outputDir must be a string`); + if (!path.isAbsolute(project.outputDir)) + throw new Error(`${title}.outputDir must be an absolute path`); + } + + if ('repeatEach' in project && project.repeatEach !== undefined) { + if (typeof project.repeatEach !== 'number' || project.repeatEach < 0) + throw new Error(`${title}.repeatEach must be a non-negative number`); + } + + if ('retries' in project && project.retries !== undefined) { + if (typeof project.retries !== 'number' || project.retries < 0) + throw new Error(`${title}.retries must be a non-negative number`); + } + + if ('testDir' in project && project.testDir !== undefined) { + if (typeof project.testDir !== 'string') + throw new Error(`${title}.testDir must be a string`); + } + + for (const prop of ['testIgnore', 'testMatch'] as const) { + if (prop in project && project[prop] !== undefined) { + const value = project[prop]; + if (Array.isArray(value)) { + value.forEach((item, index) => { + if (typeof item !== 'string' && !isRegExp(item)) + throw new Error(`${title}.${prop}[${index}] must be a string or a RegExp`); + }); + } else if (typeof value !== 'string' && !isRegExp(value)) { + throw new Error(`${title}.${prop} must be a string or a RegExp`); + } + } + } + + if ('timeout' in project && project.timeout !== undefined) { + if (typeof project.timeout !== 'number' || project.timeout < 0) + throw new Error(`${title}.timeout must be a non-negative number`); + } + + if ('use' in project && project.use !== undefined) { + if (!project.use || typeof project.use !== 'object') + throw new Error(`${title}.use must be an object`); + } +} + +function validateDefine(define: any, title: string) { + if (!define || typeof define !== 'object' || !define.test || !define.fixtures) + throw new Error(`${title} must be an object with "test" and "fixtures" properties`); +} + +const baseFullConfig: FullConfig = { + forbidOnly: false, + globalSetup: null, + globalTeardown: null, + globalTimeout: 0, + grep: /.*/, + maxFailures: 0, + preserveOutput: 'always', + projects: [], + reporter: [ ['list'] ], + rootDir: path.resolve(process.cwd()), + quiet: false, + shard: null, + updateSnapshots: 'missing', + workers: 1, +}; diff --git a/src/test/project.ts b/src/test/project.ts new file mode 100644 index 0000000000..f532a33aed --- /dev/null +++ b/src/test/project.ts @@ -0,0 +1,109 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TestType, FullProject, Fixtures, FixturesWithLocation } from './types'; +import { Spec, Test } from './test'; +import { FixturePool } from './fixtures'; +import { DeclaredFixtures, TestTypeImpl } from './testType'; + +export class ProjectImpl { + config: FullProject; + private index: number; + private defines = new Map, Fixtures>(); + private testTypePools = new Map(); + private specPools = new Map(); + + constructor(project: FullProject, index: number) { + this.config = project; + this.index = index; + this.defines = new Map(); + for (const { test, fixtures } of Array.isArray(project.define) ? project.define : [project.define]) + this.defines.set(test, fixtures); + } + + private buildTestTypePool(testType: TestTypeImpl): FixturePool { + if (!this.testTypePools.has(testType)) { + const fixtures = this.resolveFixtures(testType); + const overrides: Fixtures = this.config.use; + const overridesWithLocation = { + fixtures: overrides, + location: { + file: ``, + line: 1, + column: 1, + } + }; + const pool = new FixturePool([...fixtures, overridesWithLocation]); + this.testTypePools.set(testType, pool); + } + return this.testTypePools.get(testType)!; + } + + buildPool(spec: Spec): FixturePool { + if (!this.specPools.has(spec)) { + let pool = this.buildTestTypePool(spec._testType); + const overrides: Fixtures = spec.parent!._buildFixtureOverrides(); + if (Object.entries(overrides).length) { + const overridesWithLocation = { + fixtures: overrides, + location: { + file: spec.file, + line: 1, // TODO: capture location + column: 1, // TODO: capture location + } + }; + pool = new FixturePool([overridesWithLocation], pool); + } + this.specPools.set(spec, pool); + + pool.validateFunction(spec.fn, 'Test', true, spec); + for (let parent = spec.parent; parent; parent = parent.parent) { + for (const hook of parent._hooks) + pool.validateFunction(hook.fn, hook.type + ' hook', hook.type === 'beforeEach' || hook.type === 'afterEach', hook.location); + } + } + return this.specPools.get(spec)!; + } + + generateTests(spec: Spec, repeatEachIndex?: number) { + const digest = this.buildPool(spec).digest; + const min = repeatEachIndex === undefined ? 0 : repeatEachIndex; + const max = repeatEachIndex === undefined ? this.config.repeatEach - 1 : repeatEachIndex; + const tests: Test[] = []; + for (let i = min; i <= max; i++) { + const test = new Test(spec); + test.projectName = this.config.name; + test.retries = this.config.retries; + test._repeatEachIndex = i; + test._projectIndex = this.index; + test._workerHash = `run${this.index}-${digest}-repeat${i}`; + test._id = `${spec._ordinalInFile}@${spec.file}#run${this.index}-repeat${i}`; + spec.tests.push(test); + tests.push(test); + } + return tests; + } + + private resolveFixtures(testType: TestTypeImpl): FixturesWithLocation[] { + return testType.fixtures.map(f => { + if (f instanceof DeclaredFixtures) { + const fixtures = this.defines.get(f.testType.test) || {}; + return { fixtures, location: f.location }; + } + return f; + }); + } +} diff --git a/src/test/reporter.ts b/src/test/reporter.ts new file mode 100644 index 0000000000..e9b29050b3 --- /dev/null +++ b/src/test/reporter.ts @@ -0,0 +1,77 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { FullConfig, TestStatus } from './types'; +export type { FullConfig, TestStatus } from './types'; + +export interface Suite { + title: string; + file: string; + line: number; + column: number; + suites: Suite[]; + specs: Spec[]; + findTest(fn: (test: Test) => boolean | void): boolean; + findSpec(fn: (spec: Spec) => boolean | void): boolean; + totalTestCount(): number; +} +export interface Spec { + suite: Suite; + title: string; + file: string; + line: number; + column: number; + tests: Test[]; + fullTitle(): string; + ok(): boolean; +} +export interface Test { + spec: Spec; + results: TestResult[]; + skipped: boolean; + expectedStatus: TestStatus; + timeout: number; + annotations: { type: string, description?: string }[]; + projectName: string; + retries: number; + fullTitle(): string; + status(): 'skipped' | 'expected' | 'unexpected' | 'flaky'; + ok(): boolean; +} +export interface TestResult { + retry: number; + workerIndex: number, + duration: number; + status?: TestStatus; + error?: TestError; + stdout: (string | Buffer)[]; + stderr: (string | Buffer)[]; +} +export interface TestError { + message?: string; + stack?: string; + value?: string; +} +export interface Reporter { + onBegin(config: FullConfig, suite: Suite): void; + onTestBegin(test: Test): void; + onStdOut(chunk: string | Buffer, test?: Test): void; + onStdErr(chunk: string | Buffer, test?: Test): void; + onTestEnd(test: Test, result: TestResult): void; + onTimeout(timeout: number): void; + onError(error: TestError): void; + onEnd(): void; +} diff --git a/src/test/reporters/base.ts b/src/test/reporters/base.ts new file mode 100644 index 0000000000..1b0be934ce --- /dev/null +++ b/src/test/reporters/base.ts @@ -0,0 +1,245 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { codeFrameColumns } from '@babel/code-frame'; +import colors from 'colors/safe'; +import fs from 'fs'; +// @ts-ignore +import milliseconds from 'ms'; +import path from 'path'; +import StackUtils from 'stack-utils'; +import { FullConfig, TestStatus, Test, Suite, TestResult, TestError, Reporter } from '../reporter'; + +const stackUtils = new StackUtils(); + +export class BaseReporter implements Reporter { + duration = 0; + config!: FullConfig; + suite!: Suite; + timeout: number = 0; + fileDurations = new Map(); + monotonicStartTime: number = 0; + + constructor() { + } + + onBegin(config: FullConfig, suite: Suite) { + this.monotonicStartTime = monotonicTime(); + this.config = config; + this.suite = suite; + } + + onTestBegin(test: Test) { + } + + onStdOut(chunk: string | Buffer) { + if (!this.config.quiet) + process.stdout.write(chunk); + } + + onStdErr(chunk: string | Buffer) { + if (!this.config.quiet) + process.stderr.write(chunk); + } + + onTestEnd(test: Test, result: TestResult) { + const spec = test.spec; + let duration = this.fileDurations.get(spec.file) || 0; + duration += result.duration; + this.fileDurations.set(spec.file, duration); + } + + onError(error: TestError) { + console.log(formatError(error)); + } + + onTimeout(timeout: number) { + this.timeout = timeout; + } + + onEnd() { + this.duration = monotonicTime() - this.monotonicStartTime; + } + + private _printSlowTests() { + const fileDurations = [...this.fileDurations.entries()]; + fileDurations.sort((a, b) => b[1] - a[1]); + for (let i = 0; i < 10 && i < fileDurations.length; ++i) { + const baseName = path.basename(fileDurations[i][0]); + const duration = fileDurations[i][1]; + if (duration < 15000) + break; + console.log(colors.yellow(' Slow test: ') + baseName + colors.yellow(` (${milliseconds(duration)})`)); + } + } + + epilogue(full: boolean) { + let skipped = 0; + let expected = 0; + const unexpected: Test[] = []; + const flaky: Test[] = []; + + this.suite.findTest(test => { + switch (test.status()) { + case 'skipped': ++skipped; break; + case 'expected': ++expected; break; + case 'unexpected': unexpected.push(test); break; + case 'flaky': flaky.push(test); break; + } + }); + + if (full && unexpected.length) { + console.log(''); + this._printFailures(unexpected); + } + + this._printSlowTests(); + + console.log(''); + if (unexpected.length) { + console.log(colors.red(` ${unexpected.length} failed`)); + this._printTestHeaders(unexpected); + } + if (flaky.length) { + console.log(colors.red(` ${flaky.length} flaky`)); + this._printTestHeaders(flaky); + } + if (skipped) + console.log(colors.yellow(` ${skipped} skipped`)); + if (expected) + console.log(colors.green(` ${expected} passed`) + colors.dim(` (${milliseconds(this.duration)})`)); + if (this.timeout) + console.log(colors.red(` Timed out waiting ${this.timeout / 1000}s for the entire test run`)); + } + + private _printTestHeaders(tests: Test[]) { + tests.forEach(test => { + console.log(formatTestHeader(this.config, test, ' ')); + }); + } + + private _printFailures(failures: Test[]) { + failures.forEach((test, index) => { + console.log(formatFailure(this.config, test, index + 1)); + }); + } + + hasResultWithStatus(test: Test, status: TestStatus): boolean { + return !!test.results.find(r => r.status === status); + } + + willRetry(test: Test, result: TestResult): boolean { + return result.status !== 'passed' && result.status !== test.expectedStatus && test.results.length <= test.retries; + } +} + +export function formatFailure(config: FullConfig, test: Test, index?: number): string { + const tokens: string[] = []; + tokens.push(formatTestHeader(config, test, ' ', index)); + for (const result of test.results) { + if (result.status === 'passed') + continue; + tokens.push(formatFailedResult(test, result)); + } + tokens.push(''); + return tokens.join('\n'); +} + +export function formatTestTitle(config: FullConfig, test: Test): string { + const spec = test.spec; + let relativePath = path.relative(config.rootDir, spec.file) || path.basename(spec.file); + relativePath += ':' + spec.line + ':' + spec.column; + return `${relativePath} › ${test.fullTitle()}`; +} + +function formatTestHeader(config: FullConfig, test: Test, indent: string, index?: number): string { + const title = formatTestTitle(config, test); + const passedUnexpectedlySuffix = test.results[0].status === 'passed' ? ' -- passed unexpectedly' : ''; + const header = `${indent}${index ? index + ') ' : ''}${title}${passedUnexpectedlySuffix}`; + return colors.red(pad(header, '=')); +} + +function formatFailedResult(test: Test, result: TestResult): string { + const tokens: string[] = []; + if (result.retry) + tokens.push(colors.gray(pad(`\n Retry #${result.retry}`, '-'))); + if (result.status === 'timedOut') { + tokens.push(''); + tokens.push(indent(colors.red(`Timeout of ${test.timeout}ms exceeded.`), ' ')); + } else { + tokens.push(indent(formatError(result.error!, test.spec.file), ' ')); + } + return tokens.join('\n'); +} + +function formatError(error: TestError, file?: string) { + const stack = error.stack; + const tokens = []; + if (stack) { + tokens.push(''); + const message = error.message || ''; + const messageLocation = stack.indexOf(message); + const preamble = stack.substring(0, messageLocation + message.length); + tokens.push(preamble); + const position = file ? positionInFile(stack, file) : null; + if (position) { + const source = fs.readFileSync(file!, 'utf8'); + tokens.push(''); + tokens.push(codeFrameColumns(source, { + start: position, + }, + { highlightCode: colors.enabled } + )); + } + tokens.push(''); + tokens.push(colors.dim(preamble.length > 0 ? stack.substring(preamble.length + 1) : stack)); + } else { + tokens.push(''); + tokens.push(error.value); + } + return tokens.join('\n'); +} + +function pad(line: string, char: string): string { + return line + ' ' + colors.gray(char.repeat(Math.max(0, 100 - line.length - 1))); +} + +function indent(lines: string, tab: string) { + return lines.replace(/^(?=.+$)/gm, tab); +} + +function positionInFile(stack: string, file: string): { column: number; line: number; } { + // Stack will have /private/var/folders instead of /var/folders on Mac. + file = fs.realpathSync(file); + for (const line of stack.split('\n')) { + const parsed = stackUtils.parseLine(line); + if (!parsed || !parsed.file) + continue; + if (path.resolve(process.cwd(), parsed.file) === file) + return {column: parsed.column || 0, line: parsed.line || 0}; + } + return { column: 0, line: 0 }; +} + +function monotonicTime(): number { + const [seconds, nanoseconds] = process.hrtime(); + return seconds * 1000 + (nanoseconds / 1000000 | 0); +} + +const asciiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g'); +export function stripAscii(str: string): string { + return str.replace(asciiRegex, ''); +} diff --git a/src/test/reporters/dot.ts b/src/test/reporters/dot.ts new file mode 100644 index 0000000000..93739460ec --- /dev/null +++ b/src/test/reporters/dot.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import colors from 'colors/safe'; +import { BaseReporter } from './base'; +import { Test, TestResult } from '../reporter'; + +class DotReporter extends BaseReporter { + private _counter = 0; + + onTestEnd(test: Test, result: TestResult) { + super.onTestEnd(test, result); + if (++this._counter === 81) { + process.stdout.write('\n'); + return; + } + if (result.status === 'skipped') { + process.stdout.write(colors.yellow('°')); + return; + } + if (this.willRetry(test, result)) { + process.stdout.write(colors.gray('×')); + return; + } + switch (test.status()) { + case 'expected': process.stdout.write(colors.green('·')); break; + case 'unexpected': process.stdout.write(colors.red(test.results[test.results.length - 1].status === 'timedOut' ? 'T' : 'F')); break; + case 'flaky': process.stdout.write(colors.yellow('±')); break; + } + } + + onTimeout(timeout: number) { + super.onTimeout(timeout); + this.onEnd(); + } + + onEnd() { + super.onEnd(); + process.stdout.write('\n'); + this.epilogue(true); + } +} + +export default DotReporter; diff --git a/src/test/reporters/empty.ts b/src/test/reporters/empty.ts new file mode 100644 index 0000000000..4adc84034e --- /dev/null +++ b/src/test/reporters/empty.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FullConfig, TestResult, Test, Suite, TestError, Reporter } from '../reporter'; + +class EmptyReporter implements Reporter { + onBegin(config: FullConfig, suite: Suite) {} + onTestBegin(test: Test) {} + onStdOut(chunk: string | Buffer, test?: Test) {} + onStdErr(chunk: string | Buffer, test?: Test) {} + onTestEnd(test: Test, result: TestResult) {} + onTimeout(timeout: number) {} + onError(error: TestError) {} + onEnd() {} +} + +export default EmptyReporter; diff --git a/src/test/reporters/json.ts b/src/test/reporters/json.ts new file mode 100644 index 0000000000..2ecd590b5c --- /dev/null +++ b/src/test/reporters/json.ts @@ -0,0 +1,160 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import EmptyReporter from './empty'; +import { FullConfig, Test, Suite, Spec, TestResult, TestError } from '../reporter'; + +interface SerializedSuite { + title: string; + file: string; + column: number; + line: number; + specs: ReturnType[]; + suites?: SerializedSuite[]; +} + +export type ReportFormat = ReturnType; + +function toPosixPath(aPath: string): string { + return aPath.split(path.sep).join(path.posix.sep); +} + +class JSONReporter extends EmptyReporter { + config!: FullConfig; + suite!: Suite; + private _errors: TestError[] = []; + private _outputFile: string | undefined; + + constructor(options: { outputFile?: string } = {}) { + super(); + this._outputFile = options.outputFile; + } + + onBegin(config: FullConfig, suite: Suite) { + this.config = config; + this.suite = suite; + } + + onTimeout() { + this.onEnd(); + } + + onError(error: TestError): void { + this._errors.push(error); + } + + onEnd() { + outputReport(this._serializeReport(), this._outputFile); + } + + private _serializeReport() { + return { + config: { + ...this.config, + rootDir: toPosixPath(this.config.rootDir), + projects: this.config.projects.map(project => { + return { + outputDir: toPosixPath(project.outputDir), + repeatEach: project.repeatEach, + retries: project.retries, + metadata: project.metadata, + name: project.name, + testDir: toPosixPath(project.testDir), + testIgnore: serializePatterns(project.testIgnore), + testMatch: serializePatterns(project.testMatch), + timeout: project.timeout, + }; + }) + }, + suites: this.suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s), + errors: this._errors + }; + } + + private _serializeSuite(suite: Suite): null | SerializedSuite { + if (!suite.findSpec(test => true)) + return null; + const suites = suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s) as SerializedSuite[]; + return { + title: suite.title, + file: toPosixPath(path.relative(this.config.rootDir, suite.file)), + line: suite.line, + column: suite.column, + specs: suite.specs.map(test => this._serializeTestSpec(test)), + suites: suites.length ? suites : undefined, + }; + } + + private _serializeTestSpec(spec: Spec) { + return { + title: spec.title, + ok: spec.ok(), + tests: spec.tests.map(r => this._serializeTest(r)), + file: toPosixPath(path.relative(this.config.rootDir, spec.file)), + line: spec.line, + column: spec.column, + }; + } + + private _serializeTest(test: Test) { + return { + timeout: test.timeout, + annotations: test.annotations, + expectedStatus: test.expectedStatus, + projectName: test.projectName, + results: test.results.map(r => this._serializeTestResult(r)), + }; + } + + private _serializeTestResult(result: TestResult) { + return { + workerIndex: result.workerIndex, + status: result.status, + duration: result.duration, + error: result.error, + stdout: result.stdout.map(s => stdioEntry(s)), + stderr: result.stderr.map(s => stdioEntry(s)), + retry: result.retry, + }; + } +} + +function outputReport(report: ReportFormat, outputFile: string | undefined) { + const reportString = JSON.stringify(report, undefined, 2); + outputFile = outputFile || process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`]; + if (outputFile) { + fs.mkdirSync(path.dirname(outputFile), { recursive: true }); + fs.writeFileSync(outputFile, reportString); + } else { + console.log(reportString); + } +} + +function stdioEntry(s: string | Buffer): any { + if (typeof s === 'string') + return { text: s }; + return { buffer: s.toString('base64') }; +} + +function serializePatterns(patterns: string | RegExp | (string | RegExp)[]): string[] { + if (!Array.isArray(patterns)) + patterns = [patterns]; + return patterns.map(s => s.toString()); +} + +export default JSONReporter; diff --git a/src/test/reporters/junit.ts b/src/test/reporters/junit.ts new file mode 100644 index 0000000000..7c793bdc46 --- /dev/null +++ b/src/test/reporters/junit.ts @@ -0,0 +1,199 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import EmptyReporter from './empty'; +import { FullConfig, Suite, Test } from '../reporter'; +import { monotonicTime } from '../util'; +import { formatFailure, formatTestTitle, stripAscii } from './base'; + +class JUnitReporter extends EmptyReporter { + private config!: FullConfig; + private suite!: Suite; + private timestamp!: number; + private startTime!: number; + private totalTests = 0; + private totalFailures = 0; + private totalSkipped = 0; + private outputFile: string | undefined; + private stripANSIControlSequences = false; + + constructor(options: { outputFile?: string, stripANSIControlSequences?: boolean } = {}) { + super(); + this.outputFile = options.outputFile; + this.stripANSIControlSequences = options.stripANSIControlSequences || false; + } + + onBegin(config: FullConfig, suite: Suite) { + this.config = config; + this.suite = suite; + this.timestamp = Date.now(); + this.startTime = monotonicTime(); + } + + onEnd() { + const duration = monotonicTime() - this.startTime; + const children: XMLEntry[] = []; + for (const suite of this.suite.suites) + children.push(this._buildTestSuite(suite)); + const tokens: string[] = []; + + const self = this; + const root: XMLEntry = { + name: 'testsuites', + attributes: { + id: process.env[`PLAYWRIGHT_JUNIT_SUITE_ID`] || '', + name: process.env[`PLAYWRIGHT_JUNIT_SUITE_NAME`] || '', + tests: self.totalTests, + failures: self.totalFailures, + skipped: self.totalSkipped, + errors: 0, + time: duration / 1000 + }, + children + }; + + serializeXML(root, tokens, this.stripANSIControlSequences); + const reportString = tokens.join('\n'); + const outputFile = this.outputFile || process.env[`PLAYWRIGHT_JUNIT_OUTPUT_NAME`]; + if (outputFile) { + fs.mkdirSync(path.dirname(outputFile), { recursive: true }); + fs.writeFileSync(outputFile, reportString); + } else { + console.log(reportString); + } + } + + private _buildTestSuite(suite: Suite): XMLEntry { + let tests = 0; + let skipped = 0; + let failures = 0; + let duration = 0; + const children: XMLEntry[] = []; + + suite.findTest(test => { + ++tests; + if (test.skipped) + ++skipped; + if (!test.ok()) + ++failures; + for (const result of test.results) + duration += result.duration; + this._addTestCase(test, children); + }); + this.totalTests += tests; + this.totalSkipped += skipped; + this.totalFailures += failures; + + const entry: XMLEntry = { + name: 'testsuite', + attributes: { + name: path.relative(this.config.rootDir, suite.file), + timestamp: this.timestamp, + hostname: '', + tests, + failures, + skipped, + time: duration / 1000, + errors: 0, + }, + children + }; + + return entry; + } + + private _addTestCase(test: Test, entries: XMLEntry[]) { + const entry = { + name: 'testcase', + attributes: { + name: test.spec.fullTitle(), + classname: formatTestTitle(this.config, test), + time: (test.results.reduce((acc, value) => acc + value.duration, 0)) / 1000 + }, + children: [] as XMLEntry[] + }; + entries.push(entry); + + if (test.skipped) { + entry.children.push({ name: 'skipped'}); + return; + } + + if (!test.ok()) { + entry.children.push({ + name: 'failure', + attributes: { + message: `${path.basename(test.spec.file)}:${test.spec.line}:${test.spec.column} ${test.spec.title}`, + type: 'FAILURE', + }, + text: stripAscii(formatFailure(this.config, test)) + }); + } + for (const result of test.results) { + for (const stdout of result.stdout) { + entries.push({ + name: 'system-out', + text: stdout.toString() + }); + } + + for (const stderr of result.stderr) { + entries.push({ + name: 'system-err', + text: stderr.toString() + }); + } + } + } +} + +type XMLEntry = { + name: string; + attributes?: { [name: string]: string | number | boolean }; + children?: XMLEntry[]; + text?: string; +}; + +function serializeXML(entry: XMLEntry, tokens: string[], stripANSIControlSequences: boolean) { + const attrs: string[] = []; + for (const [name, value] of Object.entries(entry.attributes || {})) + attrs.push(`${name}="${escape(String(value), stripANSIControlSequences, false)}"`); + tokens.push(`<${entry.name}${attrs.length ? ' ' : ''}${attrs.join(' ')}>`); + for (const child of entry.children || []) + serializeXML(child, tokens, stripANSIControlSequences); + if (entry.text) + tokens.push(escape(entry.text, stripANSIControlSequences, true)); + tokens.push(``); +} + +// See https://en.wikipedia.org/wiki/Valid_characters_in_XML +const discouragedXMLCharacters = /[\u0001-\u0008\u000b-\u000c\u000e-\u001f\u007f-\u0084\u0086-\u009f]/g; +const ansiControlSequence = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g'); + +function escape(text: string, stripANSIControlSequences: boolean, isCharacterData: boolean): string { + if (stripANSIControlSequences) + text = text.replace(ansiControlSequence, ''); + const escapeRe = isCharacterData ? /[&<]/g : /[&"<>]/g; + text = text.replace(escapeRe, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); + if (isCharacterData) + text = text.replace(/]]>/g, ']]>'); + text = text.replace(discouragedXMLCharacters, ''); + return text; +} + +export default JUnitReporter; diff --git a/src/test/reporters/line.ts b/src/test/reporters/line.ts new file mode 100644 index 0000000000..a53a1607bc --- /dev/null +++ b/src/test/reporters/line.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import colors from 'colors/safe'; +import { BaseReporter, formatFailure, formatTestTitle } from './base'; +import { FullConfig, Test, Suite, TestResult } from '../reporter'; + +class LineReporter extends BaseReporter { + private _total = 0; + private _current = 0; + private _failures = 0; + private _lastTest: Test | undefined; + + onBegin(config: FullConfig, suite: Suite) { + super.onBegin(config, suite); + this._total = suite.totalTestCount(); + console.log(); + } + + onStdOut(chunk: string | Buffer, test?: Test) { + this._dumpToStdio(test, chunk, process.stdout); + } + + onStdErr(chunk: string | Buffer, test?: Test) { + this._dumpToStdio(test, chunk, process.stderr); + } + + private _dumpToStdio(test: Test | undefined, chunk: string | Buffer, stream: NodeJS.WriteStream) { + if (this.config.quiet) + return; + stream.write(`\u001B[1A\u001B[2K`); + if (test && this._lastTest !== test) { + // Write new header for the output. + stream.write(colors.gray(formatTestTitle(this.config, test) + `\n`)); + this._lastTest = test; + } + + stream.write(chunk); + console.log(); + } + + onTestEnd(test: Test, result: TestResult) { + super.onTestEnd(test, result); + const width = process.stdout.columns! - 1; + const title = `[${++this._current}/${this._total}] ${formatTestTitle(this.config, test)}`.substring(0, width); + process.stdout.write(`\u001B[1A\u001B[2K${title}\n`); + if (!this.willRetry(test, result) && !test.ok()) { + process.stdout.write(`\u001B[1A\u001B[2K`); + console.log(formatFailure(this.config, test, ++this._failures)); + console.log(); + } + } + + onEnd() { + process.stdout.write(`\u001B[1A\u001B[2K`); + super.onEnd(); + this.epilogue(false); + } +} + +export default LineReporter; diff --git a/src/test/reporters/list.ts b/src/test/reporters/list.ts new file mode 100644 index 0000000000..bbf5b36e71 --- /dev/null +++ b/src/test/reporters/list.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import colors from 'colors/safe'; +// @ts-ignore +import milliseconds from 'ms'; +import { BaseReporter, formatTestTitle } from './base'; +import { FullConfig, Suite, Test, TestResult } from '../reporter'; + +class ListReporter extends BaseReporter { + private _failure = 0; + private _lastRow = 0; + private _testRows = new Map(); + private _needNewLine = false; + + onBegin(config: FullConfig, suite: Suite) { + super.onBegin(config, suite); + console.log(); + } + + onTestBegin(test: Test) { + super.onTestBegin(test); + if (process.stdout.isTTY) { + if (this._needNewLine) { + this._needNewLine = false; + process.stdout.write('\n'); + this._lastRow++; + } + process.stdout.write(' ' + colors.gray(formatTestTitle(this.config, test) + ': ') + '\n'); + } + this._testRows.set(test, this._lastRow++); + } + + onStdOut(chunk: string | Buffer, test?: Test) { + this._dumpToStdio(test, chunk, process.stdout); + } + + onStdErr(chunk: string | Buffer, test?: Test) { + this._dumpToStdio(test, chunk, process.stdout); + } + + private _dumpToStdio(test: Test | undefined, chunk: string | Buffer, stream: NodeJS.WriteStream) { + if (this.config.quiet) + return; + const text = chunk.toString('utf-8'); + this._needNewLine = text[text.length - 1] !== '\n'; + if (process.stdout.isTTY) { + const newLineCount = text.split('\n').length - 1; + this._lastRow += newLineCount; + } + stream.write(chunk); + } + + onTestEnd(test: Test, result: TestResult) { + super.onTestEnd(test, result); + + const duration = colors.dim(` (${milliseconds(result.duration)})`); + const title = formatTestTitle(this.config, test); + let text = ''; + if (result.status === 'skipped') { + text = colors.green(' - ') + colors.cyan(title); + } else { + const statusMark = result.status === 'passed' ? ' ✓ ' : ' x '; + if (result.status === test.expectedStatus) + text = '\u001b[2K\u001b[0G' + colors.green(statusMark) + colors.gray(title) + duration; + else + text = '\u001b[2K\u001b[0G' + colors.red(`${statusMark}${++this._failure}) ` + title) + duration; + } + + const testRow = this._testRows.get(test)!; + // Go up if needed + if (process.stdout.isTTY && testRow !== this._lastRow) + process.stdout.write(`\u001B[${this._lastRow - testRow}A`); + // Erase line + if (process.stdout.isTTY) + process.stdout.write('\u001B[2K'); + if (!process.stdout.isTTY && this._needNewLine) { + this._needNewLine = false; + process.stdout.write('\n'); + } + process.stdout.write(text); + // Go down if needed. + if (testRow !== this._lastRow) { + if (process.stdout.isTTY) + process.stdout.write(`\u001B[${this._lastRow - testRow}E`); + else + process.stdout.write('\n'); + } + } + + onEnd() { + super.onEnd(); + process.stdout.write('\n'); + this.epilogue(true); + } +} + +export default ListReporter; diff --git a/src/test/reporters/multiplexer.ts b/src/test/reporters/multiplexer.ts new file mode 100644 index 0000000000..64a9162058 --- /dev/null +++ b/src/test/reporters/multiplexer.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FullConfig, Suite, Test, TestError, TestResult, Reporter } from '../reporter'; + +export class Multiplexer implements Reporter { + private _reporters: Reporter[]; + + constructor(reporters: Reporter[]) { + this._reporters = reporters; + } + + onBegin(config: FullConfig, suite: Suite) { + for (const reporter of this._reporters) + reporter.onBegin(config, suite); + } + + onTestBegin(test: Test) { + for (const reporter of this._reporters) + reporter.onTestBegin(test); + } + + onStdOut(chunk: string | Buffer, test?: Test) { + for (const reporter of this._reporters) + reporter.onStdOut(chunk, test); + } + + onStdErr(chunk: string | Buffer, test?: Test) { + for (const reporter of this._reporters) + reporter.onStdErr(chunk, test); + } + + onTestEnd(test: Test, result: TestResult) { + for (const reporter of this._reporters) + reporter.onTestEnd(test, result); + } + + onTimeout(timeout: number) { + for (const reporter of this._reporters) + reporter.onTimeout(timeout); + } + + onEnd() { + for (const reporter of this._reporters) + reporter.onEnd(); + } + + onError(error: TestError) { + for (const reporter of this._reporters) + reporter.onError(error); + } +} diff --git a/src/test/runner.ts b/src/test/runner.ts new file mode 100644 index 0000000000..609fee2fc8 --- /dev/null +++ b/src/test/runner.ts @@ -0,0 +1,319 @@ +/** + * Copyright 2019 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import rimraf from 'rimraf'; +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; +import { Dispatcher } from './dispatcher'; +import { createMatcher, monotonicTime, raceAgainstDeadline } from './util'; +import { Suite } from './test'; +import { Loader } from './loader'; +import { Reporter } from './reporter'; +import { Multiplexer } from './reporters/multiplexer'; +import DotReporter from './reporters/dot'; +import LineReporter from './reporters/line'; +import ListReporter from './reporters/list'; +import JSONReporter from './reporters/json'; +import JUnitReporter from './reporters/junit'; +import EmptyReporter from './reporters/empty'; +import { ProjectImpl } from './project'; +import { Minimatch } from 'minimatch'; +import { Config } from './types'; + +const removeFolderAsync = promisify(rimraf); +const readDirAsync = promisify(fs.readdir); +const readFileAsync = promisify(fs.readFile); + +type RunResult = 'passed' | 'failed' | 'sigint' | 'forbid-only' | 'no-tests' | 'timedout'; + +export class Runner { + private _loader: Loader; + private _reporter!: Reporter; + private _didBegin = false; + + constructor(defaultConfig: Config, configOverrides: Config) { + this._loader = new Loader(defaultConfig, configOverrides); + } + + private _createReporter() { + const reporters: Reporter[] = []; + const defaultReporters = { + dot: DotReporter, + line: LineReporter, + list: ListReporter, + json: JSONReporter, + junit: JUnitReporter, + null: EmptyReporter, + }; + for (const r of this._loader.fullConfig().reporter) { + const [name, arg] = r; + if (name in defaultReporters) { + reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg)); + } else { + const reporterConstructor = this._loader.loadReporter(name); + reporters.push(new reporterConstructor(arg)); + } + } + return new Multiplexer(reporters); + } + + loadConfigFile(file: string): Config { + return this._loader.loadConfigFile(file); + } + + loadEmptyConfig(rootDir: string) { + this._loader.loadEmptyConfig(rootDir); + } + + async run(list: boolean, testFileReFilters: RegExp[], projectName?: string): Promise { + this._reporter = this._createReporter(); + const config = this._loader.fullConfig(); + const globalDeadline = config.globalTimeout ? config.globalTimeout + monotonicTime() : undefined; + const { result, timedOut } = await raceAgainstDeadline(this._run(list, testFileReFilters, projectName), globalDeadline); + if (timedOut) { + if (!this._didBegin) + this._reporter.onBegin(config, new Suite('')); + this._reporter.onTimeout(config.globalTimeout); + await this._flushOutput(); + return 'failed'; + } + if (result === 'forbid-only') { + console.error('====================================='); + console.error(' --forbid-only found a focused test.'); + console.error('====================================='); + } else if (result === 'no-tests') { + console.error('================='); + console.error(' no tests found.'); + console.error('================='); + } + await this._flushOutput(); + return result!; + } + + async _flushOutput() { + // Calling process.exit() might truncate large stdout/stderr output. + // See https://github.com/nodejs/node/issues/6456. + // + // We can use writableNeedDrain to workaround this, but it is only available + // since node v15.2.0. + // See https://nodejs.org/api/stream.html#stream_writable_writableneeddrain. + if ((process.stdout as any).writableNeedDrain) + await new Promise(f => process.stdout.on('drain', f)); + if ((process.stderr as any).writableNeedDrain) + await new Promise(f => process.stderr.on('drain', f)); + } + + async _run(list: boolean, testFileReFilters: RegExp[], projectName?: string): Promise { + const testFileFilter = testFileReFilters.length ? createMatcher(testFileReFilters) : () => true; + const config = this._loader.fullConfig(); + + const projects = this._loader.projects().filter(project => { + return !projectName || project.config.name.toLocaleLowerCase() === projectName.toLocaleLowerCase(); + }); + if (projectName && !projects.length) { + const names = this._loader.projects().map(p => p.config.name).filter(name => !!name); + if (!names.length) + throw new Error(`No named projects are specified in the configuration file`); + throw new Error(`Project "${projectName}" not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`); + } + + const files = new Map(); + const allTestFiles = new Set(); + for (const project of projects) { + const testDir = project.config.testDir; + if (!fs.existsSync(testDir)) + throw new Error(`${testDir} does not exist`); + if (!fs.statSync(testDir).isDirectory()) + throw new Error(`${testDir} is not a directory`); + const allFiles = await collectFiles(project.config.testDir); + const testMatch = createMatcher(project.config.testMatch); + const testIgnore = createMatcher(project.config.testIgnore); + const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file)); + files.set(project, testFiles); + testFiles.forEach(file => allTestFiles.add(file)); + } + + let globalSetupResult: any; + if (config.globalSetup) + globalSetupResult = await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup')(this._loader.fullConfig()); + try { + for (const file of allTestFiles) + this._loader.loadTestFile(file); + + const rootSuite = new Suite(''); + for (const fileSuite of this._loader.fileSuites().values()) + rootSuite._addSuite(fileSuite); + if (config.forbidOnly && rootSuite._hasOnly()) + return 'forbid-only'; + filterOnly(rootSuite); + + const fileSuites = new Map(); + for (const fileSuite of rootSuite.suites) + fileSuites.set(fileSuite.file, fileSuite); + + const outputDirs = new Set(); + const grepMatcher = createMatcher(config.grep); + for (const project of projects) { + for (const file of files.get(project)!) { + const fileSuite = fileSuites.get(file); + if (!fileSuite) + continue; + for (const spec of fileSuite._allSpecs()) { + if (grepMatcher(spec._testFullTitle(project.config.name))) + project.generateTests(spec); + } + } + outputDirs.add(project.config.outputDir); + } + + const total = rootSuite.totalTestCount(); + if (!total) + return 'no-tests'; + + await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(e => {}))); + + let sigint = false; + let sigintCallback: () => void; + const sigIntPromise = new Promise(f => sigintCallback = f); + const sigintHandler = () => { + // We remove handler so that double Ctrl+C immediately kills the runner, + // for the case where our shutdown takes a lot of time or is buggy. + // Removing the handler synchronously sometimes triggers the default handler + // that exits the process, so we remove asynchronously. + setTimeout(() => process.off('SIGINT', sigintHandler), 0); + sigint = true; + sigintCallback(); + }; + process.on('SIGINT', sigintHandler); + + if (process.stdout.isTTY) { + const workers = new Set(); + rootSuite.findTest(test => { + workers.add(test.spec.file + test._workerHash); + }); + console.log(); + const jobs = Math.min(config.workers, workers.size); + const shard = config.shard; + const shardDetails = shard ? `, shard ${shard.current + 1} of ${shard.total}` : ''; + console.log(`Running ${total} test${total > 1 ? 's' : ''} using ${jobs} worker${jobs > 1 ? 's' : ''}${shardDetails}`); + } + + this._reporter.onBegin(config, rootSuite); + this._didBegin = true; + let hasWorkerErrors = false; + if (!list) { + const dispatcher = new Dispatcher(this._loader, rootSuite, this._reporter); + await Promise.race([dispatcher.run(), sigIntPromise]); + await dispatcher.stop(); + hasWorkerErrors = dispatcher.hasWorkerErrors(); + } + this._reporter.onEnd(); + + if (sigint) + return 'sigint'; + return hasWorkerErrors || rootSuite.findSpec(spec => !spec.ok()) ? 'failed' : 'passed'; + } finally { + if (globalSetupResult && typeof globalSetupResult === 'function') + await globalSetupResult(this._loader.fullConfig()); + if (config.globalTeardown) + await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown')(this._loader.fullConfig()); + } + } +} + +function filterOnly(suite: Suite) { + const onlySuites = suite.suites.filter(child => filterOnly(child) || child._only); + const onlyTests = suite.specs.filter(spec => spec._only); + const onlyEntries = new Set([...onlySuites, ...onlyTests]); + if (onlyEntries.size) { + suite.suites = onlySuites; + suite.specs = onlyTests; + suite._entries = suite._entries.filter(e => onlyEntries.has(e)); // Preserve the order. + return true; + } + return false; +} + +async function collectFiles(testDir: string): Promise { + type Rule = { + dir: string; + negate: boolean; + match: (s: string, partial?: boolean) => boolean + }; + type IgnoreStatus = 'ignored' | 'included' | 'ignored-but-recurse'; + + const checkIgnores = (entryPath: string, rules: Rule[], isDirectory: boolean, parentStatus: IgnoreStatus) => { + let status = parentStatus; + for (const rule of rules) { + const ruleIncludes = rule.negate; + if ((status === 'included') === ruleIncludes) + continue; + const relative = path.relative(rule.dir, entryPath); + if (rule.match('/' + relative) || rule.match(relative)) { + // Matches "/dir/file" or "dir/file" + status = ruleIncludes ? 'included' : 'ignored'; + } else if (isDirectory && (rule.match('/' + relative + '/') || rule.match(relative + '/'))) { + // Matches "/dir/subdir/" or "dir/subdir/" for directories. + status = ruleIncludes ? 'included' : 'ignored'; + } else if (isDirectory && ruleIncludes && (rule.match('/' + relative, true) || rule.match(relative, true))) { + // Matches "/dir/donotskip/" when "/dir" is excluded, but "!/dir/donotskip/file" is included. + status = 'ignored-but-recurse'; + } + } + return status; + }; + + const files: string[] = []; + + const visit = async (dir: string, rules: Rule[], status: IgnoreStatus) => { + const entries = await readDirAsync(dir, { withFileTypes: true }); + entries.sort((a, b) => a.name.localeCompare(b.name)); + + const gitignore = entries.find(e => e.isFile() && e.name === '.gitignore'); + if (gitignore) { + const content = await readFileAsync(path.join(dir, gitignore.name), 'utf8'); + const newRules: Rule[] = content.split(/\r?\n/).map(s => { + s = s.trim(); + if (!s) + return; + // Use flipNegate, because we handle negation ourselves. + const rule = new Minimatch(s, { matchBase: true, dot: true, flipNegate: true }) as any; + if (rule.comment) + return; + rule.dir = dir; + return rule; + }).filter(rule => !!rule); + rules = [...rules, ...newRules]; + } + + for (const entry of entries) { + if (entry === gitignore || entry.name === '.' || entry.name === '..') + continue; + if (entry.isDirectory() && entry.name === 'node_modules') + continue; + const entryPath = path.join(dir, entry.name); + const entryStatus = checkIgnores(entryPath, rules, entry.isDirectory(), status); + if (entry.isDirectory() && entryStatus !== 'ignored') + await visit(entryPath, rules, entryStatus); + else if (entry.isFile() && entryStatus === 'included') + files.push(entryPath); + } + }; + await visit(testDir, [], 'included'); + return files; +} diff --git a/src/test/test.ts b/src/test/test.ts new file mode 100644 index 0000000000..d046e7dff1 --- /dev/null +++ b/src/test/test.ts @@ -0,0 +1,223 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as reporterTypes from './reporter'; +import type { TestTypeImpl } from './testType'; +import { Location } from './types'; + +class Base { + title: string; + file: string = ''; + line: number = 0; + column: number = 0; + parent?: Suite; + + _only = false; + + constructor(title: string) { + this.title = title; + } + + titlePath(): string[] { + if (!this.parent) + return []; + if (!this.title) + return this.parent.titlePath(); + return [...this.parent.titlePath(), this.title]; + } +} + +export class Spec extends Base implements reporterTypes.Spec { + suite!: Suite; + fn: Function; + tests: Test[] = []; + _ordinalInFile: number; + _testType: TestTypeImpl; + + constructor(title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl) { + super(title); + this.fn = fn; + this._ordinalInFile = ordinalInFile; + this._testType = testType; + } + + ok(): boolean { + return !this.tests.find(r => !r.ok()); + } + + fullTitle(): string { + return this.titlePath().join(' '); + } + + _testFullTitle(projectName: string) { + return (projectName ? `[${projectName}] ` : '') + this.fullTitle(); + } +} + +export class Suite extends Base implements reporterTypes.Suite { + suites: Suite[] = []; + specs: Spec[] = []; + _fixtureOverrides: any = {}; + _entries: (Suite | Spec)[] = []; + _hooks: { + type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', + fn: Function, + location: Location, + } [] = []; + + _addSpec(spec: Spec) { + spec.parent = this; + spec.suite = this; + this.specs.push(spec); + this._entries.push(spec); + } + + _addSuite(suite: Suite) { + suite.parent = this; + this.suites.push(suite); + this._entries.push(suite); + } + + findTest(fn: (test: Test) => boolean | void): boolean { + for (const entry of this._entries) { + if (entry instanceof Suite) { + if (entry.findTest(fn)) + return true; + } else { + for (const test of entry.tests) { + if (fn(test)) + return true; + } + } + } + return false; + } + + findSpec(fn: (spec: Spec) => boolean | void): boolean { + for (const entry of this._entries) { + if (entry instanceof Suite) { + if (entry.findSpec(fn)) + return true; + } else { + if (fn(entry)) + return true; + } + } + return false; + } + + findSuite(fn: (suite: Suite) => boolean | void): boolean { + if (fn(this)) + return true; + for (const suite of this.suites) { + if (suite.findSuite(fn)) + return true; + } + return false; + } + + totalTestCount(): number { + let total = 0; + for (const suite of this.suites) + total += suite.totalTestCount(); + for (const spec of this.specs) + total += spec.tests.length; + return total; + } + + _allSpecs(): Spec[] { + const result: Spec[] = []; + this.findSpec(test => { result.push(test); }); + return result; + } + + _hasOnly(): boolean { + if (this._only) + return true; + if (this.suites.find(suite => suite._hasOnly())) + return true; + if (this.specs.find(spec => spec._only)) + return true; + return false; + } + + _buildFixtureOverrides(): any { + return this.parent ? { ...this.parent._buildFixtureOverrides(), ...this._fixtureOverrides } : this._fixtureOverrides; + } +} + +export class Test implements reporterTypes.Test { + spec: Spec; + results: reporterTypes.TestResult[] = []; + + skipped = false; + expectedStatus: reporterTypes.TestStatus = 'passed'; + timeout = 0; + annotations: { type: string, description?: string }[] = []; + projectName = ''; + retries = 0; + + _id = ''; + _repeatEachIndex = 0; + _projectIndex = 0; + _workerHash = ''; + + constructor(spec: Spec) { + this.spec = spec; + } + + status(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { + if (this.skipped) + return 'skipped'; + // List mode bail out. + if (!this.results.length) + return 'skipped'; + if (this.results.length === 1 && this.expectedStatus === this.results[0].status) + return 'expected'; + let hasPassedResults = false; + for (const result of this.results) { + // Missing status is Ok when running in shards mode. + if (!result.status) + return 'skipped'; + if (result.status === this.expectedStatus) + hasPassedResults = true; + } + if (hasPassedResults) + return 'flaky'; + return 'unexpected'; + } + + ok(): boolean { + const status = this.status(); + return status === 'expected' || status === 'flaky' || status === 'skipped'; + } + + fullTitle(): string { + return this.spec._testFullTitle(this.projectName); + } + + _appendTestResult(): reporterTypes.TestResult { + const result: reporterTypes.TestResult = { + retry: this.results.length, + workerIndex: 0, + duration: 0, + stdout: [], + stderr: [], + }; + this.results.push(result); + return result; + } +} diff --git a/src/test/testType.ts b/src/test/testType.ts new file mode 100644 index 0000000000..bc5b0b2104 --- /dev/null +++ b/src/test/testType.ts @@ -0,0 +1,161 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from './expect'; +import { currentlyLoadingFileSuite, currentTestInfo, setCurrentlyLoadingFileSuite } from './globals'; +import { Spec, Suite } from './test'; +import { callLocation, errorWithCallLocation } from './util'; +import { Fixtures, FixturesWithLocation, Location, TestInfo, TestType } from './types'; +import { inheritFixtureParameterNames } from './fixtures'; + +Error.stackTraceLimit = 15; + +const countByFile = new Map(); + +export class DeclaredFixtures { + testType!: TestTypeImpl; + location!: Location; +} + +export class TestTypeImpl { + readonly fixtures: (FixturesWithLocation | DeclaredFixtures)[]; + readonly test: TestType; + + constructor(fixtures: (FixturesWithLocation | DeclaredFixtures)[]) { + this.fixtures = fixtures; + + const test: any = this._spec.bind(this, 'default'); + test.expect = expect; + test.only = this._spec.bind(this, 'only'); + test.describe = this._describe.bind(this, 'default'); + test.describe.only = this._describe.bind(this, 'only'); + test.beforeEach = this._hook.bind(this, 'beforeEach'); + test.afterEach = this._hook.bind(this, 'afterEach'); + test.beforeAll = this._hook.bind(this, 'beforeAll'); + test.afterAll = this._hook.bind(this, 'afterAll'); + test.skip = this._modifier.bind(this, 'skip'); + test.fixme = this._modifier.bind(this, 'fixme'); + test.fail = this._modifier.bind(this, 'fail'); + test.slow = this._modifier.bind(this, 'slow'); + test.setTimeout = this._setTimeout.bind(this); + test.use = this._use.bind(this); + test.extend = this._extend.bind(this); + test.declare = this._declare.bind(this); + this.test = test; + } + + private _spec(type: 'default' | 'only', title: string, fn: Function) { + const suite = currentlyLoadingFileSuite(); + if (!suite) + throw errorWithCallLocation(`test() can only be called in a test file`); + const location = callLocation(suite.file); + + const ordinalInFile = countByFile.get(suite.file) || 0; + countByFile.set(location.file, ordinalInFile + 1); + + const spec = new Spec(title, fn, ordinalInFile, this); + spec.file = location.file; + spec.line = location.line; + spec.column = location.column; + suite._addSpec(spec); + + if (type === 'only') + spec._only = true; + } + + private _describe(type: 'default' | 'only', title: string, fn: Function) { + const suite = currentlyLoadingFileSuite(); + if (!suite) + throw errorWithCallLocation(`describe() can only be called in a test file`); + const location = callLocation(suite.file); + + const child = new Suite(title); + child.file = suite.file; + child.line = location.line; + child.column = location.column; + suite._addSuite(child); + + if (type === 'only') + child._only = true; + + setCurrentlyLoadingFileSuite(child); + fn(); + setCurrentlyLoadingFileSuite(suite); + } + + private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', fn: Function) { + const suite = currentlyLoadingFileSuite(); + if (!suite) + throw errorWithCallLocation(`${name} hook can only be called in a test file`); + suite._hooks.push({ type: name, fn, location: callLocation() }); + } + + private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', ...modiferAgs: [arg?: any | Function, description?: string]) { + const suite = currentlyLoadingFileSuite(); + if (suite) { + const location = callLocation(); + if (typeof modiferAgs[0] === 'function') { + const [conditionFn, description] = modiferAgs; + const fn = (args: any, testInfo: TestInfo) => testInfo[type](conditionFn(args), description!); + inheritFixtureParameterNames(conditionFn, fn, location); + suite._hooks.unshift({ type: 'beforeEach', fn, location }); + } else { + const fn = ({}: any, testInfo: TestInfo) => testInfo[type](...modiferAgs as [any, any]); + suite._hooks.unshift({ type: 'beforeEach', fn, location }); + } + return; + } + + const testInfo = currentTestInfo(); + if (!testInfo) + throw new Error(`test.${type}() can only be called inside test, describe block or fixture`); + if (typeof modiferAgs[0] === 'function') + throw new Error(`test.${type}() with a function can only be called inside describe block`); + testInfo[type](...modiferAgs as [any, any]); + } + + private _setTimeout(timeout: number) { + const testInfo = currentTestInfo(); + if (!testInfo) + throw new Error(`test.setTimeout() can only be called inside test or fixture`); + testInfo.setTimeout(timeout); + } + + private _use(fixtures: Fixtures) { + const suite = currentlyLoadingFileSuite(); + if (!suite) + throw errorWithCallLocation(`test.use() can only be called in a test file`); + suite._fixtureOverrides = { ...suite._fixtureOverrides, ...fixtures }; + } + + private _extend(fixtures: Fixtures) { + const fixturesWithLocation = { + fixtures, + location: callLocation(), + }; + return new TestTypeImpl([...this.fixtures, fixturesWithLocation]).test; + } + + private _declare() { + const declared = new DeclaredFixtures(); + declared.location = callLocation(); + const child = new TestTypeImpl([...this.fixtures, declared]); + declared.testType = child; + return child.test; + } +} + +export const rootTestType = new TestTypeImpl([]); diff --git a/src/test/transform.ts b/src/test/transform.ts new file mode 100644 index 0000000000..72ce0c6502 --- /dev/null +++ b/src/test/transform.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as crypto from 'crypto'; +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as pirates from 'pirates'; +import * as babel from '@babel/core'; +import * as sourceMapSupport from 'source-map-support'; + +const version = 4; +const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache'); +const sourceMaps: Map = new Map(); + +sourceMapSupport.install({ + environment: 'node', + handleUncaughtExceptions: false, + retrieveSourceMap(source) { + if (!sourceMaps.has(source)) + return null; + const sourceMapPath = sourceMaps.get(source)!; + if (!fs.existsSync(sourceMapPath)) + return null; + return { + map: JSON.parse(fs.readFileSync(sourceMapPath, 'utf-8')), + url: source + }; + } +}); + +function calculateCachePath(content: string, filePath: string): string { + const hash = crypto.createHash('sha1').update(content).update(filePath).update(String(version)).digest('hex'); + const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash; + return path.join(cacheDir, hash[0] + hash[1], fileName); +} + +export function installTransform(): () => void { + return pirates.addHook((code, filename) => { + const cachePath = calculateCachePath(code, filename); + const codePath = cachePath + '.js'; + const sourceMapPath = cachePath + '.map'; + sourceMaps.set(filename, sourceMapPath); + if (fs.existsSync(codePath)) + return fs.readFileSync(codePath, 'utf8'); + // We don't use any browserslist data, but babel checks it anyway. + // Silence the annoying warning. + process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; + const result = babel.transformFileSync(filename, { + babelrc: false, + configFile: false, + assumptions: { + // Without this, babel defines a top level function that + // breaks playwright evaluates. + setPublicClassFields: true, + }, + presets: [ + [require.resolve('@babel/preset-typescript'), { onlyRemoveTypeImports: true }], + ], + plugins: [ + [require.resolve('@babel/plugin-proposal-class-properties')], + [require.resolve('@babel/plugin-proposal-numeric-separator')], + [require.resolve('@babel/plugin-proposal-logical-assignment-operators')], + [require.resolve('@babel/plugin-proposal-nullish-coalescing-operator')], + [require.resolve('@babel/plugin-proposal-optional-chaining')], + [require.resolve('@babel/plugin-syntax-json-strings')], + [require.resolve('@babel/plugin-syntax-optional-catch-binding')], + [require.resolve('@babel/plugin-syntax-async-generators')], + [require.resolve('@babel/plugin-syntax-object-rest-spread')], + [require.resolve('@babel/plugin-proposal-export-namespace-from')], + [require.resolve('@babel/plugin-transform-modules-commonjs')], + [require.resolve('@babel/plugin-proposal-dynamic-import')], + ], + sourceMaps: 'both', + } as babel.TransformOptions)!; + if (result.code) { + fs.mkdirSync(path.dirname(cachePath), {recursive: true}); + if (result.map) + fs.writeFileSync(sourceMapPath, JSON.stringify(result.map), 'utf8'); + fs.writeFileSync(codePath, result.code, 'utf8'); + } + return result.code || ''; + }, { + exts: ['.ts'] + }); +} diff --git a/src/test/types.ts b/src/test/types.ts new file mode 100644 index 0000000000..3720d4b4db --- /dev/null +++ b/src/test/types.ts @@ -0,0 +1,17 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '../../types/testInternal'; diff --git a/src/test/util.ts b/src/test/util.ts new file mode 100644 index 0000000000..48dfcf9afe --- /dev/null +++ b/src/test/util.ts @@ -0,0 +1,195 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import util from 'util'; +import StackUtils from 'stack-utils'; +import type { Location } from './types'; +import type { TestError } from './reporter'; +import { default as minimatch } from 'minimatch'; + +const TEST_RUNNER_DIRS = [ + path.join('@playwright', 'test', 'lib'), + path.join(__dirname, '..', '..', 'src', 'test'), +]; +const cwd = process.cwd(); +const stackUtils = new StackUtils({ cwd }); + +export class DeadlineRunner { + private _timer: NodeJS.Timer | undefined; + private _done = false; + private _fulfill!: (t: { result?: T, timedOut?: boolean }) => void; + private _reject!: (error: any) => void; + + readonly result: Promise<{ result?: T, timedOut?: boolean }>; + + constructor(promise: Promise, deadline: number | undefined) { + this.result = new Promise((f, r) => { + this._fulfill = f; + this._reject = r; + }); + promise.then(result => { + this._finish({ result }); + }).catch(e => { + this._finish(undefined, e); + }); + this.setDeadline(deadline); + } + + private _finish(success?: { result?: T, timedOut?: boolean }, error?: any) { + if (this._done) + return; + this.setDeadline(undefined); + if (success) + this._fulfill(success); + else + this._reject(error); + } + + setDeadline(deadline: number | undefined) { + if (this._timer) { + clearTimeout(this._timer); + this._timer = undefined; + } + if (deadline === undefined) + return; + const timeout = deadline - monotonicTime(); + if (timeout <= 0) + this._finish({ timedOut: true }); + else + this._timer = setTimeout(() => this._finish({ timedOut: true }), timeout); + } +} + +export async function raceAgainstDeadline(promise: Promise, deadline: number | undefined): Promise<{ result?: T, timedOut?: boolean }> { + return (new DeadlineRunner(promise, deadline)).result; +} + +export function serializeError(error: Error | any): TestError { + if (error instanceof Error) { + return { + message: error.message, + stack: error.stack + }; + } + return { + value: util.inspect(error) + }; +} + +function callFrames(): string[] { + const obj = { stack: '' }; + Error.captureStackTrace(obj); + const frames = obj.stack.split('\n').slice(1); + while (frames.length && TEST_RUNNER_DIRS.some(dir => frames[0].includes(dir))) + frames.shift(); + return frames; +} + +export function callLocation(fallbackFile?: string): Location { + const frames = callFrames(); + if (!frames.length) + return {file: fallbackFile || '', line: 1, column: 1}; + const location = stackUtils.parseLine(frames[0])!; + return { + file: path.resolve(cwd, location.file || ''), + line: location.line || 0, + column: location.column || 0, + }; +} + +export function errorWithCallLocation(message: string): Error { + const frames = callFrames(); + const error = new Error(message); + error.stack = 'Error: ' + message + '\n' + frames.join('\n'); + return error; +} + +export function monotonicTime(): number { + const [seconds, nanoseconds] = process.hrtime(); + return seconds * 1000 + (nanoseconds / 1000000 | 0); +} + +export function prependErrorMessage(e: Error, message: string) { + let stack = e.stack || ''; + if (stack.includes(e.message)) + stack = stack.substring(stack.indexOf(e.message) + e.message.length); + let m = e.message; + if (m.startsWith('Error:')) + m = m.substring('Error:'.length); + e.message = message + m; + e.stack = e.message + stack; +} + +export function isRegExp(e: any): e is RegExp { + return e && typeof e === 'object' && (e instanceof RegExp || Object.prototype.toString.call(e) === '[object RegExp]'); +} + +export type Matcher = (value: string) => boolean; + +export function createMatcher(patterns: string | RegExp | (string | RegExp)[]): Matcher { + const reList: RegExp[] = []; + const filePatterns: string[] = []; + for (const pattern of Array.isArray(patterns) ? patterns : [patterns]) { + if (isRegExp(pattern)) { + reList.push(pattern); + } else { + if (!pattern.startsWith('**/') && !pattern.startsWith('**/')) + filePatterns.push('**/' + pattern); + else + filePatterns.push(pattern); + } + } + + return (value: string) => { + for (const re of reList) { + re.lastIndex = 0; + if (re.test(value)) + return true; + } + for (const pattern of filePatterns) { + if (minimatch(value, pattern)) + return true; + } + return false; + }; +} + +export function mergeObjects(a: A | undefined | void, b: B | undefined | void): A & B { + const result = { ...a } as any; + if (!Object.is(b, undefined)) { + for (const [name, value] of Object.entries(b as B)) { + if (!Object.is(value, undefined)) + result[name] = value; + } + } + return result as any; +} + +export async function wrapInPromise(value: any) { + return value; +} + +export function formatLocation(location: Location) { + return location.file + ':' + location.line + ':' + location.column; +} + +export function forceRegExp(pattern: string): RegExp { + const match = pattern.match(/^\/(.*)\/([gi]*)$/); + if (match) + return new RegExp(match[1], match[2]); + return new RegExp(pattern, 'g'); +} diff --git a/src/test/worker.ts b/src/test/worker.ts new file mode 100644 index 0000000000..6cd4d91d1b --- /dev/null +++ b/src/test/worker.ts @@ -0,0 +1,119 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Console } from 'console'; +import * as util from 'util'; +import { RunPayload, TestOutputPayload, WorkerInitParams } from './ipc'; +import { serializeError } from './util'; +import { WorkerRunner } from './workerRunner'; + +let closed = false; + +sendMessageToParent('ready'); + +global.console = new Console({ + stdout: process.stdout, + stderr: process.stderr, + colorMode: process.env.FORCE_COLOR === '1', +}); + +process.stdout.write = (chunk: string | Buffer) => { + const outPayload: TestOutputPayload = { + testId: workerRunner?._currentTest?.testId, + ...chunkToParams(chunk) + }; + sendMessageToParent('stdOut', outPayload); + return true; +}; + +if (!process.env.PW_RUNNER_DEBUG) { + process.stderr.write = (chunk: string | Buffer) => { + const outPayload: TestOutputPayload = { + testId: workerRunner?._currentTest?.testId, + ...chunkToParams(chunk) + }; + sendMessageToParent('stdErr', outPayload); + return true; + }; +} + +process.on('disconnect', gracefullyCloseAndExit); +process.on('SIGINT',() => {}); +process.on('SIGTERM',() => {}); + +let workerRunner: WorkerRunner; + +process.on('unhandledRejection', (reason, promise) => { + if (workerRunner) + workerRunner.unhandledError(reason); +}); + +process.on('uncaughtException', error => { + if (workerRunner) + workerRunner.unhandledError(error); +}); + +process.on('message', async message => { + if (message.method === 'init') { + const initParams = message.params as WorkerInitParams; + workerRunner = new WorkerRunner(initParams); + for (const event of ['testBegin', 'testEnd', 'done']) + workerRunner.on(event, sendMessageToParent.bind(null, event)); + return; + } + if (message.method === 'stop') { + await gracefullyCloseAndExit(); + return; + } + if (message.method === 'run') { + const runPayload = message.params as RunPayload; + await workerRunner!.run(runPayload); + } +}); + +async function gracefullyCloseAndExit() { + if (closed) + return; + closed = true; + // Force exit after 30 seconds. + setTimeout(() => process.exit(0), 30000); + // Meanwhile, try to gracefully shutdown. + try { + if (workerRunner) { + workerRunner.stop(); + await workerRunner.cleanup(); + } + } catch (e) { + process.send!({ method: 'teardownError', params: { error: serializeError(e) } }); + } + process.exit(0); +} + +function sendMessageToParent(method: string, params = {}) { + try { + process.send!({ method, params }); + } catch (e) { + // Can throw when closing. + } +} + +function chunkToParams(chunk: Buffer | string): { text?: string, buffer?: string } { + if (chunk instanceof Buffer) + return { buffer: chunk.toString('base64') }; + if (typeof chunk !== 'string') + return { text: util.inspect(chunk) }; + return { text: chunk }; +} diff --git a/src/test/workerRunner.ts b/src/test/workerRunner.ts new file mode 100644 index 0000000000..8db588a368 --- /dev/null +++ b/src/test/workerRunner.ts @@ -0,0 +1,453 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import rimraf from 'rimraf'; +import util from 'util'; +import { EventEmitter } from 'events'; +import { monotonicTime, DeadlineRunner, raceAgainstDeadline, serializeError } from './util'; +import { TestBeginPayload, TestEndPayload, RunPayload, TestEntry, DonePayload, WorkerInitParams } from './ipc'; +import { setCurrentTestInfo } from './globals'; +import { Loader } from './loader'; +import { Spec, Suite, Test } from './test'; +import { TestInfo, WorkerInfo } from './types'; +import { ProjectImpl } from './project'; +import { FixtureRunner } from './fixtures'; + +const removeFolderAsync = util.promisify(rimraf); + +export class WorkerRunner extends EventEmitter { + private _params: WorkerInitParams; + private _loader!: Loader; + private _project!: ProjectImpl; + private _workerInfo!: WorkerInfo; + private _projectNamePathSegment = ''; + private _uniqueProjectNamePathSegment = ''; + private _fixtureRunner: FixtureRunner; + + private _failedTestId: string | undefined; + private _fatalError: any | undefined; + private _entries = new Map(); + private _remaining = new Map(); + private _isStopped: any; + _currentTest: { testId: string, testInfo: TestInfo } | null = null; + + constructor(params: WorkerInitParams) { + super(); + this._params = params; + this._fixtureRunner = new FixtureRunner(); + } + + stop() { + this._isStopped = true; + this._setCurrentTest(null); + } + + async cleanup() { + // TODO: separate timeout for teardown? + const result = await raceAgainstDeadline((async () => { + await this._fixtureRunner.teardownScope('test'); + await this._fixtureRunner.teardownScope('worker'); + })(), this._deadline()); + if (result.timedOut) + throw new Error(`Timeout of ${this._project.config.timeout}ms exceeded while shutting down environment`); + } + + unhandledError(error: Error | any) { + if (this._isStopped) + return; + if (this._currentTest) { + this._currentTest.testInfo.status = 'failed'; + this._currentTest.testInfo.error = serializeError(error); + this._failedTestId = this._currentTest.testId; + this.emit('testEnd', buildTestEndPayload(this._currentTest.testId, this._currentTest.testInfo)); + } else { + // No current test - fatal error. + this._fatalError = serializeError(error); + } + this._reportDoneAndStop(); + } + + private _deadline() { + return this._project.config.timeout ? monotonicTime() + this._project.config.timeout : undefined; + } + + private _loadIfNeeded() { + if (this._loader) + return; + + this._loader = Loader.deserialize(this._params.loader); + this._project = this._loader.projects()[this._params.projectIndex]; + + this._projectNamePathSegment = sanitizeForFilePath(this._project.config.name); + + const sameName = this._loader.projects().filter(project => project.config.name === this._project.config.name); + if (sameName.length > 1) + this._uniqueProjectNamePathSegment = this._project.config.name + (sameName.indexOf(this._project) + 1); + else + this._uniqueProjectNamePathSegment = this._project.config.name; + this._uniqueProjectNamePathSegment = sanitizeForFilePath(this._uniqueProjectNamePathSegment); + + this._workerInfo = { + workerIndex: this._params.workerIndex, + project: this._project.config, + config: this._loader.fullConfig(), + }; + } + + async run(runPayload: RunPayload) { + this._entries = new Map(runPayload.entries.map(e => [ e.testId, e ])); + this._remaining = new Map(runPayload.entries.map(e => [ e.testId, e ])); + + this._loadIfNeeded(); + const fileSuite = this._loader.loadTestFile(runPayload.file); + let anySpec: Spec | undefined; + fileSuite.findSpec(spec => { + const test = this._project.generateTests(spec, this._params.repeatEachIndex)[0]; + if (this._entries.has(test._id)) + anySpec = spec; + }); + if (!anySpec) { + this._reportDone(); + return; + } + + this._fixtureRunner.setPool(this._project.buildPool(anySpec)); + await this._runSuite(fileSuite); + if (this._isStopped) + return; + + this._reportDone(); + } + + private async _runSuite(suite: Suite) { + if (this._isStopped) + return; + const skipHooks = !this._hasTestsToRun(suite); + for (const hook of suite._hooks) { + if (hook.type !== 'beforeAll' || skipHooks) + continue; + if (this._isStopped) + return; + // TODO: separate timeout for beforeAll? + const result = await raceAgainstDeadline(this._fixtureRunner.resolveParametersAndRunHookOrTest(hook.fn, 'worker', this._workerInfo), this._deadline()); + if (result.timedOut) { + this._fatalError = serializeError(new Error(`Timeout of ${this._project.config.timeout}ms exceeded while running beforeAll hook`)); + this._reportDoneAndStop(); + } + } + for (const entry of suite._entries) { + if (entry instanceof Suite) + await this._runSuite(entry); + else + await this._runSpec(entry); + } + for (const hook of suite._hooks) { + if (hook.type !== 'afterAll' || skipHooks) + continue; + if (this._isStopped) + return; + // TODO: separate timeout for afterAll? + const result = await raceAgainstDeadline(this._fixtureRunner.resolveParametersAndRunHookOrTest(hook.fn, 'worker', this._workerInfo), this._deadline()); + if (result.timedOut) { + this._fatalError = serializeError(new Error(`Timeout of ${this._project.config.timeout}ms exceeded while running afterAll hook`)); + this._reportDoneAndStop(); + } + } + } + + private async _runSpec(spec: Spec) { + if (this._isStopped) + return; + const test = spec.tests[0]; + const entry = this._entries.get(test._id); + if (!entry) + return; + this._remaining.delete(test._id); + + const startTime = monotonicTime(); + let deadlineRunner: DeadlineRunner | undefined; + const testId = test._id; + + const baseOutputDir = (() => { + const relativeTestFilePath = path.relative(this._project.config.testDir, spec.file.replace(/\.(spec|test)\.(js|ts)/, '')); + const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-'); + let testOutputDir = sanitizedRelativePath + '-' + sanitizeForFilePath(spec.title); + if (this._uniqueProjectNamePathSegment) + testOutputDir += '-' + this._uniqueProjectNamePathSegment; + if (entry.retry) + testOutputDir += '-retry' + entry.retry; + if (this._params.repeatEachIndex) + testOutputDir += '-repeat' + this._params.repeatEachIndex; + return path.join(this._project.config.outputDir, testOutputDir); + })(); + + const testInfo: TestInfo = { + ...this._workerInfo, + title: spec.title, + file: spec.file, + line: spec.line, + column: spec.column, + fn: spec.fn, + repeatEachIndex: this._params.repeatEachIndex, + retry: entry.retry, + expectedStatus: 'passed', + annotations: [], + duration: 0, + status: 'passed', + stdout: [], + stderr: [], + timeout: this._project.config.timeout, + snapshotSuffix: '', + outputDir: baseOutputDir, + outputPath: (...pathSegments: string[]): string => { + fs.mkdirSync(baseOutputDir, { recursive: true }); + return path.join(baseOutputDir, ...pathSegments); + }, + snapshotPath: (snapshotName: string): string => { + let suffix = ''; + if (this._projectNamePathSegment) + suffix += '-' + this._projectNamePathSegment; + if (testInfo.snapshotSuffix) + suffix += '-' + testInfo.snapshotSuffix; + if (suffix) { + const ext = path.extname(snapshotName); + if (ext) + snapshotName = snapshotName.substring(0, snapshotName.length - ext.length) + suffix + ext; + else + snapshotName += suffix; + } + return path.join(spec.file + '-snapshots', snapshotName); + }, + skip: (...args: [arg?: any, description?: string]) => modifier(testInfo, 'skip', args), + fixme: (...args: [arg?: any, description?: string]) => modifier(testInfo, 'fixme', args), + fail: (...args: [arg?: any, description?: string]) => modifier(testInfo, 'fail', args), + slow: (...args: [arg?: any, description?: string]) => modifier(testInfo, 'slow', args), + setTimeout: (timeout: number) => { + testInfo.timeout = timeout; + if (deadlineRunner) + deadlineRunner.setDeadline(deadline()); + }, + }; + this._setCurrentTest({ testInfo, testId }); + const deadline = () => { + return testInfo.timeout ? startTime + testInfo.timeout : undefined; + }; + + this.emit('testBegin', buildTestBeginPayload(testId, testInfo)); + + if (testInfo.expectedStatus === 'skipped') { + testInfo.status = 'skipped'; + this.emit('testEnd', buildTestEndPayload(testId, testInfo)); + return; + } + + // Update the fixture pool - it may differ between tests, but only in test-scoped fixtures. + this._fixtureRunner.setPool(this._project.buildPool(spec)); + + deadlineRunner = new DeadlineRunner(this._runTestWithBeforeHooks(test, testInfo), deadline()); + const result = await deadlineRunner.result; + // Do not overwrite test failure upon hook timeout. + if (result.timedOut && testInfo.status === 'passed') + testInfo.status = 'timedOut'; + if (this._isStopped) + return; + + if (!result.timedOut) { + deadlineRunner = new DeadlineRunner(this._runAfterHooks(test, testInfo), deadline()); + deadlineRunner.setDeadline(deadline()); + const hooksResult = await deadlineRunner.result; + // Do not overwrite test failure upon hook timeout. + if (hooksResult.timedOut && testInfo.status === 'passed') + testInfo.status = 'timedOut'; + } else { + // A timed-out test gets a full additional timeout to run after hooks. + const newDeadline = this._deadline(); + deadlineRunner = new DeadlineRunner(this._runAfterHooks(test, testInfo), newDeadline); + await deadlineRunner.result; + } + + if (this._isStopped) + return; + + testInfo.duration = monotonicTime() - startTime; + this.emit('testEnd', buildTestEndPayload(testId, testInfo)); + + const isFailure = testInfo.status === 'timedOut' || (testInfo.status === 'failed' && testInfo.expectedStatus !== 'failed'); + const preserveOutput = this._loader.fullConfig().preserveOutput === 'always' || + (this._loader.fullConfig().preserveOutput === 'failures-only' && isFailure); + if (!preserveOutput) + await removeFolderAsync(testInfo.outputDir).catch(e => {}); + + if (testInfo.status !== 'passed') { + this._failedTestId = testId; + this._reportDoneAndStop(); + } + this._setCurrentTest(null); + } + + private _setCurrentTest(currentTest: { testId: string, testInfo: TestInfo} | null) { + this._currentTest = currentTest; + setCurrentTestInfo(currentTest ? currentTest.testInfo : null); + } + + private async _runTestWithBeforeHooks(test: Test, testInfo: TestInfo) { + try { + await this._runHooks(test.spec.parent!, 'beforeEach', testInfo); + } catch (error) { + if (error instanceof SkipError) { + if (testInfo.status === 'passed') + testInfo.status = 'skipped'; + } else { + testInfo.status = 'failed'; + testInfo.error = serializeError(error); + } + // Continue running afterEach hooks even after the failure. + } + + // Do not run the test when beforeEach hook fails. + if (this._isStopped || testInfo.status === 'failed' || testInfo.status === 'skipped') + return; + + try { + await this._fixtureRunner.resolveParametersAndRunHookOrTest(test.spec.fn, 'test', testInfo); + } catch (error) { + if (error instanceof SkipError) { + if (testInfo.status === 'passed') + testInfo.status = 'skipped'; + } else { + // We might fail after the timeout, e.g. due to fixture teardown. + // Do not overwrite the timeout status with this error. + if (testInfo.status === 'passed') { + testInfo.status = 'failed'; + testInfo.error = serializeError(error); + } + } + } + } + + private async _runAfterHooks(test: Test, testInfo: TestInfo) { + try { + await this._runHooks(test.spec.parent!, 'afterEach', testInfo); + } catch (error) { + // Do not overwrite test failure error. + if (!(error instanceof SkipError) && testInfo.status === 'passed') { + testInfo.status = 'failed'; + testInfo.error = serializeError(error); + // Continue running even after the failure. + } + } + try { + await this._fixtureRunner.teardownScope('test'); + } catch (error) { + // Do not overwrite test failure error. + if (testInfo.status === 'passed') { + testInfo.status = 'failed'; + testInfo.error = serializeError(error); + } + } + } + + private async _runHooks(suite: Suite, type: 'beforeEach' | 'afterEach', testInfo: TestInfo) { + if (this._isStopped) + return; + const all = []; + for (let s: Suite | undefined = suite; s; s = s.parent) { + const funcs = s._hooks.filter(e => e.type === type).map(e => e.fn); + all.push(...funcs.reverse()); + } + if (type === 'beforeEach') + all.reverse(); + let error: Error | undefined; + for (const hook of all) { + try { + await this._fixtureRunner.resolveParametersAndRunHookOrTest(hook, 'test', testInfo); + } catch (e) { + // Always run all the hooks, and capture the first error. + error = error || e; + } + } + if (error) + throw error; + } + + private _reportDone() { + const donePayload: DonePayload = { + failedTestId: this._failedTestId, + fatalError: this._fatalError, + remaining: [...this._remaining.values()], + }; + this.emit('done', donePayload); + } + + private _reportDoneAndStop() { + if (this._isStopped) + return; + this._reportDone(); + this.stop(); + } + + private _hasTestsToRun(suite: Suite): boolean { + return suite.findSpec(spec => { + const entry = this._entries.get(spec.tests[0]._id); + return !!entry; + }); + } +} + +function buildTestBeginPayload(testId: string, testInfo: TestInfo): TestBeginPayload { + return { + testId, + workerIndex: testInfo.workerIndex + }; +} + +function buildTestEndPayload(testId: string, testInfo: TestInfo): TestEndPayload { + return { + testId, + duration: testInfo.duration, + status: testInfo.status!, + error: testInfo.error, + expectedStatus: testInfo.expectedStatus, + annotations: testInfo.annotations, + timeout: testInfo.timeout, + }; +} + +function modifier(testInfo: TestInfo, type: 'skip' | 'fail' | 'fixme' | 'slow', modifierArgs: [arg?: any, description?: string]) { + if (modifierArgs.length >= 1 && !modifierArgs[0]) + return; + + const description = modifierArgs[1]; + testInfo.annotations.push({ type, description }); + if (type === 'slow') { + testInfo.setTimeout(testInfo.timeout * 3); + } else if (type === 'skip' || type === 'fixme') { + testInfo.expectedStatus = 'skipped'; + throw new SkipError('Test is skipped: ' + (description || '')); + } else if (type === 'fail') { + if (testInfo.expectedStatus !== 'skipped') + testInfo.expectedStatus = 'failed'; + } +} + +class SkipError extends Error { +} + +function sanitizeForFilePath(s: string) { + return s.replace(/[^\w\d]+/g, '-'); +} diff --git a/src/third_party/diff_match_patch.js b/src/third_party/diff_match_patch.js new file mode 100644 index 0000000000..ba0df0f6ab --- /dev/null +++ b/src/third_party/diff_match_patch.js @@ -0,0 +1,2222 @@ +/** + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * @constructor + */ +var diff_match_patch = function() { + + // Defaults. + // Redefine these in your program to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + this.Diff_Timeout = 1.0; + // Cost of an empty edit operation in terms of edit characters. + this.Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + this.Match_Threshold = 0.5; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + this.Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close do + // the contents have to be to match the expected contents. (0.0 = perfection, + // 1.0 = very loose). Note that Match_Threshold controls how closely the + // end points of a delete need to match. + this.Patch_DeleteThreshold = 0.5; + // Chunk size for context length. + this.Patch_Margin = 4; + + // The number of bits in an int. + this.Match_MaxBits = 32; +}; + + +// DIFF FUNCTIONS + + +/** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ +var DIFF_DELETE = -1; +var DIFF_INSERT = 1; +var DIFF_EQUAL = 0; + +/** + * Class representing one diff tuple. + * Attempts to look like a two-element array (which is what this used to be). + * @param {number} op Operation, one of: DIFF_DELETE, DIFF_INSERT, DIFF_EQUAL. + * @param {string} text Text to be deleted, inserted, or retained. + * @constructor + */ +diff_match_patch.Diff = function(op, text) { + this[0] = op; + this[1] = text; +}; + +diff_match_patch.Diff.prototype.length = 2; + +/** + * Emulate the output of a two-element array. + * @return {string} Diff operation as a string. + */ +diff_match_patch.Diff.prototype.toString = function() { + return this[0] + ',' + this[1]; +}; + + +/** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} opt_checklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @param {number=} opt_deadline Optional time when the diff should be complete + * by. Used internally for recursive calls. Users should set DiffTimeout + * instead. + * @return {!Array.} Array of diff tuples. + */ +diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines, + opt_deadline) { + // Set a deadline by which time the diff must be complete. + if (typeof opt_deadline == 'undefined') { + if (this.Diff_Timeout <= 0) { + opt_deadline = Number.MAX_VALUE; + } else { + opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000; + } + } + var deadline = opt_deadline; + + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new Error('Null input. (diff_main)'); + } + + // Check for equality (speedup). + if (text1 == text2) { + if (text1) { + return [new diff_match_patch.Diff(DIFF_EQUAL, text1)]; + } + return []; + } + + if (typeof opt_checklines == 'undefined') { + opt_checklines = true; + } + var checklines = opt_checklines; + + // Trim off common prefix (speedup). + var commonlength = this.diff_commonPrefix(text1, text2); + var commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = this.diff_commonSuffix(text1, text2); + var commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + var diffs = this.diff_compute_(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix) { + diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, commonprefix)); + } + if (commonsuffix) { + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, commonsuffix)); + } + this.diff_cleanupMerge(diffs); + return diffs; +}; + + +/** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines, + deadline) { + var diffs; + + if (!text1) { + // Just add some text (speedup). + return [new diff_match_patch.Diff(DIFF_INSERT, text2)]; + } + + if (!text2) { + // Just delete some text (speedup). + return [new diff_match_patch.Diff(DIFF_DELETE, text1)]; + } + + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + var i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + diffs = [new diff_match_patch.Diff(DIFF_INSERT, longtext.substring(0, i)), + new diff_match_patch.Diff(DIFF_EQUAL, shorttext), + new diff_match_patch.Diff(DIFF_INSERT, + longtext.substring(i + shorttext.length))]; + // Swap insertions for deletions if diff is reversed. + if (text1.length > text2.length) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [new diff_match_patch.Diff(DIFF_DELETE, text1), + new diff_match_patch.Diff(DIFF_INSERT, text2)]; + } + + // Check to see if the problem can be split in two. + var hm = this.diff_halfMatch_(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + var text1_a = hm[0]; + var text1_b = hm[1]; + var text2_a = hm[2]; + var text2_b = hm[3]; + var mid_common = hm[4]; + // Send both pairs off for separate processing. + var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline); + var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + return diffs_a.concat([new diff_match_patch.Diff(DIFF_EQUAL, mid_common)], + diffs_b); + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return this.diff_lineMode_(text1, text2, deadline); + } + + return this.diff_bisect_(text1, text2, deadline); +}; + + +/** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) { + // Scan the text on a line-by-line basis first. + var a = this.diff_linesToChars_(text1, text2); + text1 = a.chars1; + text2 = a.chars2; + var linearray = a.lineArray; + + var diffs = this.diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + this.diff_charsToLines_(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + this.diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + var subDiff = + this.diff_main(text_delete, text_insert, false, deadline); + for (var j = subDiff.length - 1; j >= 0; j--) { + diffs.splice(pointer, 0, subDiff[j]); + } + pointer = pointer + subDiff.length; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; +}; + + +/** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + var max_d = Math.ceil((text1_length + text2_length) / 2); + var v_offset = max_d; + var v_length = 2 * max_d; + var v1 = new Array(v_length); + var v2 = new Array(v_length); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (var x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + var delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + var front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + var k1start = 0; + var k1end = 0; + var k2start = 0; + var k2end = 0; + for (var d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if ((new Date()).getTime() > deadline) { + break; + } + + // Walk the front path one step. + for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + var k1_offset = v_offset + k1; + var x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + var y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length && + text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + var k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + var x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + var k2_offset = v_offset + k2; + var x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + var y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length && + text1.charAt(text1_length - x2 - 1) == + text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + var k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + var x1 = v1[k1_offset]; + var y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [new diff_match_patch.Diff(DIFF_DELETE, text1), + new diff_match_patch.Diff(DIFF_INSERT, text2)]; +}; + + +/** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y, + deadline) { + var text1a = text1.substring(0, x); + var text2a = text2.substring(0, y); + var text1b = text1.substring(x); + var text2b = text2.substring(y); + + // Compute both diffs serially. + var diffs = this.diff_main(text1a, text2a, false, deadline); + var diffsb = this.diff_main(text1b, text2b, false, deadline); + + return diffs.concat(diffsb); +}; + + +/** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ +diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) { + var lineArray = []; // e.g. lineArray[4] == 'Hello\n' + var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[0] = ''; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diff_linesToCharsMunge_(text) { + var chars = ''; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + var lineStart = 0; + var lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + var lineArrayLength = lineArray.length; + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length - 1; + } + var line = text.substring(lineStart, lineEnd + 1); + + if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : + (lineHash[line] !== undefined)) { + chars += String.fromCharCode(lineHash[line]); + } else { + if (lineArrayLength == maxLines) { + // Bail out at 65535 because + // String.fromCharCode(65536) == String.fromCharCode(0) + line = text.substring(lineStart); + lineEnd = text.length; + } + chars += String.fromCharCode(lineArrayLength); + lineHash[line] = lineArrayLength; + lineArray[lineArrayLength++] = line; + } + lineStart = lineEnd + 1; + } + return chars; + } + // Allocate 2/3rds of the space for text1, the rest for text2. + var maxLines = 40000; + var chars1 = diff_linesToCharsMunge_(text1); + maxLines = 65535; + var chars2 = diff_linesToCharsMunge_(text2); + return {chars1: chars1, chars2: chars2, lineArray: lineArray}; +}; + + +/** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ +diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) { + for (var i = 0; i < diffs.length; i++) { + var chars = diffs[i][1]; + var text = []; + for (var j = 0; j < chars.length; j++) { + text[j] = lineArray[chars.charCodeAt(j)]; + } + diffs[i][1] = text.join(''); + } +}; + + +/** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ +diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { + return 0; + } + // Binary search. + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerstart = 0; + while (pointermin < pointermid) { + if (text1.substring(pointerstart, pointermid) == + text2.substring(pointerstart, pointermid)) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ +diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || + text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { + return 0; + } + // Binary search. + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerend = 0; + while (pointermin < pointermid) { + if (text1.substring(text1.length - pointermid, text1.length - pointerend) == + text2.substring(text2.length - pointermid, text2.length - pointerend)) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ +diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + var text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + var best = 0; + var length = 1; + while (true) { + var pattern = text1.substring(text_length - length); + var found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length) == + text2.substring(0, length)) { + best = length; + length++; + } + } +}; + + +/** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ +diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diff_halfMatchI_(longtext, shorttext, i) { + // Start with a 1/4 length substring at position i as a seed. + var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + var j = -1; + var best_common = ''; + var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length * 2 >= longtext.length) { + return [best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + var hm1 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 4)); + // Check again based on the third quarter. + var hm2 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 2)); + var hm; + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + var text1_a, text1_b, text2_a, text2_b; + if (text1.length > text2.length) { + text1_a = hm[0]; + text1_b = hm[1]; + text2_a = hm[2]; + text2_b = hm[3]; + } else { + text2_a = hm[0]; + text2_b = hm[1]; + text1_a = hm[2]; + text1_b = hm[3]; + } + var mid_common = hm[4]; + return [text1_a, text1_b, text2_a, text2_b, mid_common]; +}; + + +/** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastEquality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + var length_insertions1 = 0; + var length_deletions1 = 0; + // Number of characters that changed after the equality. + var length_insertions2 = 0; + var length_deletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + equalities[equalitiesLength++] = pointer; + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = diffs[pointer][1]; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_INSERT) { + length_insertions2 += diffs[pointer][1].length; + } else { + length_deletions2 += diffs[pointer][1].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastEquality && (lastEquality.length <= + Math.max(length_insertions1, length_deletions1)) && + (lastEquality.length <= Math.max(length_insertions2, + length_deletions2))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + // Throw away the equality we just deleted. + equalitiesLength--; + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + this.diff_cleanupMerge(diffs); + } + this.diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1][0] == DIFF_DELETE && + diffs[pointer][0] == DIFF_INSERT) { + var deletion = diffs[pointer - 1][1]; + var insertion = diffs[pointer][1]; + var overlap_length1 = this.diff_commonOverlap_(deletion, insertion); + var overlap_length2 = this.diff_commonOverlap_(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length / 2 || + overlap_length1 >= insertion.length / 2) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, + insertion.substring(0, overlap_length1))); + diffs[pointer - 1][1] = + deletion.substring(0, deletion.length - overlap_length1); + diffs[pointer + 1][1] = insertion.substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.length / 2 || + overlap_length2 >= insertion.length / 2) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, + deletion.substring(0, overlap_length2))); + diffs[pointer - 1][0] = DIFF_INSERT; + diffs[pointer - 1][1] = + insertion.substring(0, insertion.length - overlap_length2); + diffs[pointer + 1][0] = DIFF_DELETE; + diffs[pointer + 1][1] = + deletion.substring(overlap_length2); + pointer++; + } + } + pointer++; + } + pointer++; + } +}; + + +/** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * Closure, but does not reference any external variables. + * @param {string} one First string. + * @param {string} two Second string. + * @return {number} The score. + * @private + */ + function diff_cleanupSemanticScore_(one, two) { + if (!one || !two) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + var char1 = one.charAt(one.length - 1); + var char2 = two.charAt(0); + var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_); + var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_); + var whitespace1 = nonAlphaNumeric1 && + char1.match(diff_match_patch.whitespaceRegex_); + var whitespace2 = nonAlphaNumeric2 && + char2.match(diff_match_patch.whitespaceRegex_); + var lineBreak1 = whitespace1 && + char1.match(diff_match_patch.linebreakRegex_); + var lineBreak2 = whitespace2 && + char2.match(diff_match_patch.linebreakRegex_); + var blankLine1 = lineBreak1 && + one.match(diff_match_patch.blanklineEndRegex_); + var blankLine2 = lineBreak2 && + two.match(diff_match_patch.blanklineStartRegex_); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + var pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + var equality1 = diffs[pointer - 1][1]; + var edit = diffs[pointer][1]; + var equality2 = diffs[pointer + 1][1]; + + // First, shift the edit as far left as possible. + var commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset) { + var commonString = edit.substring(edit.length - commonOffset); + equality1 = equality1.substring(0, equality1.length - commonOffset); + edit = commonString + edit.substring(0, edit.length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + var bestEquality1 = equality1; + var bestEdit = edit; + var bestEquality2 = equality2; + var bestScore = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + while (edit.charAt(0) === equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + var score = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1][1] != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1) { + diffs[pointer - 1][1] = bestEquality1; + } else { + diffs.splice(pointer - 1, 1); + pointer--; + } + diffs[pointer][1] = bestEdit; + if (bestEquality2) { + diffs[pointer + 1][1] = bestEquality2; + } else { + diffs.splice(pointer + 1, 1); + pointer--; + } + } + } + pointer++; + } +}; + +// Define some regex patterns for matching boundaries. +diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/; +diff_match_patch.whitespaceRegex_ = /\s/; +diff_match_patch.linebreakRegex_ = /[\r\n]/; +diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/; +diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/; + +/** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastEquality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + var pre_ins = false; + // Is there a deletion operation before the last equality. + var pre_del = false; + // Is there an insertion operation after the last equality. + var post_ins = false; + // Is there a deletion operation after the last equality. + var post_del = false; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + if (diffs[pointer][1].length < this.Diff_EditCost && + (post_ins || post_del)) { + // Candidate found. + equalities[equalitiesLength++] = pointer; + pre_ins = post_ins; + pre_del = post_del; + lastEquality = diffs[pointer][1]; + } else { + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastEquality = null; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastEquality && ((pre_ins && pre_del && post_ins && post_del) || + ((lastEquality.length < this.Diff_EditCost / 2) && + (pre_ins + pre_del + post_ins + post_del) == 3))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastEquality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? + equalities[equalitiesLength - 1] : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { + // Add a dummy entry at the end. + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + var commonlength; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete !== 0 && count_insert !== 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength !== 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1][0] == + DIFF_EQUAL) { + diffs[pointer - count_delete - count_insert - 1][1] += + text_insert.substring(0, commonlength); + } else { + diffs.splice(0, 0, new diff_match_patch.Diff(DIFF_EQUAL, + text_insert.substring(0, commonlength))); + pointer++; + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength !== 0) { + diffs[pointer][1] = text_insert.substring(text_insert.length - + commonlength) + diffs[pointer][1]; + text_insert = text_insert.substring(0, text_insert.length - + commonlength); + text_delete = text_delete.substring(0, text_delete.length - + commonlength); + } + } + // Delete the offending records and add the merged ones. + pointer -= count_delete + count_insert; + diffs.splice(pointer, count_delete + count_insert); + if (text_delete.length) { + diffs.splice(pointer, 0, + new diff_match_patch.Diff(DIFF_DELETE, text_delete)); + pointer++; + } + if (text_insert.length) { + diffs.splice(pointer, 0, + new diff_match_patch.Diff(DIFF_INSERT, text_insert)); + pointer++; + } + pointer++; + } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + } + if (diffs[diffs.length - 1][1] === '') { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + var changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer][1].substring(diffs[pointer][1].length - + diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + + diffs[pointer][1].substring(0, diffs[pointer][1].length - + diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == + diffs[pointer + 1][1]) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = + diffs[pointer][1].substring(diffs[pointer + 1][1].length) + + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 + * @param {!Array.} diffs Array of diff tuples. + * @param {number} loc Location within text1. + * @return {number} Location within text2. + */ +diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { + var chars1 = 0; + var chars2 = 0; + var last_chars1 = 0; + var last_chars2 = 0; + var x; + for (x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. + chars1 += diffs[x][1].length; + } + if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. + chars2 += diffs[x][1].length; + } + if (chars1 > loc) { // Overshot the location. + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + // Was the location was deleted? + if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); +}; + + +/** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @return {string} HTML representation. + */ +diff_match_patch.prototype.diff_prettyHtml = function(diffs) { + var html = []; + var pattern_amp = /&/g; + var pattern_lt = //g; + var pattern_para = /\n/g; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; // Operation (insert, delete, equal) + var data = diffs[x][1]; // Text of change. + var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') + .replace(pattern_gt, '>').replace(pattern_para, '¶
'); + switch (op) { + case DIFF_INSERT: + html[x] = '' + text + ''; + break; + case DIFF_DELETE: + html[x] = '' + text + ''; + break; + case DIFF_EQUAL: + html[x] = '' + text + ''; + break; + } + } + return html.join(''); +}; + + +/** + * Compute and return the source text (all equalities and deletions). + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Source text. + */ +diff_match_patch.prototype.diff_text1 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute and return the destination text (all equalities and insertions). + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Destination text. + */ +diff_match_patch.prototype.diff_text2 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_DELETE) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param {!Array.} diffs Array of diff tuples. + * @return {number} Number of changes. + */ +diff_match_patch.prototype.diff_levenshtein = function(diffs) { + var levenshtein = 0; + var insertions = 0; + var deletions = 0; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; + var data = diffs[x][1]; + switch (op) { + case DIFF_INSERT: + insertions += data.length; + break; + case DIFF_DELETE: + deletions += data.length; + break; + case DIFF_EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; +}; + + +/** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param {!Array.} diffs Array of diff tuples. + * @return {string} Delta text. + */ +diff_match_patch.prototype.diff_toDelta = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + switch (diffs[x][0]) { + case DIFF_INSERT: + text[x] = '+' + encodeURI(diffs[x][1]); + break; + case DIFF_DELETE: + text[x] = '-' + diffs[x][1].length; + break; + case DIFF_EQUAL: + text[x] = '=' + diffs[x][1].length; + break; + } + } + return text.join('\t').replace(/%20/g, ' '); +}; + + +/** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param {string} text1 Source string for the diff. + * @param {string} delta Delta text. + * @return {!Array.} Array of diff tuples. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { + var diffs = []; + var diffsLength = 0; // Keeping our own length var is faster in JS. + var pointer = 0; // Cursor in text1 + var tokens = delta.split(/\t/g); + for (var x = 0; x < tokens.length; x++) { + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + var param = tokens[x].substring(1); + switch (tokens[x].charAt(0)) { + case '+': + try { + diffs[diffsLength++] = + new diff_match_patch.Diff(DIFF_INSERT, decodeURI(param)); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in diff_fromDelta: ' + param); + } + break; + case '-': + // Fall through. + case '=': + var n = parseInt(param, 10); + if (isNaN(n) || n < 0) { + throw new Error('Invalid number in diff_fromDelta: ' + param); + } + var text = text1.substring(pointer, pointer += n); + if (tokens[x].charAt(0) == '=') { + diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_EQUAL, text); + } else { + diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_DELETE, text); + } + break; + default: + // Blank tokens are ok (from a trailing \t). + // Anything else is an error. + if (tokens[x]) { + throw new Error('Invalid diff operation in diff_fromDelta: ' + + tokens[x]); + } + } + } + if (pointer != text1.length) { + throw new Error('Delta length (' + pointer + + ') does not equal source text length (' + text1.length + ').'); + } + return diffs; +}; + + +// MATCH FUNCTIONS + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + */ +diff_match_patch.prototype.match_main = function(text, pattern, loc) { + // Check for null inputs. + if (text == null || pattern == null || loc == null) { + throw new Error('Null input. (match_main)'); + } + + loc = Math.max(0, Math.min(loc, text.length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (!text.length) { + // Nothing to match. + return -1; + } else if (text.substring(loc, loc + pattern.length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return this.match_bitap_(text, pattern, loc); + } +}; + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + * @private + */ +diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) { + if (pattern.length > this.Match_MaxBits) { + throw new Error('Pattern too long for this browser.'); + } + + // Initialise the alphabet. + var s = this.match_alphabet_(pattern); + + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + */ + function match_bitapScore_(e, x) { + var accuracy = e / pattern.length; + var proximity = Math.abs(loc - x); + if (!dmp.Match_Distance) { + // Dodge divide by zero error. + return proximity ? 1.0 : accuracy; + } + return accuracy + (proximity / dmp.Match_Distance); + } + + // Highest score beyond which we give up. + var score_threshold = this.Match_Threshold; + // Is there a nearby exact match? (speedup) + var best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length); + if (best_loc != -1) { + score_threshold = + Math.min(match_bitapScore_(0, best_loc), score_threshold); + } + } + + // Initialise the bit arrays. + var matchmask = 1 << (pattern.length - 1); + best_loc = -1; + + var bin_min, bin_mid; + var bin_max = pattern.length + text.length; + var last_rd; + for (var d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at this + // error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + var start = Math.max(1, loc - bin_mid + 1); + var finish = Math.min(loc + bin_mid, text.length) + pattern.length; + + var rd = Array(finish + 2); + rd[finish + 1] = (1 << d) - 1; + for (var j = finish; j >= start; j--) { + // The alphabet (s) is a sparse hash, so the following line generates + // warnings. + var charMatch = s[text.charAt(j - 1)]; + if (d === 0) { // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | + (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | + last_rd[j + 1]; + } + if (rd[j] & matchmask) { + var score = match_bitapScore_(d, j - 1); + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if (match_bitapScore_(d + 1, loc) > score_threshold) { + break; + } + last_rd = rd; + } + return best_loc; +}; + + +/** + * Initialise the alphabet for the Bitap algorithm. + * @param {string} pattern The text to encode. + * @return {!Object} Hash of character locations. + * @private + */ +diff_match_patch.prototype.match_alphabet_ = function(pattern) { + var s = {}; + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] = 0; + } + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); + } + return s; +}; + + +// PATCH FUNCTIONS + + +/** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param {!diff_match_patch.patch_obj} patch The patch to grow. + * @param {string} text Source text. + * @private + */ +diff_match_patch.prototype.patch_addContext_ = function(patch, text) { + if (text.length == 0) { + return; + } + if (patch.start2 === null) { + throw Error('patch not initialized'); + } + var pattern = text.substring(patch.start2, patch.start2 + patch.length1); + var padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) && + pattern.length < this.Match_MaxBits - this.Patch_Margin - + this.Patch_Margin) { + padding += this.Patch_Margin; + pattern = text.substring(patch.start2 - padding, + patch.start2 + patch.length1 + padding); + } + // Add one chunk for good luck. + padding += this.Patch_Margin; + + // Add the prefix. + var prefix = text.substring(patch.start2 - padding, patch.start2); + if (prefix) { + patch.diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, prefix)); + } + // Add the suffix. + var suffix = text.substring(patch.start2 + patch.length1, + patch.start2 + patch.length1 + padding); + if (suffix) { + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length; + patch.start2 -= prefix.length; + // Extend the lengths. + patch.length1 += prefix.length + suffix.length; + patch.length2 += prefix.length + suffix.length; +}; + + +/** + * Compute a list of patches to turn text1 into text2. + * Use diffs if provided, otherwise compute it ourselves. + * There are four ways to call this function, depending on what data is + * available to the caller: + * Method 1: + * a = text1, b = text2 + * Method 2: + * a = diffs + * Method 3 (optimal): + * a = text1, b = diffs + * Method 4 (deprecated, use method 3): + * a = text1, b = text2, c = diffs + * + * @param {string|!Array.} a text1 (methods 1,3,4) or + * Array of diff tuples for text1 to text2 (method 2). + * @param {string|!Array.=} opt_b text2 (methods 1,4) or + * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). + * @param {string|!Array.=} opt_c Array of diff tuples + * for text1 to text2 (method 4) or undefined (methods 1,2,3). + * @return {!Array.} Array of Patch objects. + */ +diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) { + var text1, diffs; + if (typeof a == 'string' && typeof opt_b == 'string' && + typeof opt_c == 'undefined') { + // Method 1: text1, text2 + // Compute diffs from text1 and text2. + text1 = /** @type {string} */(a); + diffs = this.diff_main(text1, /** @type {string} */(opt_b), true); + if (diffs.length > 2) { + this.diff_cleanupSemantic(diffs); + this.diff_cleanupEfficiency(diffs); + } + } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' && + typeof opt_c == 'undefined') { + // Method 2: diffs + // Compute text1 from diffs. + diffs = /** @type {!Array.} */(a); + text1 = this.diff_text1(diffs); + } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' && + typeof opt_c == 'undefined') { + // Method 3: text1, diffs + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.} */(opt_b); + } else if (typeof a == 'string' && typeof opt_b == 'string' && + opt_c && typeof opt_c == 'object') { + // Method 4: text1, text2, diffs + // text2 is not used. + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.} */(opt_c); + } else { + throw new Error('Unknown call format to patch_make.'); + } + + if (diffs.length === 0) { + return []; // Get rid of the null case. + } + var patches = []; + var patch = new diff_match_patch.patch_obj(); + var patchDiffLength = 0; // Keeping our own length var is faster in JS. + var char_count1 = 0; // Number of characters into the text1 string. + var char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + var prepatch_text = text1; + var postpatch_text = text1; + for (var x = 0; x < diffs.length; x++) { + var diff_type = diffs[x][0]; + var diff_text = diffs[x][1]; + + if (!patchDiffLength && diff_type !== DIFF_EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (diff_type) { + case DIFF_INSERT: + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length2 += diff_text.length; + postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + + postpatch_text.substring(char_count2); + break; + case DIFF_DELETE: + patch.length1 += diff_text.length; + patch.diffs[patchDiffLength++] = diffs[x]; + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + + diff_text.length); + break; + case DIFF_EQUAL: + if (diff_text.length <= 2 * this.Patch_Margin && + patchDiffLength && diffs.length != x + 1) { + // Small equality inside a patch. + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length1 += diff_text.length; + patch.length2 += diff_text.length; + } else if (diff_text.length >= 2 * this.Patch_Margin) { + // Time for a new patch. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + patch = new diff_match_patch.patch_obj(); + patchDiffLength = 0; + // Unlike Unidiff, our patch lists have a rolling context. + // https://github.com/google/diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (diff_type !== DIFF_INSERT) { + char_count1 += diff_text.length; + } + if (diff_type !== DIFF_DELETE) { + char_count2 += diff_text.length; + } + } + // Pick up the leftover patch if not empty. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + } + + return patches; +}; + + +/** + * Given an array of patches, return another array that is identical. + * @param {!Array.} patches Array of Patch objects. + * @return {!Array.} Array of Patch objects. + */ +diff_match_patch.prototype.patch_deepCopy = function(patches) { + // Making deep copies is hard in JavaScript. + var patchesCopy = []; + for (var x = 0; x < patches.length; x++) { + var patch = patches[x]; + var patchCopy = new diff_match_patch.patch_obj(); + patchCopy.diffs = []; + for (var y = 0; y < patch.diffs.length; y++) { + patchCopy.diffs[y] = + new diff_match_patch.Diff(patch.diffs[y][0], patch.diffs[y][1]); + } + patchCopy.start1 = patch.start1; + patchCopy.start2 = patch.start2; + patchCopy.length1 = patch.length1; + patchCopy.length2 = patch.length2; + patchesCopy[x] = patchCopy; + } + return patchesCopy; +}; + + +/** + * Merge a set of patches onto the text. Return a patched text, as well + * as a list of true/false values indicating which patches were applied. + * @param {!Array.} patches Array of Patch objects. + * @param {string} text Old text. + * @return {!Array.>} Two element Array, containing the + * new text and an array of boolean values. + */ +diff_match_patch.prototype.patch_apply = function(patches, text) { + if (patches.length == 0) { + return [text, []]; + } + + // Deep copy the patches so that no changes are made to originals. + patches = this.patch_deepCopy(patches); + + var nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + this.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + var results = []; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = this.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), + expected_loc); + if (start_loc != -1) { + end_loc = this.match_main(text, + text1.substring(text1.length - this.Match_MaxBits), + expected_loc + text1.length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + + this.diff_text2(patches[x].diffs) + + text.substring(start_loc + text1.length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + var diffs = this.diff_main(text1, text2, false); + if (text1.length > this.Match_MaxBits && + this.diff_levenshtein(diffs) / text1.length > + this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + this.diff_cleanupSemanticLossless(diffs); + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = this.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + } else if (mod[0] === DIFF_DELETE) { // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + this.diff_xIndex(diffs, + index1 + mod[1].length)); + } + if (mod[0] !== DIFF_DELETE) { + index1 += mod[1].length; + } + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return [text, results]; +}; + + +/** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param {!Array.} patches Array of Patch objects. + * @return {string} The padding string added to each side. + */ +diff_match_patch.prototype.patch_addPadding = function(patches) { + var paddingLength = this.Patch_Margin; + var nullPadding = ''; + for (var x = 1; x <= paddingLength; x++) { + nullPadding += String.fromCharCode(x); + } + + // Bump all the patches forward. + for (var x = 0; x < patches.length; x++) { + patches[x].start1 += paddingLength; + patches[x].start2 += paddingLength; + } + + // Add some padding on start of first diff. + var patch = patches[0]; + var diffs = patch.diffs; + if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[0][1].length) { + // Grow first equality. + var extraLength = paddingLength - diffs[0][1].length; + diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches[patches.length - 1]; + diffs = patch.diffs; + if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[diffs.length - 1][1].length) { + // Grow last equality. + var extraLength = paddingLength - diffs[diffs.length - 1][1].length; + diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; +}; + + +/** + * Look through the patches and break up any which are longer than the maximum + * limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param {!Array.} patches Array of Patch objects. + */ +diff_match_patch.prototype.patch_splitMax = function(patches) { + var patch_size = this.Match_MaxBits; + for (var x = 0; x < patches.length; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + var bigpatch = patches[x]; + // Remove the big old patch. + patches.splice(x--, 1); + var start1 = bigpatch.start1; + var start2 = bigpatch.start2; + var precontext = ''; + while (bigpatch.diffs.length !== 0) { + // Create one of several smaller patches. + var patch = new diff_match_patch.patch_obj(); + var empty = true; + patch.start1 = start1 - precontext.length; + patch.start2 = start2 - precontext.length; + if (precontext !== '') { + patch.length1 = patch.length2 = precontext.length; + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, precontext)); + } + while (bigpatch.diffs.length !== 0 && + patch.length1 < patch_size - this.Patch_Margin) { + var diff_type = bigpatch.diffs[0][0]; + var diff_text = bigpatch.diffs[0][1]; + if (diff_type === DIFF_INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length; + start2 += diff_text.length; + patch.diffs.push(bigpatch.diffs.shift()); + empty = false; + } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 && + patch.diffs[0][0] == DIFF_EQUAL && + diff_text.length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length; + start1 += diff_text.length; + empty = false; + patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); + bigpatch.diffs.shift(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, + patch_size - patch.length1 - this.Patch_Margin); + patch.length1 += diff_text.length; + start1 += diff_text.length; + if (diff_type === DIFF_EQUAL) { + patch.length2 += diff_text.length; + start2 += diff_text.length; + } else { + empty = false; + } + patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs[0][1]) { + bigpatch.diffs.shift(); + } else { + bigpatch.diffs[0][1] = + bigpatch.diffs[0][1].substring(diff_text.length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = + precontext.substring(precontext.length - this.Patch_Margin); + // Append the end context for this patch. + var postcontext = this.diff_text1(bigpatch.diffs) + .substring(0, this.Patch_Margin); + if (postcontext !== '') { + patch.length1 += postcontext.length; + patch.length2 += postcontext.length; + if (patch.diffs.length !== 0 && + patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { + patch.diffs[patch.diffs.length - 1][1] += postcontext; + } else { + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, postcontext)); + } + } + if (!empty) { + patches.splice(++x, 0, patch); + } + } + } +}; + + +/** + * Take a list of patches and return a textual representation. + * @param {!Array.} patches Array of Patch objects. + * @return {string} Text representation of patches. + */ +diff_match_patch.prototype.patch_toText = function(patches) { + var text = []; + for (var x = 0; x < patches.length; x++) { + text[x] = patches[x]; + } + return text.join(''); +}; + + +/** + * Parse a textual representation of patches and return a list of Patch objects. + * @param {string} textline Text representation of patches. + * @return {!Array.} Array of Patch objects. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.patch_fromText = function(textline) { + var patches = []; + if (!textline) { + return patches; + } + var text = textline.split('\n'); + var textPointer = 0; + var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; + while (textPointer < text.length) { + var m = text[textPointer].match(patchHeader); + if (!m) { + throw new Error('Invalid patch string: ' + text[textPointer]); + } + var patch = new diff_match_patch.patch_obj(); + patches.push(patch); + patch.start1 = parseInt(m[1], 10); + if (m[2] === '') { + patch.start1--; + patch.length1 = 1; + } else if (m[2] == '0') { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = parseInt(m[2], 10); + } + + patch.start2 = parseInt(m[3], 10); + if (m[4] === '') { + patch.start2--; + patch.length2 = 1; + } else if (m[4] == '0') { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = parseInt(m[4], 10); + } + textPointer++; + + while (textPointer < text.length) { + var sign = text[textPointer].charAt(0); + try { + var line = decodeURI(text[textPointer].substring(1)); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in patch_fromText: ' + line); + } + if (sign == '-') { + // Deletion. + patch.diffs.push(new diff_match_patch.Diff(DIFF_DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.push(new diff_match_patch.Diff(DIFF_INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else if (sign === '') { + // Blank line? Whatever. + } else { + // WTF? + throw new Error('Invalid patch mode "' + sign + '" in: ' + line); + } + textPointer++; + } + } + return patches; +}; + + +/** + * Class representing one patch operation. + * @constructor + */ +diff_match_patch.patch_obj = function() { + /** @type {!Array.} */ + this.diffs = []; + /** @type {?number} */ + this.start1 = null; + /** @type {?number} */ + this.start2 = null; + /** @type {number} */ + this.length1 = 0; + /** @type {number} */ + this.length2 = 0; +}; + + +/** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indices are printed as 1-based, not 0-based. + * @return {string} The GNU diff string. + */ +diff_match_patch.patch_obj.prototype.toString = function() { + var coords1, coords2; + if (this.length1 === 0) { + coords1 = this.start1 + ',0'; + } else if (this.length1 == 1) { + coords1 = this.start1 + 1; + } else { + coords1 = (this.start1 + 1) + ',' + this.length1; + } + if (this.length2 === 0) { + coords2 = this.start2 + ',0'; + } else if (this.length2 == 1) { + coords2 = this.start2 + 1; + } else { + coords2 = (this.start2 + 1) + ',' + this.length2; + } + var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; + var op; + // Escape the body of the patch with %xx notation. + for (var x = 0; x < this.diffs.length; x++) { + switch (this.diffs[x][0]) { + case DIFF_INSERT: + op = '+'; + break; + case DIFF_DELETE: + op = '-'; + break; + case DIFF_EQUAL: + op = ' '; + break; + } + text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; + } + return text.join('').replace(/%20/g, ' '); +}; + +module.exports = { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL }; diff --git a/tests/android/androidTest.ts b/tests/android/androidTest.ts index 374190239b..7fb1cdecde 100644 --- a/tests/android/androidTest.ts +++ b/tests/android/androidTest.ts @@ -16,15 +16,15 @@ import type { AndroidDevice, BrowserContext } from '../../index'; import { CommonWorkerFixtures, baseTest } from '../config/baseTest'; -import * as folio from 'folio'; +import type { Fixtures } from '../config/test-runner'; import { PageTestFixtures } from '../page/pageTest'; -export { expect } from 'folio'; +export { expect } from '../config/test-runner'; type AndroidWorkerFixtures = { androidDevice: AndroidDevice; }; -export const androidFixtures: folio.Fixtures = { +export const androidFixtures: Fixtures = { androidDevice: [ async ({ playwright }, run) => { const device = (await playwright._android.devices())[0]; await device.shell('am force-stop org.chromium.webview_shell'); diff --git a/tests/config/android.config.ts b/tests/config/android.config.ts index 0eafb6979e..83328bbf9f 100644 --- a/tests/config/android.config.ts +++ b/tests/config/android.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import * as folio from 'folio'; +import type { Config } from '../config/test-runner'; import * as path from 'path'; import { test as pageTest } from '../page/pageTest'; import { androidFixtures } from '../android/androidTest'; @@ -23,7 +23,7 @@ import { CommonOptions } from './baseTest'; const outputDir = path.join(__dirname, '..', '..', 'test-results'); const testDir = path.join(__dirname, '..'); -const config: folio.Config = { +const config: Config = { testDir, outputDir, timeout: 120000, diff --git a/tests/config/baseTest.ts b/tests/config/baseTest.ts index 486592409d..6443662c04 100644 --- a/tests/config/baseTest.ts +++ b/tests/config/baseTest.ts @@ -15,7 +15,7 @@ */ import { TestServer } from '../../utils/testserver'; -import * as folio from 'folio'; +import { Fixtures, TestType } from './test-runner'; import * as path from 'path'; import * as fs from 'fs'; import socks from 'socksv5'; @@ -101,7 +101,7 @@ class DefaultMode { } } -const baseFixtures: folio.Fixtures<{}, BaseOptions & BaseFixtures> = { +const baseFixtures: Fixtures<{}, BaseOptions & BaseFixtures> = { mode: [ 'default', { scope: 'worker' } ], browserName: [ 'chromium' , { scope: 'worker' } ], channel: [ undefined, { scope: 'worker' } ], @@ -136,7 +136,7 @@ type ServerFixtures = { }; type ServersInternal = ServerFixtures & { socksServer: any }; -const serverFixtures: folio.Fixtures = { +const serverFixtures: Fixtures = { loopback: [ undefined, { scope: 'worker' } ], __servers: [ async ({ loopback }, run, workerInfo) => { const assetsPath = path.join(__dirname, '..', 'assets'); @@ -208,7 +208,7 @@ type CoverageOptions = { coverageName?: string; }; -const coverageFixtures: folio.Fixtures<{}, CoverageOptions & { __collectCoverage: void }> = { +const coverageFixtures: Fixtures<{}, CoverageOptions & { __collectCoverage: void }> = { coverageName: [ undefined, { scope: 'worker' } ], __collectCoverage: [ async ({ coverageName }, run, workerInfo) => { @@ -230,4 +230,5 @@ const coverageFixtures: folio.Fixtures<{}, CoverageOptions & { __collectCoverage export type CommonOptions = BaseOptions & ServerOptions & CoverageOptions; export type CommonWorkerFixtures = CommonOptions & BaseFixtures; -export const baseTest = folio.test.extend<{}, CoverageOptions>(coverageFixtures).extend(serverFixtures).extend<{}, BaseOptions & BaseFixtures>(baseFixtures); +const __baseTest = require('./test-runner').__baseTest as TestType<{}, {}>; +export const baseTest = __baseTest.extend<{}, CoverageOptions>(coverageFixtures).extend(serverFixtures).extend<{}, BaseOptions & BaseFixtures>(baseFixtures); diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index e6270f1d3a..383da47034 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import * as folio from 'folio'; +import type { Fixtures } from './test-runner'; import type { Browser, BrowserContext, BrowserContextOptions, BrowserType, LaunchOptions, Page } from '../../index'; import { removeFolders } from '../../lib/utils/utils'; import * as path from 'path'; @@ -49,7 +49,7 @@ type PlaywrightTestFixtures = { }; export type PlaywrightOptions = PlaywrightWorkerOptions & PlaywrightTestOptions; -export const playwrightFixtures: folio.Fixtures = { +export const playwrightFixtures: Fixtures = { tracesDir: [ undefined, { scope: 'worker' } ], executablePath: [ undefined, { scope: 'worker' } ], proxy: [ undefined, { scope: 'worker' } ], @@ -159,4 +159,4 @@ export const playwrightTest = test; export const browserTest = test; export const contextTest = test; -export { expect } from 'folio'; +export { expect } from './test-runner'; diff --git a/tests/config/default.config.ts b/tests/config/default.config.ts index c2d51d1419..5210a4c9ab 100644 --- a/tests/config/default.config.ts +++ b/tests/config/default.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import * as folio from 'folio'; +import type { Config } from './test-runner'; import * as path from 'path'; import { PlaywrightOptions, playwrightFixtures } from './browserTest'; import { test as pageTest } from '../page/pageTest'; @@ -45,7 +45,7 @@ const video = !!process.env.PWTEST_VIDEO; const outputDir = path.join(__dirname, '..', '..', 'test-results'); const testDir = path.join(__dirname, '..'); -const config: folio.Config = { +const config: Config = { testDir, outputDir, timeout: video || process.env.PWTRACE ? 60000 : 30000, @@ -66,7 +66,7 @@ for (const browserName of browserNames) { if (executablePath && !process.env.TEST_WORKER_INDEX) console.error(`Using executable at ${executablePath}`); const testIgnore: RegExp[] = browserNames.filter(b => b !== browserName).map(b => new RegExp(b)); - testIgnore.push(/android/, /electron/); + testIgnore.push(/android/, /electron/, /playwrigh-test/); config.projects.push({ name: browserName, testDir, diff --git a/tests/config/electron.config.ts b/tests/config/electron.config.ts index 0cd8a2343f..a6e480a881 100644 --- a/tests/config/electron.config.ts +++ b/tests/config/electron.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import * as folio from 'folio'; +import type { Config } from './test-runner'; import * as path from 'path'; import { electronFixtures } from '../electron/electronTest'; import { test as pageTest } from '../page/pageTest'; @@ -23,7 +23,7 @@ import { CommonOptions } from './baseTest'; const outputDir = path.join(__dirname, '..', '..', 'test-results'); const testDir = path.join(__dirname, '..'); -const config: folio.Config = { +const config: Config = { testDir, outputDir, timeout: 30000, diff --git a/tests/config/test-runner/.gitignore b/tests/config/test-runner/.gitignore new file mode 100644 index 0000000000..2ccbe4656c --- /dev/null +++ b/tests/config/test-runner/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/tests/config/test-runner/README.md b/tests/config/test-runner/README.md new file mode 100644 index 0000000000..0d049d1028 --- /dev/null +++ b/tests/config/test-runner/README.md @@ -0,0 +1,3 @@ +This directory holds a stable test runner: +- It is possible to test broken test runner with a stable working one. +- Dependencies between ToT and stable test runner do not clash. diff --git a/tests/config/test-runner/index.d.ts b/tests/config/test-runner/index.d.ts new file mode 100644 index 0000000000..71da899b22 --- /dev/null +++ b/tests/config/test-runner/index.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './node_modules/@playwright/test'; diff --git a/tests/config/test-runner/index.js b/tests/config/test-runner/index.js new file mode 100644 index 0000000000..9705b868f3 --- /dev/null +++ b/tests/config/test-runner/index.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = require('./node_modules/@playwright/test'); diff --git a/tests/config/test-runner/package-lock.json b/tests/config/test-runner/package-lock.json new file mode 100644 index 0000000000..8d2a2c17cf --- /dev/null +++ b/tests/config/test-runner/package-lock.json @@ -0,0 +1,3358 @@ +{ + "name": "test-runner", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@playwright/test": "=1.12.0-next-1622928816000" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dependencies": { + "@babel/highlight": "^7.12.13" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz", + "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==" + }, + "node_modules/@babel/core": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.3", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-module-transforms": "^7.14.2", + "@babel/helpers": "^7.14.0", + "@babel/parser": "^7.14.3", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", + "dependencies": { + "@babel/types": "^7.14.2", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", + "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz", + "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==", + "dependencies": { + "@babel/compat-data": "^7.14.4", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz", + "integrity": "sha512-idr3pthFlDCpV+p/rMgGLGYIVtazeatrSOQk8YzO2pAepIjQhCN3myeihVg58ax2bbbGK9PUE1reFi7axOYIOw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-function-name": "^7.14.2", + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-replace-supers": "^7.14.4", + "@babel/helper-split-export-declaration": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz", + "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==", + "dependencies": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.14.2" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", + "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "dependencies": { + "@babel/types": "^7.13.12" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "dependencies": { + "@babel/types": "^7.13.12" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", + "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", + "dependencies": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-replace-supers": "^7.13.12", + "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.14.0", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==" + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz", + "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.4" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", + "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "dependencies": { + "@babel/types": "^7.13.12" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "dependencies": { + "@babel/types": "^7.12.1" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dependencies": { + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==" + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" + }, + "node_modules/@babel/helpers": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", + "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", + "dependencies": { + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz", + "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", + "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.2.tgz", + "integrity": "sha512-oxVQZIWFh91vuNEMKltqNsKLFWkOIyJc95k2Gv9lWVyDfPUQGSSlbDEgWuJUU1afGE9WwlzpucMZ3yDRHIItkA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.2.tgz", + "integrity": "sha512-sRxW3z3Zp3pFfLAgVEvzTFutTXax837oOatUIvSG9o5gRj9mKwm3br1Se5f4QalTQs9x4AzlA/HrCWbQIHASUQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.2.tgz", + "integrity": "sha512-1JAZtUrqYyGsS7IDmFeaem+/LJqujfLZ2weLR9ugB0ufUPjzf8cguyVT1g5im7f7RXxuLq1xUxEzvm68uYRtGg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz", + "integrity": "sha512-ebR0zU9OvI2N4qiAC38KIAK75KItpIPTpAtd2r4OZmMFeKbKJpUFLYP2EuDut82+BmYi8sz42B+TfTptJ9iG5Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.2.tgz", + "integrity": "sha512-DcTQY9syxu9BpU3Uo94fjCB3LN9/hgPS8oUL7KrSW3bA2ePrKZZPJcc5y0hoJAM9dft3pGfErtEUvxXQcfLxUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.2.tgz", + "integrity": "sha512-qQByMRPwMZJainfig10BoaDldx/+VDtNcrA7qdNaEOAj6VXud+gfrkA8j4CRAU5HjnWREXqIpSpH30qZX1xivA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", + "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.0.tgz", + "integrity": "sha512-59ANdmEwwRUkLjB7CRtwJxxwtjESw+X2IePItA+RGQh+oy5RmpCh/EvVVvh5XQc3yxsm5gtv0+i9oBZhaDNVTg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-create-class-features-plugin": "^7.14.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-private-property-in-object": "^7.14.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.0.tgz", + "integrity": "sha512-bda3xF8wGl5/5btF794utNOL0Jw+9jE5C1sLZcoK7c4uonE/y3iQiyG+KbkF3WBV/paX58VCpjhxLPkdj5Fe4w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", + "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.14.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-simple-access": "^7.13.12", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.4.tgz", + "integrity": "sha512-WYdcGNEO7mCCZ2XzRlxwGj3PgeAr50ifkofOUC/+IN/GzKLB+biDPVBUAQN2C/dVZTvEXCp80kfQ1FFZPrwykQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.14.4", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-typescript": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", + "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-typescript": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "node_modules/@babel/traverse": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", + "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.2", + "@babel/helper-function-name": "^7.14.2", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.2", + "@babel/types": "^7.14.2", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "node_modules/@babel/types": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz", + "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@playwright/test": { + "version": "1.12.0-next-1622928816000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.12.0-next-1622928816000.tgz", + "integrity": "sha512-vOitU8srbM0mk7hHsklchL06Tmd7j2NF4wawBkRsdTHg/ANn2Qvm4qSfplf4zXZbdlNiiAvsvXXhhBteXbLAmA==", + "hasInstallScript": true, + "dependencies": { + "commander": "^6.1.0", + "debug": "^4.1.1", + "extract-zip": "^2.0.1", + "folio": "=0.4.0-alpha28", + "https-proxy-agent": "^5.0.0", + "jpeg-js": "^0.4.2", + "mime": "^2.4.6", + "pngjs": "^5.0.0", + "progress": "^2.0.3", + "proper-lockfile": "^4.1.1", + "proxy-from-env": "^1.1.0", + "rimraf": "^3.0.2", + "stack-utils": "^2.0.3", + "ws": "^7.4.6", + "yazl": "^2.5.1" + }, + "bin": { + "playwright": "lib/cli/cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "15.12.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.1.tgz", + "integrity": "sha512-zyxJM8I1c9q5sRMtVF+zdd13Jt6RU4r4qfhTd7lQubyThvLfx6yYekWSQjGCGV2Tkecgxnlpl/DNlb6Hg+dmEw==" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==" + }, + "node_modules/@types/yargs": { + "version": "15.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", + "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" + }, + "node_modules/@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dependencies": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "engines": { + "node": "*" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001234", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001234.tgz", + "integrity": "sha512-a3gjUVKkmwLdNysa1xkUAwN2VfJUJyVW47rsi3aCbkRCtbHAfo+rOsCqVw29G6coQ8gzAPb5XBXwiGHwme3isA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.3.749", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz", + "integrity": "sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expect/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/expect/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/folio": { + "version": "0.4.0-alpha28", + "resolved": "https://registry.npmjs.org/folio/-/folio-0.4.0-alpha28.tgz", + "integrity": "sha512-sbHdEDRXPkkhzHAyRy/tQKTWImNy38cICoii4ox9AGYFVWgF+i4l37AL2cVfJkUEvUqZpq+u4NkuV1cMelV5AA==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/core": "^7.14.0", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-dynamic-import": "^7.13.8", + "@babel/plugin-proposal-export-namespace-from": "^7.12.13", + "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-numeric-separator": "^7.12.13", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-private-property-in-object": "^7.14.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.14.0", + "@babel/preset-typescript": "^7.13.0", + "colors": "^1.4.0", + "commander": "^6.1.0", + "expect": "^26.4.2", + "jpeg-js": "^0.4.2", + "minimatch": "^3.0.3", + "ms": "^2.1.2", + "pirates": "^4.0.1", + "pixelmatch": "^5.2.1", + "rimraf": "^3.0.2", + "source-map-support": "^0.4.18", + "stack-utils": "^2.0.2" + }, + "bin": { + "folio": "cli.js" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", + "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dependencies": { + "node-modules-regexp": "^1.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pixelmatch": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", + "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", + "dependencies": { + "pngjs": "^4.0.1" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", + "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pretty-format/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/compat-data": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz", + "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==" + }, + "@babel/core": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.3", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-module-transforms": "^7.14.2", + "@babel/helpers": "^7.14.0", + "@babel/parser": "^7.14.3", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", + "requires": { + "@babel/types": "^7.14.2", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", + "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz", + "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==", + "requires": { + "@babel/compat-data": "^7.14.4", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.4.tgz", + "integrity": "sha512-idr3pthFlDCpV+p/rMgGLGYIVtazeatrSOQk8YzO2pAepIjQhCN3myeihVg58ax2bbbGK9PUE1reFi7axOYIOw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-function-name": "^7.14.2", + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-replace-supers": "^7.14.4", + "@babel/helper-split-export-declaration": "^7.12.13" + } + }, + "@babel/helper-function-name": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz", + "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.14.2" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", + "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-transforms": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", + "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", + "requires": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-replace-supers": "^7.13.12", + "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.14.0", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==" + }, + "@babel/helper-replace-supers": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz", + "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", + "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==" + }, + "@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" + }, + "@babel/helpers": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", + "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", + "requires": { + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" + } + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz", + "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==" + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", + "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.2.tgz", + "integrity": "sha512-oxVQZIWFh91vuNEMKltqNsKLFWkOIyJc95k2Gv9lWVyDfPUQGSSlbDEgWuJUU1afGE9WwlzpucMZ3yDRHIItkA==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.2.tgz", + "integrity": "sha512-sRxW3z3Zp3pFfLAgVEvzTFutTXax837oOatUIvSG9o5gRj9mKwm3br1Se5f4QalTQs9x4AzlA/HrCWbQIHASUQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.2.tgz", + "integrity": "sha512-1JAZtUrqYyGsS7IDmFeaem+/LJqujfLZ2weLR9ugB0ufUPjzf8cguyVT1g5im7f7RXxuLq1xUxEzvm68uYRtGg==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz", + "integrity": "sha512-ebR0zU9OvI2N4qiAC38KIAK75KItpIPTpAtd2r4OZmMFeKbKJpUFLYP2EuDut82+BmYi8sz42B+TfTptJ9iG5Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.2.tgz", + "integrity": "sha512-DcTQY9syxu9BpU3Uo94fjCB3LN9/hgPS8oUL7KrSW3bA2ePrKZZPJcc5y0hoJAM9dft3pGfErtEUvxXQcfLxUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.2.tgz", + "integrity": "sha512-qQByMRPwMZJainfig10BoaDldx/+VDtNcrA7qdNaEOAj6VXud+gfrkA8j4CRAU5HjnWREXqIpSpH30qZX1xivA==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", + "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.0.tgz", + "integrity": "sha512-59ANdmEwwRUkLjB7CRtwJxxwtjESw+X2IePItA+RGQh+oy5RmpCh/EvVVvh5XQc3yxsm5gtv0+i9oBZhaDNVTg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-create-class-features-plugin": "^7.14.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-private-property-in-object": "^7.14.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.0.tgz", + "integrity": "sha512-bda3xF8wGl5/5btF794utNOL0Jw+9jE5C1sLZcoK7c4uonE/y3iQiyG+KbkF3WBV/paX58VCpjhxLPkdj5Fe4w==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", + "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", + "requires": { + "@babel/helper-module-transforms": "^7.14.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-simple-access": "^7.13.12", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.4.tgz", + "integrity": "sha512-WYdcGNEO7mCCZ2XzRlxwGj3PgeAr50ifkofOUC/+IN/GzKLB+biDPVBUAQN2C/dVZTvEXCp80kfQ1FFZPrwykQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.4", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-typescript": "^7.12.13" + } + }, + "@babel/preset-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", + "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-typescript": "^7.13.0" + } + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", + "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.2", + "@babel/helper-function-name": "^7.14.2", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.2", + "@babel/types": "^7.14.2", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz", + "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@playwright/test": { + "version": "1.12.0-next-1622928816000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.12.0-next-1622928816000.tgz", + "integrity": "sha512-vOitU8srbM0mk7hHsklchL06Tmd7j2NF4wawBkRsdTHg/ANn2Qvm4qSfplf4zXZbdlNiiAvsvXXhhBteXbLAmA==", + "requires": { + "commander": "^6.1.0", + "debug": "^4.1.1", + "extract-zip": "^2.0.1", + "folio": "=0.4.0-alpha28", + "https-proxy-agent": "^5.0.0", + "jpeg-js": "^0.4.2", + "mime": "^2.4.6", + "pngjs": "^5.0.0", + "progress": "^2.0.3", + "proper-lockfile": "^4.1.1", + "proxy-from-env": "^1.1.0", + "rimraf": "^3.0.2", + "stack-utils": "^2.0.3", + "ws": "^7.4.6", + "yazl": "^2.5.1" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/node": { + "version": "15.12.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.1.tgz", + "integrity": "sha512-zyxJM8I1c9q5sRMtVF+zdd13Jt6RU4r4qfhTd7lQubyThvLfx6yYekWSQjGCGV2Tkecgxnlpl/DNlb6Hg+dmEw==" + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==" + }, + "@types/yargs": { + "version": "15.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", + "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" + }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "requires": { + "object.assign": "^4.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "caniuse-lite": { + "version": "1.0.30001234", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001234.tgz", + "integrity": "sha512-a3gjUVKkmwLdNysa1xkUAwN2VfJUJyVW47rsi3aCbkRCtbHAfo+rOsCqVw29G6coQ8gzAPb5XBXwiGHwme3isA==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==" + }, + "electron-to-chromium": { + "version": "1.3.749", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz", + "integrity": "sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "requires": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "folio": { + "version": "0.4.0-alpha28", + "resolved": "https://registry.npmjs.org/folio/-/folio-0.4.0-alpha28.tgz", + "integrity": "sha512-sbHdEDRXPkkhzHAyRy/tQKTWImNy38cICoii4ox9AGYFVWgF+i4l37AL2cVfJkUEvUqZpq+u4NkuV1cMelV5AA==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/core": "^7.14.0", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-dynamic-import": "^7.13.8", + "@babel/plugin-proposal-export-namespace-from": "^7.12.13", + "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-numeric-separator": "^7.12.13", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-private-property-in-object": "^7.14.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.14.0", + "@babel/preset-typescript": "^7.13.0", + "colors": "^1.4.0", + "commander": "^6.1.0", + "expect": "^26.4.2", + "jpeg-js": "^0.4.2", + "minimatch": "^3.0.3", + "ms": "^2.1.2", + "pirates": "^4.0.1", + "pixelmatch": "^5.2.1", + "rimraf": "^3.0.2", + "source-map-support": "^0.4.18", + "stack-utils": "^2.0.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==" + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==" + }, + "jpeg-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", + "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" + }, + "node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pixelmatch": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", + "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", + "requires": { + "pngjs": "^4.0.1" + }, + "dependencies": { + "pngjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", + "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==" + } + } + }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "^0.5.6" + } + }, + "stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "requires": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/tests/config/test-runner/package.json b/tests/config/test-runner/package.json new file mode 100644 index 0000000000..5e1541a390 --- /dev/null +++ b/tests/config/test-runner/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "dependencies": { + "@playwright/test": "=1.12.0-next-1622928816000" + } +} diff --git a/tests/config/utils.ts b/tests/config/utils.ts index 4e24a05619..ba0d776964 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { expect } from 'folio'; +import { expect } from './test-runner'; import type { Frame, Page } from '../../index'; export async function attachFrame(page: Page, frameId: string, url: string): Promise { diff --git a/tests/electron/electronTest.ts b/tests/electron/electronTest.ts index 4680c23ac4..151316a10d 100644 --- a/tests/electron/electronTest.ts +++ b/tests/electron/electronTest.ts @@ -16,10 +16,10 @@ import { baseTest, CommonWorkerFixtures } from '../config/baseTest'; import { ElectronApplication, Page } from '../../index'; -import * as folio from 'folio'; +import type { Fixtures } from '../config/test-runner'; import * as path from 'path'; import { PageTestFixtures } from '../page/pageTest'; -export { expect } from 'folio'; +export { expect } from '../config/test-runner'; type ElectronTestFixtures = PageTestFixtures & { electronApp: ElectronApplication; @@ -27,7 +27,7 @@ type ElectronTestFixtures = PageTestFixtures & { }; const electronVersion = require('electron/package.json').version; -export const electronFixtures: folio.Fixtures = { +export const electronFixtures: Fixtures = { browserVersion: electronVersion, browserMajorVersion: Number(electronVersion.split('.')[0]), isAndroid: false, diff --git a/tests/inspector/inspectorTest.ts b/tests/inspector/inspectorTest.ts index c9f0192622..8ae288f90c 100644 --- a/tests/inspector/inspectorTest.ts +++ b/tests/inspector/inspectorTest.ts @@ -20,7 +20,7 @@ import * as path from 'path'; import type { Source } from '../../src/server/supplements/recorder/recorderTypes'; import { ChildProcess, spawn } from 'child_process'; import { chromium } from '../../index'; -export { expect } from 'folio'; +export { expect } from '../config/test-runner'; type CLITestArgs = { recorderPageGetter: () => Promise; diff --git a/tests/page/pageTest.ts b/tests/page/pageTest.ts index 4692e0dd7b..5e8f21ad79 100644 --- a/tests/page/pageTest.ts +++ b/tests/page/pageTest.ts @@ -16,7 +16,7 @@ import { baseTest } from '../config/baseTest'; import type { Page } from '../../index'; -export { expect } from 'folio'; +export { expect } from '../config/test-runner'; // Page test does not guarantee an isolated context, just a new page (because Android). export type PageTestFixtures = { diff --git a/tests/playwright-test/access-data.spec.ts b/tests/playwright-test/access-data.spec.ts new file mode 100644 index 0000000000..db40dc9659 --- /dev/null +++ b/tests/playwright-test/access-data.spec.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should access error in fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'test-error-visible-in-env.spec.ts': ` + const test = folio.test.extend({ + foo: [async ({}, run, testInfo) => { + await run(); + console.log('ERROR[[[' + JSON.stringify(testInfo.error, undefined, 2) + ']]]'); + }, { auto: true }], + }); + test('ensure env handles test error', async ({}) => { + expect(true).toBe(false); + }); + ` + }, {}); + expect(result.exitCode).toBe(1); + const start = result.output.indexOf('ERROR[[[') + 8; + const end = result.output.indexOf(']]]'); + const data = JSON.parse(result.output.substring(start, end)); + expect(data.message).toContain('Object.is equality'); +}); + +test('should access annotations in fixture', async ({ runInlineTest }) => { + const { exitCode, report } = await runInlineTest({ + 'test-data-visible-in-env.spec.ts': ` + const test = folio.test.extend({ + foo: [async ({}, run, testInfo) => { + await run(); + testInfo.annotations.push({ type: 'myname', description: 'hello' }); + }, { auto: true }], + }); + test('ensure env can set data', async ({}, testInfo) => { + test.slow(true, 'just slow'); + + console.log('console.log'); + console.error('console.error'); + expect(testInfo.config.rootDir).toBeTruthy(); + expect(testInfo.file).toContain('test-data-visible-in-env'); + }); + ` + }); + expect(exitCode).toBe(0); + const test = report.suites[0].specs[0].tests[0]; + expect(test.annotations).toEqual([ { type: 'slow', description: 'just slow' }, { type: 'myname', description: 'hello' } ]); + expect(test.results[0].stdout).toEqual([{ text: 'console.log\n' }]); + expect(test.results[0].stderr).toEqual([{ text: 'console.error\n' }]); +}); + +test('should report projectName in result', async ({ runInlineTest }) => { + const { exitCode, report } = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + projects: [ + { name: 'foo' }, + {}, + ], + }; + `, + 'test-data-visible-in-env.spec.ts': ` + folio.test('some test', async ({}, testInfo) => { + }); + ` + }); + expect(report.suites[0].specs[0].tests[0].projectName).toBe('foo'); + expect(report.suites[0].specs[0].tests[1].projectName).toBe(''); + expect(exitCode).toBe(0); +}); diff --git a/tests/playwright-test/assets/screenshot-canvas-actual.png b/tests/playwright-test/assets/screenshot-canvas-actual.png new file mode 100644 index 0000000000..da464f96df Binary files /dev/null and b/tests/playwright-test/assets/screenshot-canvas-actual.png differ diff --git a/tests/playwright-test/assets/screenshot-canvas-expected.png b/tests/playwright-test/assets/screenshot-canvas-expected.png new file mode 100644 index 0000000000..dfafbda638 Binary files /dev/null and b/tests/playwright-test/assets/screenshot-canvas-expected.png differ diff --git a/tests/playwright-test/base-reporter.spec.ts b/tests/playwright-test/base-reporter.spec.ts new file mode 100644 index 0000000000..0395642bb5 --- /dev/null +++ b/tests/playwright-test/base-reporter.spec.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +test('handle long test names', async ({ runInlineTest }) => { + const title = 'title'.repeat(30); + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('${title}', async ({}) => { + expect(1).toBe(0); + }); + `, + }); + expect(stripAscii(result.output)).toContain('expect(1).toBe'); + expect(result.exitCode).toBe(1); +}); + +test('print the error name', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const { test } = folio; + test('foobar', async ({}) => { + const error = new Error('my-message'); + error.name = 'FooBarError'; + throw error; + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('FooBarError: my-message'); +}); + +test('print should print the error name without a message', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const { test } = folio; + test('foobar', async ({}) => { + const error = new Error(); + error.name = 'FooBarError'; + throw error; + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('FooBarError'); +}); + +test('print an error in a codeframe', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'my-lib.ts': ` + const foobar = () => { + const error = new Error('my-message'); + error.name = 'FooBarError'; + throw error; + } + export default () => { + foobar(); + } + `, + 'a.spec.ts': ` + const { test } = folio; + import myLib from './my-lib'; + test('foobar', async ({}) => { + const error = new Error('my-message'); + error.name = 'FooBarError'; + throw error; + }); + ` + }, {}, { + FORCE_COLOR: '0', + }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('FooBarError: my-message'); + expect(result.output).toContain('test(\'foobar\', async'); + expect(result.output).toContain('throw error;'); + expect(result.output).toContain('import myLib from \'./my-lib\';'); +}); diff --git a/tests/playwright-test/basic.spec.ts b/tests/playwright-test/basic.spec.ts new file mode 100644 index 0000000000..ad499835e0 --- /dev/null +++ b/tests/playwright-test/basic.spec.ts @@ -0,0 +1,253 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; +import * as path from 'path'; + +test('should fail', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'one-failure.spec.ts': ` + const { test } = folio; + test('fails', () => { + expect(1 + 1).toBe(7); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.output).toContain('1) one-failure.spec.ts:6'); +}); + +test('should timeout', async ({ runInlineTest }) => { + const { exitCode, passed, failed, output } = await runInlineTest({ + 'one-timeout.spec.js': ` + const { test } = folio; + test('timeout', async () => { + await new Promise(f => setTimeout(f, 10000)); + }); + ` + }, { timeout: 100 }); + expect(exitCode).toBe(1); + expect(passed).toBe(0); + expect(failed).toBe(1); + expect(output).toContain('Timeout of 100ms exceeded.'); +}); + +test('should succeed', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'one-success.spec.js': ` + const { test } = folio; + test('succeeds', () => { + expect(1 + 1).toBe(2); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); +}); + +test('should report suite errors', async ({ runInlineTest }) => { + const { exitCode, failed, output } = await runInlineTest({ + 'suite-error.spec.js': ` + if (new Error().stack.includes('workerRunner')) + throw new Error('Suite error'); + + const { test } = folio; + test('passes',() => { + expect(1 + 1).toBe(2); + }); + ` + }); + expect(exitCode).toBe(1); + expect(failed).toBe(1); + expect(output).toContain('Suite error'); +}); + +test('should respect nested skip', async ({ runInlineTest }) => { + const { exitCode, passed, failed, skipped } = await runInlineTest({ + 'nested-skip.spec.js': ` + const { test } = folio; + test.describe('skipped', () => { + test.skip(); + test('succeeds',() => { + expect(1 + 1).toBe(2); + }); + }); + ` + }); + expect(exitCode).toBe(0); + expect(passed).toBe(0); + expect(failed).toBe(0); + expect(skipped).toBe(1); +}); + +test('should respect excluded tests', async ({ runInlineTest }) => { + const { exitCode, passed } = await runInlineTest({ + 'excluded.spec.ts': ` + const { test } = folio; + test('included test', () => { + expect(1 + 1).toBe(2); + }); + + test('excluded test', () => { + test.skip(); + expect(1 + 1).toBe(3); + }); + + test('excluded test', () => { + test.skip(); + expect(1 + 1).toBe(3); + }); + + test.describe('included describe', () => { + test('included describe test', () => { + expect(1 + 1).toBe(2); + }); + }); + + test.describe('excluded describe', () => { + test.skip(); + test('excluded describe test', () => { + expect(1 + 1).toBe(3); + }); + }); + `, + }); + expect(passed).toBe(2); + expect(exitCode).toBe(0); +}); + +test('should respect focused tests', async ({ runInlineTest }) => { + const { exitCode, passed } = await runInlineTest({ + 'focused.spec.ts': ` + const { test } = folio; + test('included test', () => { + expect(1 + 1).toBe(3); + }); + + test.only('focused test', () => { + expect(1 + 1).toBe(2); + }); + + test.only('focused only test', () => { + expect(1 + 1).toBe(2); + }); + + test.describe.only('focused describe', () => { + test('describe test', () => { + expect(1 + 1).toBe(2); + }); + }); + + test.describe('non-focused describe', () => { + test('describe test', () => { + expect(1 + 1).toBe(3); + }); + }); + + test.describe.only('focused describe', () => { + test('test1', () => { + expect(1 + 1).toBe(2); + }); + test.only('test2', () => { + expect(1 + 1).toBe(2); + }); + test('test3', () => { + expect(1 + 1).toBe(2); + }); + test.only('test4', () => { + expect(1 + 1).toBe(2); + }); + }); + ` + }); + expect(passed).toBe(5); + expect(exitCode).toBe(0); +}); + +test('skip should take priority over fail', async ({ runInlineTest }) => { + const { passed, skipped, failed } = await runInlineTest({ + 'test.spec.ts': ` + const { test } = folio; + test.describe('failing suite', () => { + test.fail(); + + test('skipped', () => { + test.skip(); + expect(1 + 1).toBe(3); + }); + + test('passing', () => { + expect(1 + 1).toBe(3); + }); + test('passing2', () => { + expect(1 + 1).toBe(3); + }); + + test('failing', () => { + expect(1 + 1).toBe(2); + }); + }); + ` + }); + expect(passed).toBe(2); + expect(skipped).toBe(1); + expect(failed).toBe(1); +}); + +test('should focus test from one runTests', async ({ runInlineTest }) => { + const { exitCode, passed, skipped, failed } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { projects: [ + { testDir: path.join(__dirname, 'a') }, + { testDir: path.join(__dirname, 'b') }, + ] }; + `, + 'a/afile.spec.ts': ` + const { test } = folio; + test('just a test', () => { + expect(1 + 1).toBe(3); + }); + `, + 'b/bfile.spec.ts': ` + const { test } = folio; + test.only('focused test', () => { + expect(1 + 1).toBe(2); + }); + `, + }, { reporter: 'list,json' }); + expect(passed).toBe(1); + expect(failed).toBe(0); + expect(skipped).toBe(0); + expect(exitCode).toBe(0); +}); + +test('should work with default export', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'file.spec.ts': ` + import t from ${JSON.stringify(path.join(__dirname, 'playwright-test-internal'))}; + t('passed', () => { + t.expect(1 + 1).toBe(2); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); +}); diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts new file mode 100644 index 0000000000..0153770fc8 --- /dev/null +++ b/tests/playwright-test/config.spec.ts @@ -0,0 +1,401 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { test, expect } from './playwright-test-fixtures'; + +test('should be able to define config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { timeout: 12345 }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}, testInfo) => { + expect(testInfo.timeout).toBe(12345); + }); + ` + }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should prioritize project timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { timeout: 500, projects: [{ timeout: 10000}, {}] }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}, testInfo) => { + await new Promise(f => setTimeout(f, 1500)); + }); + ` + }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Timeout of 500ms exceeded.'); +}); + +test('should prioritize command line timeout over project timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [{ timeout: 10000}] }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}, testInfo) => { + await new Promise(f => setTimeout(f, 1500)); + }); + ` + }, { timeout: '500' }); + + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Timeout of 500ms exceeded.'); +}); + +test('should read config from --config, resolve relative testDir', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'my.config.ts': ` + import * as path from 'path'; + module.exports = { + testDir: 'dir', + }; + `, + 'a.test.ts': ` + const { test } = folio; + test('ignored', async ({}) => { + }); + `, + 'dir/b.test.ts': ` + const { test } = folio; + test('run', async ({}) => { + }); + `, + }, { config: 'my.config.ts' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.report.suites.length).toBe(1); + expect(result.report.suites[0].file).toBe('b.test.ts'); +}); + +test('should default testDir to the config file', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'dir/my.config.ts': ` + module.exports = {}; + `, + 'a.test.ts': ` + const { test } = folio; + test('ignored', async ({}) => { + }); + `, + 'dir/b.test.ts': ` + const { test } = folio; + test('run', async ({}) => { + }); + `, + }, { config: path.join('dir', 'my.config.ts') }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.report.suites.length).toBe(1); + expect(result.report.suites[0].file).toBe('b.test.ts'); +}); + +test('should be able to set reporters', async ({ runInlineTest }, testInfo) => { + const reportFile = testInfo.outputPath('my-report.json'); + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [ + ['json', { outputFile: ${JSON.stringify(reportFile)} }], + ['list'], + ] + }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async () => { + }); + ` + }, { reporter: '' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const report = JSON.parse(fs.readFileSync(reportFile).toString()); + expect(report.suites[0].file).toBe('a.test.ts'); +}); + +test('should support different testDirs', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { projects: [ + { testDir: __dirname }, + { testDir: 'dir' }, + ] }; + `, + 'a.test.ts': ` + const { test } = folio; + test('runs once', async ({}) => { + }); + `, + 'dir/b.test.ts': ` + const { test } = folio; + test('runs twice', async ({}) => { + }); + `, + }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + + expect(result.report.suites[0].specs[0].tests.length).toBe(1); + expect(result.report.suites[0].specs[0].title).toBe('runs once'); + + expect(result.report.suites[1].specs[0].tests.length).toBe(2); + expect(result.report.suites[1].specs[0].title).toBe('runs twice'); +}); + +test('should allow export default form the config file', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { timeout: 1000 }; + `, + 'a.test.ts': ` + const { test } = folio; + test('fails', async ({}, testInfo) => { + await new Promise(f => setTimeout(f, 2000)); + }); + ` + }); + + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Timeout of 1000ms exceeded.'); +}); + +test('should allow root testDir and use it for relative paths', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'config/config.ts': ` + import * as path from 'path'; + module.exports = { + testDir: path.join(__dirname, '..'), + projects: [{ testDir: path.join(__dirname, '..', 'dir') }] + }; + `, + 'a.test.ts': ` + const { test } = folio; + test('fails', async ({}, testInfo) => { + expect(1 + 1).toBe(3); + }); + `, + 'dir/a.test.ts': ` + const { test } = folio; + test('fails', async ({}, testInfo) => { + expect(1 + 1).toBe(3); + }); + `, + }, { config: path.join('config', 'config.ts') }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.skipped).toBe(0); + expect(result.failed).toBe(1); + expect(result.output).toContain(`1) ${path.join('dir', 'a.test.ts')}:6:7 › fails`); +}); + +test('should throw when test() is called in config file', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + folio.test('hey', () => {}); + module.exports = {}; + `, + 'a.test.ts': ` + const { test } = folio; + test('test', async ({}) => { + }); + `, + }); + expect(result.output).toContain('test() can only be called in a test file'); +}); + +test('should filter by project, case-insensitive', async ({ runInlineTest }) => { + const { passed, failed, output, skipped } = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [ + { name: 'suite1' }, + { name: 'suite2' }, + ] }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}, testInfo) => { + console.log(testInfo.project.name); + }); + ` + }, { project: 'SUite2' }); + expect(passed).toBe(1); + expect(failed).toBe(0); + expect(skipped).toBe(0); + expect(output).toContain('suite2'); + expect(output).not.toContain('suite1'); +}); + +test('should print nice error when project is unknown', async ({ runInlineTest }) => { + const { output, exitCode } = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [ + { name: 'suite1' }, + { name: 'suite2' }, + ] }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}, testInfo) => { + console.log(testInfo.project.name); + }); + ` + }, { project: 'suite3' }); + expect(exitCode).toBe(1); + expect(output).toContain('Project "suite3" not found. Available named projects: "suite1", "suite2"'); +}); + +test('should work without config file', async ({ runInlineTest }) => { + const { exitCode, passed, failed, skipped } = await runInlineTest({ + 'playwright.config.ts': ` + throw new Error('This file should not be required'); + `, + 'dir/a.test.ts': ` + const { test } = folio; + test('pass', async ({}) => { + test.expect(1 + 1).toBe(2); + }); + ` + }, { config: 'dir' }); + expect(exitCode).toBe(0); + expect(passed).toBe(1); + expect(failed).toBe(0); + expect(skipped).toBe(0); +}); + +test('should inerhit use options in projects', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { foo: 'config' }, + projects: [{ + use: { bar: 'project' }, + }] + }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({ foo, bar }, testInfo) => { + test.expect(foo).toBe('config'); + test.expect(bar).toBe('project'); + }); + ` + }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should work with undefined values and base', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + updateSnapshots: undefined, + }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}, testInfo) => { + expect(testInfo.config.updateSnapshots).toBe('none'); + }); + ` + }, {}, { CI: '1' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should work with custom reporter', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + class Reporter { + constructor(options) { + this.options = options; + } + onBegin() { + console.log('\\n%%reporter-begin%%' + this.options.begin); + } + onTestBegin() { + console.log('\\n%%reporter-testbegin%%'); + } + onStdOut() { + console.log('\\n%%reporter-stdout%%'); + } + onStdErr() { + console.log('\\n%%reporter-stderr%%'); + } + onTestEnd() { + console.log('\\n%%reporter-testend%%'); + } + onTimeout() { + console.log('\\n%%reporter-timeout%%'); + } + onError() { + console.log('\\n%%reporter-error%%'); + } + onEnd() { + console.log('\\n%%reporter-end%%' + this.options.end); + } + } + export default Reporter; + `, + 'playwright.config.ts': ` + module.exports = { + reporter: [ + [ './reporter.ts', { begin: 'begin', end: 'end' } ] + ] + }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', async ({}) => { + console.log('log'); + console.error('error'); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ + '%%reporter-begin%%begin', + '%%reporter-testbegin%%', + '%%reporter-stdout%%', + '%%reporter-stderr%%', + '%%reporter-testend%%', + '%%reporter-end%%end', + ]); +}); diff --git a/tests/playwright-test/dot-reporter.spec.ts b/tests/playwright-test/dot-reporter.spec.ts new file mode 100644 index 0000000000..b59cb5966e --- /dev/null +++ b/tests/playwright-test/dot-reporter.spec.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import colors from 'colors/safe'; +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +test('render expected', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(1); + }); + `, + }); + expect(result.output).toContain(colors.green('·')); + expect(result.exitCode).toBe(0); +}); + +test('render unexpected', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(0); + }); + `, + }); + expect(result.output).toContain(colors.red('F')); + expect(result.exitCode).toBe(1); +}); + +test('render unexpected after retry', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(0); + }); + `, + }, { retries: 3 }); + const text = stripAscii(result.output); + expect(text).toContain('×××F'); + expect(result.output).toContain(colors.red('F')); + expect(result.exitCode).toBe(1); +}); + +test('render flaky', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}, testInfo) => { + expect(testInfo.retry).toBe(3); + }); + `, + }, { retries: 3 }); + const text = stripAscii(result.output); + expect(text).toContain('×××±'); + expect(result.output).toContain(colors.yellow('±')); + expect(text).toContain('1 flaky'); + expect(text).not.toContain('Retry #1'); + expect(result.exitCode).toBe(0); +}); + +test('should work from config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { reporter: 'dot' }; + `, + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(1); + }); + `, + }); + expect(result.output).toContain(colors.green('·')); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/exit-code.spec.ts b/tests/playwright-test/exit-code.spec.ts new file mode 100644 index 0000000000..92640f4cea --- /dev/null +++ b/tests/playwright-test/exit-code.spec.ts @@ -0,0 +1,163 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +function monotonicTime(): number { + const [seconds, nanoseconds] = process.hrtime(); + return seconds * 1000 + (nanoseconds / 1000000 | 0); +} + +test('should collect stdio', async ({ runInlineTest }) => { + const { exitCode, report } = await runInlineTest({ + 'stdio.spec.js': ` + const { test } = folio; + test('stdio', () => { + process.stdout.write('stdout text'); + process.stdout.write(Buffer.from('stdout buffer')); + process.stderr.write('stderr text'); + process.stderr.write(Buffer.from('stderr buffer')); + }); + ` + }); + expect(exitCode).toBe(0); + const testResult = report.suites[0].specs[0].tests[0].results[0]; + const { stdout, stderr } = testResult; + expect(stdout).toEqual([{ text: 'stdout text' }, { buffer: Buffer.from('stdout buffer').toString('base64') }]); + expect(stderr).toEqual([{ text: 'stderr text' }, { buffer: Buffer.from('stderr buffer').toString('base64') }]); +}); + +test('should work with not defined errors', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'is-not-defined-error.spec.ts': ` + foo(); + ` + }); + expect(stripAscii(result.output)).toContain('foo is not defined'); + expect(result.exitCode).toBe(1); +}); + +test('should work with typescript', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'global-foo.js': ` + global.foo = true; + module.exports = { + abc: 123 + }; + `, + 'typescript.spec.ts': ` + import './global-foo'; + + const { test } = folio; + test('should find global foo', () => { + expect(global['foo']).toBe(true); + }); + + test('should work with type annotations', () => { + const x: number = 5; + expect(x).toBe(5); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should repeat each', async ({ runInlineTest }) => { + const { exitCode, report, passed } = await runInlineTest({ + 'one-success.spec.js': ` + const { test } = folio; + test('succeeds', () => { + expect(1 + 1).toBe(2); + }); + ` + }, { 'repeat-each': 3 }); + expect(exitCode).toBe(0); + expect(passed).toBe(3); + expect(report.suites.length).toBe(1); + expect(report.suites[0].specs.length).toBe(1); + expect(report.suites[0].specs[0].tests.length).toBe(3); +}); + +test('should allow flaky', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('flake', async ({}, testInfo) => { + expect(testInfo.retry).toBe(1); + }); + `, + }, { retries: 1 }); + expect(result.exitCode).toBe(0); + expect(result.flaky).toBe(1); +}); + +test('should fail on unexpected pass', async ({ runInlineTest }) => { + const { exitCode, failed, output } = await runInlineTest({ + 'unexpected-pass.spec.js': ` + const { test } = folio; + test('succeeds', () => { + test.fail(); + expect(1 + 1).toBe(2); + }); + ` + }); + expect(exitCode).toBe(1); + expect(failed).toBe(1); + expect(output).toContain('passed unexpectedly'); +}); + +test('should respect global timeout', async ({ runInlineTest }) => { + const now = monotonicTime(); + const { exitCode, output } = await runInlineTest({ + 'one-timeout.spec.js': ` + const { test } = folio; + test('timeout', async () => { + await new Promise(f => setTimeout(f, 10000)); + }); + ` + }, { 'timeout': 100000, 'global-timeout': 3000 }); + expect(exitCode).toBe(1); + expect(output).toContain('Timed out waiting 3s for the entire test run'); + expect(monotonicTime() - now).toBeGreaterThan(2900); +}); + +test('should exit with code 1 if the specified folder does not exist', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { testDir: '111111111111.js' }; + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`111111111111.js does not exist`); +}); + +test('should exit with code 1 if passed a file name', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { testDir: 'test.spec.js' }; + `, + 'test.spec.js': ` + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`test.spec.js is not a directory`); +}); + +test('should exit with code 1 when config is not found', async ({runInlineTest}) => { + const result = await runInlineTest({'my.config.js': ''}, { 'config': 'foo.config.js' }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`foo.config.js does not exist`); +}); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts new file mode 100644 index 0000000000..8cce68a696 --- /dev/null +++ b/tests/playwright-test/expect.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should be able to extend the expect matchers with test.extend in the folio config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + folio.expect.extend({ + toBeWithinRange(received, floor, ceiling) { + const pass = received >= floor && received <= ceiling; + if (pass) { + return { + message: () => + 'passed', + pass: true, + }; + } else { + return { + message: () => 'failed', + pass: false, + }; + } + }, + }); + export const test = folio.test; + `, + 'expect-test.spec.ts': ` + import { test } from './helper'; + test('numeric ranges', () => { + test.expect(100).toBeWithinRange(90, 110); + test.expect(101).not.toBeWithinRange(0, 100); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should work with default expect prototype functions', async ({runTSC}) => { + const result = await runTSC({ + 'a.spec.ts': ` + const { test } = folio; + const expected = [1, 2, 3, 4, 5, 6]; + test.expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]).toEqual( + expect.arrayContaining(expected), + ); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should work with default expect matchers', async ({runTSC}) => { + const result = await runTSC({ + 'a.spec.ts': ` + const { test } = folio; + test.expect(42).toBe(42); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should work with jest-community/jest-extended', async ({runTSC}) => { + const result = await runTSC({ + 'global.d.ts': ` + // Extracted example from their typings. + // Reference: https://github.com/jest-community/jest-extended/blob/master/types/index.d.ts + declare namespace jest { + interface Matchers { + toBeEmpty(): R; + } + } + `, + 'a.spec.ts': ` + const { test } = folio; + test.expect('').toBeEmpty(); + test.expect('hello').not.toBeEmpty(); + test.expect([]).toBeEmpty(); + test.expect(['hello']).not.toBeEmpty(); + test.expect({}).toBeEmpty(); + test.expect({ hello: 'world' }).not.toBeEmpty(); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should work with custom folio namespace', async ({runTSC}) => { + const result = await runTSC({ + 'global.d.ts': ` + // Extracted example from their typings. + // Reference: https://github.com/jest-community/jest-extended/blob/master/types/index.d.ts + declare namespace folio { + interface Matchers { + toBeEmpty(): R; + } + } + `, + 'a.spec.ts': ` + const { test } = folio; + test.expect.extend({ + toBeWithinRange() { }, + }); + + test.expect('').toBeEmpty(); + test.expect('hello').not.toBeEmpty(); + test.expect([]).toBeEmpty(); + test.expect(['hello']).not.toBeEmpty(); + test.expect({}).toBeEmpty(); + test.expect({ hello: 'world' }).not.toBeEmpty(); + ` + }); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/fixture-errors.spec.ts b/tests/playwright-test/fixture-errors.spec.ts new file mode 100644 index 0000000000..66fe91fc46 --- /dev/null +++ b/tests/playwright-test/fixture-errors.spec.ts @@ -0,0 +1,392 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +test('should handle fixture timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + timeout: async ({}, runTest) => { + await runTest(); + await new Promise(f => setTimeout(f, 100000)); + } + }); + + test('fixture timeout', async ({timeout}) => { + expect(1).toBe(1); + }); + + test('failing fixture timeout', async ({timeout}) => { + expect(1).toBe(2); + }); + ` + }, { timeout: 500 }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('Timeout of 500ms'); + expect(result.failed).toBe(2); +}); + +test('should handle worker fixture timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + timeout: [async ({}, runTest) => { + await runTest(); + await new Promise(f => setTimeout(f, 100000)); + }, { scope: 'worker' }] + }); + + test('fails', async ({timeout}) => { + }); + ` + }, { timeout: 500 }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('Timeout of 500ms'); +}); + +test('should handle worker fixture error', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + failure: [async ({}, runTest) => { + throw new Error('Worker failed'); + }, { scope: 'worker' }] + }); + + test('fails', async ({failure}) => { + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Worker failed'); +}); + +test('should handle worker tear down fixture error', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + failure: [async ({}, runTest) => { + await runTest(); + throw new Error('Worker failed'); + }, { scope: 'worker' }] + }); + + test('pass', async ({failure}) => { + expect(true).toBe(true); + }); + ` + }); + expect(result.report.errors[0].message).toContain('Worker failed'); + expect(result.exitCode).toBe(1); +}); + +test('should throw when using non-defined super worker fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + foo: [async ({ foo }, runTest) => { + await runTest(); + }, { scope: 'worker' }] + }); + + test('works', async ({foo}) => {}); + ` + }); + expect(result.output).toContain(`Fixture "foo" references itself, but does not have a base implementation.`); + expect(result.output).toContain('a.spec.ts:5:31'); + expect(result.exitCode).toBe(1); +}); + +test('should throw when defining test fixture with the same name as a worker fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'e.spec.ts': ` + const test1 = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'worker' }] + }); + const test2 = test1.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'test' }] + }); + + test2('works', async ({foo}) => {}); + `, + }); + expect(result.output).toContain(`Fixture "foo" has already been registered as a { scope: 'worker' } fixture.`); + expect(result.output).toContain(`e.spec.ts:10`); + expect(result.output).toContain(`e.spec.ts:5`); + expect(result.exitCode).toBe(1); +}); + +test('should throw when defining worker fixture with the same name as a test fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'e.spec.ts': ` + const test1 = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'test' }] + }); + const test2 = test1.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'worker' }] + }); + + test2('works', async ({foo}) => {}); + `, + }); + expect(result.output).toContain(`Fixture "foo" has already been registered as a { scope: 'test' } fixture.`); + expect(result.output).toContain(`e.spec.ts:10`); + expect(result.output).toContain(`e.spec.ts:5`); + expect(result.exitCode).toBe(1); +}); + +test('should throw when worker fixture depends on a test fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'f.spec.ts': ` + const test = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'test' }], + + bar: [async ({ foo }, runTest) => { + await runTest(); + }, { scope: 'worker' }], + }); + + test('works', async ({bar}) => {}); + `, + }); + expect(result.output).toContain('Worker fixture "bar" cannot depend on a test fixture "foo".'); + expect(result.output).toContain(`f.spec.ts:5`); + expect(result.exitCode).toBe(1); +}); + +test('should throw when beforeAll hook depends on a test fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'f.spec.ts': ` + const test = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'test' }], + }); + + test.beforeAll(async ({ foo }) => {}); + test('works', async ({ foo }) => {}); + `, + }); + expect(result.output).toContain('beforeAll hook cannot depend on a test fixture "foo".'); + expect(result.output).toContain(`f.spec.ts:11:12`); + expect(result.output).toContain(`f.spec.ts:5:31`); + expect(result.exitCode).toBe(1); +}); + +test('should throw when afterAll hook depends on a test fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'f.spec.ts': ` + const test = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'test' }], + }); + + test.afterAll(async ({ foo }) => {}); + test('works', async ({ foo }) => {}); + `, + }); + expect(result.output).toContain('afterAll hook cannot depend on a test fixture "foo".'); + expect(result.output).toContain(`f.spec.ts:11:12`); + expect(result.output).toContain(`f.spec.ts:5:31`); + expect(result.exitCode).toBe(1); +}); + +test('should define the same fixture in two files', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test1 = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'worker' }] + }); + + test1('works', async ({foo}) => {}); + `, + 'b.spec.ts': ` + const test2 = folio.test.extend({ + foo: [async ({}, runTest) => { + await runTest(); + }, { scope: 'worker' }] + }); + + test2('works', async ({foo}) => {}); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + +test('should detect fixture dependency cycle', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'x.spec.ts': ` + const test = folio.test.extend({ + good1: async ({}, run) => run(), + foo: async ({bar}, run) => run(), + bar: async ({baz}, run) => run(), + good2: async ({good1}, run) => run(), + baz: async ({qux}, run) => run(), + qux: async ({foo}, run) => run(), + }); + + test('works', async ({foo}) => {}); + `, + }); + expect(result.output).toContain('Fixtures "bar" -> "baz" -> "qux" -> "foo" -> "bar" form a dependency cycle.'); + expect(result.output).toContain('"foo" defined at'); + expect(result.output).toContain('"bar" defined at'); + expect(result.output).toContain('"baz" defined at'); + expect(result.output).toContain('"qux" defined at'); + expect(result.output).toContain('x.spec.ts:5:31'); + expect(result.exitCode).toBe(1); +}); + +test('should not reuse fixtures from one file in another one', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ foo: ({}, run) => run() }); + test('test1', async ({}) => {}); + `, + 'b.spec.ts': ` + const test = folio.test; + test('test1', async ({}) => {}); + test('test2', async ({foo}) => {}); + `, + }); + expect(result.output).toContain('Test has unknown parameter "foo".'); + expect(result.output).toContain('b.spec.ts:7:7'); +}); + +test('should throw for cycle in two overrides', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const test1 = folio.test.extend({ + foo: async ({}, run) => await run('foo'), + bar: async ({}, run) => await run('bar'), + }); + const test2 = test1.extend({ + foo: async ({ foo, bar }, run) => await run(foo + '-' + bar), + }); + const test3 = test2.extend({ + bar: async ({ bar, foo }, run) => await run(bar + '-' + foo), + }); + + test3('test', async ({foo, bar}) => { + expect(1).toBe(1); + }); + `, + }); + expect(result.output).toContain('Fixtures "bar" -> "foo" -> "bar" form a dependency cycle.'); + expect(result.output).toContain('a.test.js:9'); + expect(result.output).toContain('a.test.js:12'); +}); + +test('should throw when overridden worker fixture depends on a test fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'f.spec.ts': ` + const test1 = folio.test.extend({ + foo: async ({}, run) => await run('foo'), + bar: [ async ({}, run) => await run('bar'), { scope: 'worker' } ], + }); + const test2 = test1.extend({ + bar: async ({ foo }, run) => await run(), + }); + + test2('works', async ({bar}) => {}); + `, + }); + expect(result.output).toContain('Worker fixture "bar" cannot depend on a test fixture "foo".'); + expect(result.exitCode).toBe(1); +}); + +test('should throw for unknown fixture parameter', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'f.spec.ts': ` + const test = folio.test.extend({ + foo: async ({ bar }, run) => await run('foo'), + }); + + test('works', async ({ foo }) => {}); + `, + }); + expect(result.output).toContain('Fixture "foo" has unknown parameter "bar".'); + expect(result.output).toContain('f.spec.ts:5:31'); + expect(result.exitCode).toBe(1); +}); + +test('should throw when calling runTest twice', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'f.spec.ts': ` + const test = folio.test.extend({ + foo: async ({}, run) => { + await run(); + await run(); + } + }); + + test('works', async ({foo}) => {}); + `, + }); + expect(result.results[0].error.message).toBe('Cannot provide fixture value for the second time'); + expect(result.exitCode).toBe(1); +}); + +test('should print nice error message for problematic fixtures', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'x.spec.ts': ` + const test = folio.test.extend({ + bad: [ undefined, { get scope() { throw new Error('oh my!') } } ], + }); + test('works', async ({foo}) => {}); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('oh my!'); + expect(result.output).toContain('x.spec.ts:5:31'); +}); + +test('should exit with timeout when fixture causes an exception in the test', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + throwAfterTimeout: async ({}, use) => { + let callback; + const promise = new Promise((f, r) => callback = r); + await use(promise); + callback(new Error('BAD')); + }, + }); + test('times out and throws', async ({ throwAfterTimeout }) => { + await throwAfterTimeout; + }); + `, + }, { timeout: 500 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Timeout of 500ms exceeded'); +}); diff --git a/tests/playwright-test/fixtures.spec.ts b/tests/playwright-test/fixtures.spec.ts new file mode 100644 index 0000000000..148a568294 --- /dev/null +++ b/tests/playwright-test/fixtures.spec.ts @@ -0,0 +1,598 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should work', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(123), + }); + + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(123); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with a sync test function', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(123), + }); + + test('should use asdf', ({asdf}) => { + expect(asdf).toBe(123); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with a sync fixture function', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: ({}, use) => { + use(123); + }, + }); + + test('should use asdf', ({asdf}) => { + expect(asdf).toBe(123); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with a non-arrow function', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(123), + }); + + test('should use asdf', function ({asdf}) { + expect(asdf).toBe(123); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with a named function', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(123), + }); + + test('should use asdf', async function hello({asdf}) { + expect(asdf).toBe(123); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with renamed parameters', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(123), + }); + + test('should use asdf', function ({asdf: renamed}) { + expect(renamed).toBe(123); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with destructured object', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test({ foo: 'foo', bar: { x: 'x', y: 'y' }, baz: 'baz' }), + }); + + test('should use asdf', async ({ asdf: { foo, + bar: { x, y }, baz } }) => { + expect(foo).toBe('foo'); + expect(x).toBe('x'); + expect(y).toBe('y'); + expect(baz).toBe('baz'); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should work with destructured array', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(['foo', 'bar', { baz: 'baz' }]), + more: async ({}, test) => await test(55), + }); + + test('should use asdf', async ( + + { + asdf: [foo, bar, { baz}] + + + ,more}) => { + expect(foo).toBe('foo'); + expect(bar).toBe('bar'); + expect(baz).toBe('baz'); + expect(more).toBe(55); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('should fail if parameters are not destructured', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + asdf: async ({}, test) => await test(123), + }); + test('should pass', function () { + expect(1).toBe(1); + }); + test('should use asdf', function (abc) { + expect(abc.asdf).toBe(123); + }); + `, + }); + expect(result.output).toContain('First argument must use the object destructuring pattern: abc'); + expect(result.output).toContain('a.test.js:11:7'); + expect(result.results.length).toBe(0); +}); + +test('should fail with an unknown fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + folio.test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(123); + }); + `, + }); + expect(result.output).toContain('Test has unknown parameter "asdf".'); + expect(result.output).toContain('a.test.js:5:13'); + expect(result.results.length).toBe(0); +}); + +test('should run the fixture every time', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + let counter = 0; + const test = folio.test.extend({ + asdf: async ({}, test) => await test(counter++), + }); + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(0); + }); + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(1); + }); + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(2); + }); + `, + }); + expect(results.map(r => r.status)).toEqual(['passed', 'passed', 'passed']); +}); + +test('should only run worker fixtures once', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + let counter = 0; + const test = folio.test.extend({ + asdf: [ async ({}, test) => await test(counter++), { scope: 'worker' } ], + }); + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(0); + }); + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(0); + }); + test('should use asdf', async ({asdf}) => { + expect(asdf).toBe(0); + }); + `, + }); + expect(results.map(r => r.status)).toEqual(['passed', 'passed', 'passed']); +}); + +test('each file should get their own fixtures', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'a.test.js': ` + const test = folio.test.extend({ + worker: [ async ({}, test) => await test('worker-a'), { scope: 'worker' } ], + test: async ({}, test) => await test('test-a'), + }); + test('should use worker', async ({worker, test}) => { + expect(worker).toBe('worker-a'); + expect(test).toBe('test-a'); + }); + `, + 'b.test.js': ` + const test = folio.test.extend({ + worker: [ async ({}, test) => await test('worker-b'), { scope: 'worker' } ], + test: async ({}, test) => await test('test-b'), + }); + test('should use worker', async ({worker, test}) => { + expect(worker).toBe('worker-b'); + expect(test).toBe('test-b'); + }); + `, + 'c.test.js': ` + const test = folio.test.extend({ + worker: [ async ({}, test) => await test('worker-c'), { scope: 'worker' } ], + test: async ({}, test) => await test('test-c'), + }); + test('should use worker', async ({worker, test}) => { + expect(worker).toBe('worker-c'); + expect(test).toBe('test-c'); + }); + `, + }); + expect(results.map(r => r.status)).toEqual(['passed', 'passed', 'passed']); +}); + +test('tests should be able to share worker fixtures', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'worker.js': ` + global.counter = 0; + const test = folio.test.extend({ + worker: [ async ({}, test) => await test(global.counter++), { scope: 'worker' } ], + }); + module.exports = test; + `, + 'a.test.js': ` + const test = require('./worker.js'); + test('should use worker', async ({worker}) => { + expect(worker).toBe(0); + }); + `, + 'b.test.js': ` + const test = require('./worker.js'); + test('should use worker', async ({worker}) => { + expect(worker).toBe(0); + }); + `, + 'c.test.js': ` + const test = require('./worker.js'); + test('should use worker', async ({worker}) => { + expect(worker).toBe(0); + }); + `, + }); + expect(results.map(r => r.status)).toEqual(['passed', 'passed', 'passed']); +}); + +test('automatic fixtures should work', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + let counterTest = 0; + let counterWorker = 0; + const test = folio.test; + test.use({ + automaticTestFixture: [ async ({}, runTest) => { + ++counterTest; + await runTest(); + }, { auto: true } ], + + automaticWorkerFixture: [ async ({}, runTest) => { + ++counterWorker; + await runTest(); + }, { scope: 'worker', auto: true } ], + }); + test.beforeAll(async ({}) => { + expect(counterWorker).toBe(1); + expect(counterTest).toBe(0); + }); + test.beforeEach(async ({}) => { + expect(counterWorker).toBe(1); + expect(counterTest === 1 || counterTest === 2).toBe(true); + }); + test('test 1', async ({}) => { + expect(counterWorker).toBe(1); + expect(counterTest).toBe(1); + }); + test('test 2', async ({}) => { + expect(counterWorker).toBe(1); + expect(counterTest).toBe(2); + }); + test.afterEach(async ({}) => { + expect(counterWorker).toBe(1); + expect(counterTest === 1 || counterTest === 2).toBe(true); + }); + test.afterAll(async ({}) => { + expect(counterWorker).toBe(1); + expect(counterTest).toBe(2); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.results.map(r => r.status)).toEqual(['passed', 'passed']); +}); + +test('tests does not run non-automatic worker fixtures', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + let counter = 0; + const test = folio.test.extend({ + nonAutomaticWorkerFixture: [ async ({}, runTest) => { + ++counter; + await runTest(); + }, { scope: 'worker' }], + }); + test('test 1', async ({}) => { + expect(counter).toBe(0); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.results.map(r => r.status)).toEqual(['passed']); +}); + +test('should teardown fixtures after timeout', async ({ runInlineTest }, testInfo) => { + const file = testInfo.outputPath('log.txt'); + require('fs').writeFileSync(file, '', 'utf8'); + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = folio.test.extend({ + file: [ ${JSON.stringify(file)}, { scope: 'worker' } ], + w: [ async ({ file }, runTest) => { + await runTest('w'); + require('fs').appendFileSync(file, 'worker fixture teardown\\n', 'utf8'); + }, { scope: 'worker' } ], + t: async ({ file }, runTest) => { + await runTest('t'); + require('fs').appendFileSync(file, 'test fixture teardown\\n', 'utf8'); + }, + }); + test('test', async ({t, w}) => { + expect(t).toBe('t'); + expect(w).toBe('w'); + await new Promise(() => {}); + }); + `, + }, { timeout: 1000 }); + expect(result.results[0].status).toBe('timedOut'); + const content = require('fs').readFileSync(file, 'utf8'); + expect(content).toContain('worker fixture teardown'); + expect(content).toContain('test fixture teardown'); +}); + +test('should work with two different test objects', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const test1 = folio.test.extend({ + foo: async ({}, test) => await test(123), + }); + const test2 = folio.test.extend({ + bar: async ({}, test) => await test(456), + }); + test1('test 1', async ({foo}) => { + expect(foo).toBe(123); + }); + test2('test 2', async ({bar}) => { + expect(bar).toBe(456); + }); + `, + }); + expect(result.results.map(r => r.workerIndex).sort()).toEqual([0, 0]); + expect(result.results.map(r => r.status).sort()).toEqual(['passed', 'passed']); +}); + +test('should work with overrides calling base', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const test1 = folio.test.extend({ + dep: async ({}, test) => await test('override'), + foo: async ({}, test) => await test('base'), + bar: async ({foo}, test) => await test(foo + '-bar'), + }); + const test2 = test1.extend({ + foo: async ({ foo, dep }, test) => await test(foo + '-' + dep + '1'), + }); + const test3 = test2.extend({ + foo: async ({ foo, dep }, test) => await test(foo + '-' + dep + '2'), + }); + test3('test', async ({bar}) => { + expect(bar).toBe('base-override1-override2-bar'); + }); + `, + }); + expect(result.results[0].status).toBe('passed'); +}); + +test('should understand worker fixture params in overrides calling base', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const test1 = folio.test.extend({ + param: [ 'param', { scope: 'worker' }], + foo: async ({}, test) => await test('foo'), + bar: async ({foo}, test) => await test(foo + '-bar'), + }); + const test2 = test1.extend({ + foo: async ({ foo, param }, test) => await test(foo + '-' + param), + }); + const test3 = test2.extend({ + foo: async ({ foo }, test) => await test(foo + '-override'), + }); + test3('test', async ({ bar }) => { + console.log(bar); + }); + `, + 'playwright.config.ts': ` + module.exports = { projects: [ + { use: { param: 'p1' } }, + { use: { param: 'p2' } }, + { use: { param: 'p3' } }, + ]}; + `, + }); + const outputs = result.results.map(r => r.stdout[0].text.replace(/\s/g, '')); + expect(outputs.sort()).toEqual(['foo-p1-override-bar', 'foo-p2-override-bar', 'foo-p3-override-bar']); +}); + +test('should work with two overrides calling base', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const test1 = folio.test.extend({ + foo: async ({}, test) => await test('foo'), + bar: async ({}, test) => await test('bar'), + baz: async ({foo, bar}, test) => await test(foo + '-baz-' + bar), + }); + const test2 = test1.extend({ + foo: async ({ foo, bar }, test) => await test(foo + '-' + bar), + bar: async ({ bar }, test) => await test(bar + '-override'), + }); + test2('test', async ({baz}) => { + expect(baz).toBe('foo-bar-override-baz-bar-override'); + }); + `, + }); + expect(result.results[0].status).toBe('passed'); +}); + +test('should not create a new worker for test fixtures', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = folio; + test('base test', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + + const test2 = test.extend({ + foo: async ({}, run) => { + console.log('foo-a'); + await run(); + } + }); + test2('a test', async ({ foo }, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + `, + 'b.test.ts': ` + const { test } = folio; + const test2 = test.extend({ + foo: async ({}, run) => { + console.log('foo-b'); + await run(); + } + }); + const test3 = test2.extend({ + foo: async ({ foo }, run) => { + console.log('foo-c'); + await run(); + } + }); + test3('b test', async ({ foo }, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + `, + }, { workers: 1 }); + expect(result.output).toContain('foo-a'); + expect(result.output).toContain('foo-b'); + expect(result.output).toContain('foo-c'); + expect(result.passed).toBe(3); +}); + +test('should create a new worker for worker fixtures', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = folio; + test('base test', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(1); + }); + + const test2 = test.extend({ + foo: [async ({}, run) => { + console.log('foo-a'); + await run(); + }, { scope: 'worker' }], + }); + test2('a test', async ({ foo }, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + `, + 'b.test.ts': ` + const { test } = folio; + const test2 = test.extend({ + bar: async ({}, run) => { + console.log('bar-b'); + await run(); + }, + }); + test2('b test', async ({ bar }, testInfo) => { + expect(testInfo.workerIndex).toBe(1); + }); + `, + }, { workers: 1 }); + expect(result.output).toContain('foo-a'); + expect(result.output).toContain('bar-b'); + expect(result.passed).toBe(3); +}); + +test('should run tests in order', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = folio; + test('test1', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + console.log('\\n%%test1'); + }); + + const child = test.extend({ + foo: async ({}, run) => { + console.log('\\n%%beforeEach'); + await run(); + console.log('\\n%%afterEach'); + }, + }); + child('test2', async ({ foo }, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + console.log('\\n%%test2'); + }); + + test('test3', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + console.log('\\n%%test3'); + }); + `, + }, { workers: 1 }); + expect(result.passed).toBe(3); + expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ + '%%test1', + '%%beforeEach', + '%%test2', + '%%afterEach', + '%%test3', + ]); +}); diff --git a/tests/playwright-test/gitignore.spec.ts b/tests/playwright-test/gitignore.spec.ts new file mode 100644 index 0000000000..3d7238e066 --- /dev/null +++ b/tests/playwright-test/gitignore.spec.ts @@ -0,0 +1,111 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should respect .gitignore', async ({runInlineTest}) => { + const result = await runInlineTest({ + '.gitignore': `a.spec.js`, + 'a.spec.js': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'b.spec.js': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should respect nested .gitignore', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a/.gitignore': `a.spec.js`, + 'a/a.spec.js': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'a/b.spec.js': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should respect enclosing .gitignore', async ({runInlineTest}) => { + const result = await runInlineTest({ + '.gitignore': `a/a.spec.js`, + 'a/a.spec.js': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'a/b.spec.js': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should respect negations and comments in .gitignore', async ({runInlineTest}) => { + const result = await runInlineTest({ + '.gitignore': ` + # A comment + dir1/ + /dir2 + #a.spec.js + !dir1/foo/a.spec.js + `, + 'a.spec.js': ` + const { test } = folio; + test('pass', ({}) => console.log('\\n%%a.spec.js')); + `, + 'dir1/a.spec.js': ` + const { test } = folio; + test('pass', ({}) => console.log('\\n%%dir1/a.spec.js')); + `, + 'dir1/foo/a.spec.js': ` + const { test } = folio; + test('pass', ({}) => console.log('\\n%%dir1/foo/a.spec.js')); + `, + 'dir2/a.spec.js': ` + const { test } = folio; + test('pass', ({}) => console.log('\\n%%dir2/a.spec.js')); + `, + 'dir3/.gitignore': ` + b.*.js + `, + 'dir3/a.spec.js': ` + const { test } = folio; + test('pass', ({}) => console.log('\\n%%dir3/a.spec.js')); + `, + 'dir3/b.spec.js': ` + const { test } = folio; + test('pass', ({}) => console.log('\\n%%dir3/b.spec.js')); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ + '%%a.spec.js', + '%%dir1/foo/a.spec.js', + '%%dir3/a.spec.js', + ]); +}); diff --git a/tests/playwright-test/global-setup.spec.ts b/tests/playwright-test/global-setup.spec.ts new file mode 100644 index 0000000000..d961d8855c --- /dev/null +++ b/tests/playwright-test/global-setup.spec.ts @@ -0,0 +1,214 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('globalSetup and globalTeardown should work', async ({ runInlineTest }) => { + const { results, output } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + globalTeardown: path.join(__dirname, 'globalTeardown.ts'), + }; + `, + 'globalSetup.ts': ` + module.exports = async () => { + await new Promise(f => setTimeout(f, 100)); + global.value = 42; + process.env.FOO = String(global.value); + }; + `, + 'globalTeardown.ts': ` + module.exports = async () => { + console.log('teardown=' + global.value); + }; + `, + 'a.test.js': ` + const { test } = folio; + test('should work', async ({}, testInfo) => { + expect(process.env.FOO).toBe('42'); + }); + `, + }); + expect(results[0].status).toBe('passed'); + expect(output).toContain('teardown=42'); +}); + +test('globalTeardown runs after failures', async ({ runInlineTest }) => { + const { results, output } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + globalTeardown: path.join(__dirname, 'globalTeardown.ts'), + }; + `, + 'globalSetup.ts': ` + module.exports = async () => { + await new Promise(f => setTimeout(f, 100)); + global.value = 42; + process.env.FOO = String(global.value); + }; + `, + 'globalTeardown.ts': ` + module.exports = async () => { + console.log('teardown=' + global.value); + }; + `, + 'a.test.js': ` + const { test } = folio; + test('should work', async ({}, testInfo) => { + expect(process.env.FOO).toBe('43'); + }); + `, + }); + expect(results[0].status).toBe('failed'); + expect(output).toContain('teardown=42'); +}); + +test('globalTeardown does not run when globalSetup times out', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + globalTeardown: path.join(__dirname, 'globalTeardown.ts'), + globalTimeout: 1000, + }; + `, + 'globalSetup.ts': ` + module.exports = async () => { + await new Promise(f => setTimeout(f, 10000)); + }; + `, + 'globalTeardown.ts': ` + module.exports = async () => { + console.log('teardown='); + }; + `, + 'a.test.js': ` + const { test } = folio; + test('should not run', async ({}, testInfo) => { + }); + `, + }); + // We did not collect tests, so everything should be zero. + expect(result.skipped).toBe(0); + expect(result.passed).toBe(0); + expect(result.failed).toBe(0); + expect(result.exitCode).toBe(1); + expect(result.output).not.toContain('teardown='); +}); + +test('globalSetup should be run before requiring tests', async ({ runInlineTest }) => { + const { passed } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + }; + `, + 'globalSetup.ts': ` + module.exports = async () => { + process.env.FOO = JSON.stringify({ foo: 'bar' }); + }; + `, + 'a.test.js': ` + const { test } = folio; + let value = JSON.parse(process.env.FOO); + test('should work', async ({}) => { + expect(value).toEqual({ foo: 'bar' }); + }); + `, + }); + expect(passed).toBe(1); +}); + +test('globalSetup should work with sync function', async ({ runInlineTest }) => { + const { passed } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + }; + `, + 'globalSetup.ts': ` + module.exports = () => { + process.env.FOO = JSON.stringify({ foo: 'bar' }); + }; + `, + 'a.test.js': ` + const { test } = folio; + let value = JSON.parse(process.env.FOO); + test('should work', async ({}) => { + expect(value).toEqual({ foo: 'bar' }); + }); + `, + }); + expect(passed).toBe(1); +}); + +test('globalSetup should throw when passed non-function', async ({ runInlineTest }) => { + const { output } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + }; + `, + 'globalSetup.ts': ` + module.exports = 42; + `, + 'a.test.js': ` + const { test } = folio; + test('should work', async ({}) => { + }); + `, + }); + expect(output).toContain(`globalSetup file must export a single function.`); +}); + +test('globalSetup should work with default export and run the returned fn', async ({ runInlineTest }) => { + const { output, exitCode, passed } = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { + globalSetup: path.join(__dirname, 'globalSetup.ts'), + }; + `, + 'globalSetup.ts': ` + function setup() { + let x = 42; + console.log('\\n%%setup: ' + x); + return async () => { + await x; + console.log('\\n%%teardown: ' + x); + }; + } + export default setup; + `, + 'a.test.js': ` + const { test } = folio; + test('should work', async ({}) => { + }); + `, + }); + expect(passed).toBe(1); + expect(exitCode).toBe(0); + expect(output).toContain(`%%setup: 42`); + expect(output).toContain(`%%teardown: 42`); +}); diff --git a/tests/playwright-test/golden.spec.ts b/tests/playwright-test/golden.spec.ts new file mode 100644 index 0000000000..b55c0f78eb --- /dev/null +++ b/tests/playwright-test/golden.spec.ts @@ -0,0 +1,256 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import colors from 'colors/safe'; +import * as fs from 'fs'; +import * as path from 'path'; +import { test, expect } from './playwright-test-fixtures'; + +test('should support golden', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.txt': `Hello world`, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world').toMatchSnapshot('snapshot.txt'); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should fail on wrong golden', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.txt': `Line1 +Line2 +Line3 +Hello world line1 +Line5 +Line6 +Line7`, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + const data = []; + data.push('Line1'); + data.push('Line22'); + data.push('Line3'); + data.push('Hi world line2'); + data.push('Line5'); + data.push('Line6'); + data.push('Line7'); + expect(data.join('\\n')).toMatchSnapshot('snapshot.txt'); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('Line1'); + expect(result.output).toContain('Line2' + colors.green('2')); + expect(result.output).toContain('line' + colors.strikethrough(colors.red('1')) + colors.green('2')); + expect(result.output).toContain('Line3'); + expect(result.output).toContain('Line5'); + expect(result.output).toContain('Line7'); +}); + +test('should write missing expectations locally', async ({runInlineTest}, testInfo) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world').toMatchSnapshot('snapshot.txt'); + }); + ` + }, {}, { CI: '' }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('snapshot.txt is missing in snapshots, writing actual'); + const data = fs.readFileSync(testInfo.outputPath('a.spec.js-snapshots/snapshot.txt')); + expect(data.toString()).toBe('Hello world'); +}); + +test('should not write missing expectations on CI', async ({runInlineTest}, testInfo) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world').toMatchSnapshot('snapshot.txt'); + }); + ` + }, {}, { CI: '1' }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('snapshot.txt is missing in snapshots'); + expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots/snapshot.txt'))).toBe(false); +}); + +test('should update expectations', async ({runInlineTest}, testInfo) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.txt': `Hello world`, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world updated').toMatchSnapshot('snapshot.txt'); + }); + ` + }, { 'update-snapshots': true }); + expect(result.exitCode).toBe(0); + expect(result.output).toContain('snapshot.txt does not match, writing actual.'); + const data = fs.readFileSync(testInfo.outputPath('a.spec.js-snapshots/snapshot.txt')); + expect(data.toString()).toBe('Hello world updated'); +}); + +test('should match multiple snapshots', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot1.txt': `Snapshot1`, + 'a.spec.js-snapshots/snapshot2.txt': `Snapshot2`, + 'a.spec.js-snapshots/snapshot3.txt': `Snapshot3`, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Snapshot1').toMatchSnapshot('snapshot1.txt'); + expect('Snapshot2').toMatchSnapshot('snapshot2.txt'); + expect('Snapshot3').toMatchSnapshot('snapshot3.txt'); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should match snapshots from multiple projects', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { projects: [ + { testDir: path.join(__dirname, 'p1') }, + { testDir: path.join(__dirname, 'p2') }, + ]}; + `, + 'p1/a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Snapshot1').toMatchSnapshot('snapshot.txt'); + }); + `, + 'p1/a.spec.js-snapshots/snapshot.txt': `Snapshot1`, + 'p2/a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Snapshot2').toMatchSnapshot('snapshot.txt'); + }); + `, + 'p2/a.spec.js-snapshots/snapshot.txt': `Snapshot2`, + }); + expect(result.exitCode).toBe(0); +}); + +test('should use provided name', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/provided.txt': `Hello world`, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world').toMatchSnapshot('provided.txt'); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should throw without a name', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world').toMatchSnapshot(); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('toMatchSnapshot() requires a "name" parameter'); +}); + +test('should use provided name via options', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/provided.txt': `Hello world`, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect('Hello world').toMatchSnapshot({ name: 'provided.txt' }); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should compare binary', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.dat': Buffer.from([1,2,3,4]), + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect(Buffer.from([1,2,3,4])).toMatchSnapshot('snapshot.dat'); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should compare PNG images', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.png': + Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'), + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64')).toMatchSnapshot('snapshot.png'); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should compare different PNG images', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.png': + Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'), + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png'); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('Snapshot comparison failed'); + expect(result.output).toContain('snapshot-diff.png'); +}); + +test('should respect threshold', async ({runInlineTest}) => { + const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png')); + const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png')); + const result = await runInlineTest({ + 'a.spec.js-snapshots/snapshot.png': expected, + 'a.spec.js-snapshots/snapshot2.png': expected, + 'a.spec.js': ` + const { test } = folio; + test('is a test', ({}) => { + expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 }); + expect(Buffer.from('${actual.toString('base64')}', 'base64')).not.toMatchSnapshot('snapshot.png', { threshold: 0.2 }); + expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot2.png', { threshold: 0.3 }); + expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot({ name: 'snapshot2.png', threshold: 0.3 }); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/hooks.spec.ts b/tests/playwright-test/hooks.spec.ts new file mode 100644 index 0000000000..953713c812 --- /dev/null +++ b/tests/playwright-test/hooks.spec.ts @@ -0,0 +1,192 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('hooks should work with fixtures', async ({ runInlineTest }) => { + const { results } = await runInlineTest({ + 'helper.ts': ` + global.logs = []; + export const test = folio.test.extend({ + w: [ async ({}, run) => { + global.logs.push('+w'); + await run(17); + global.logs.push('-w'); + }, { scope: 'worker' }], + + t: async ({}, run) => { + global.logs.push('+t'); + await run(42); + global.logs.push('-t'); + }, + }); + `, + 'a.test.js': ` + const { test } = require('./helper'); + test.describe('suite', () => { + test.beforeAll(async ({ w }) => { + global.logs.push('beforeAll-' + w); + }); + test.afterAll(async ({ w }) => { + global.logs.push('afterAll-' + w); + }); + + test.beforeEach(async ({t}) => { + global.logs.push('beforeEach-' + t); + }); + test.afterEach(async ({t}) => { + global.logs.push('afterEach-' + t); + }); + + test('one', async ({t}) => { + global.logs.push('test'); + expect(t).toBe(42); + }); + }); + + test('two', async ({t}) => { + expect(global.logs).toEqual([ + '+w', + 'beforeAll-17', + '+t', + 'beforeEach-42', + 'test', + 'afterEach-42', + '-t', + 'afterAll-17', + '+t', + ]); + }); + `, + }); + expect(results[0].status).toBe('passed'); +}); + +test('afterEach failure should not prevent other hooks and fixtures teardown', async ({ runInlineTest }) => { + const report = await runInlineTest({ + 'helper.ts': ` + global.logs = []; + export const test = folio.test.extend({ + foo: async ({}, run) => { + console.log('+t'); + await run(); + console.log('-t'); + } + }); + `, + 'a.test.js': ` + const { test } = require('./helper'); + test.describe('suite', () => { + test.afterEach(async () => { + console.log('afterEach1'); + }); + test.afterEach(async () => { + console.log('afterEach2'); + throw new Error('afterEach2'); + }); + test('one', async ({foo}) => { + console.log('test'); + expect(true).toBe(true); + }); + }); + `, + }); + expect(report.output).toContain('+t\ntest\nafterEach2\nafterEach1\n-t'); + expect(report.results[0].error.message).toContain('afterEach2'); +}); + +test('beforeEach failure should prevent the test, but not other hooks', async ({ runInlineTest }) => { + const report = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test.describe('suite', () => { + test.beforeEach(async ({}) => { + console.log('beforeEach1'); + }); + test.beforeEach(async ({}) => { + console.log('beforeEach2'); + throw new Error('beforeEach2'); + }); + test.afterEach(async ({}) => { + console.log('afterEach'); + }); + test('one', async ({}) => { + console.log('test'); + }); + }); + `, + }); + expect(report.output).toContain('beforeEach1\nbeforeEach2\nafterEach'); + expect(report.results[0].error.message).toContain('beforeEach2'); +}); + +test('beforeAll should be run once', async ({ runInlineTest }) => { + const report = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test.describe('suite1', () => { + let counter = 0; + test.beforeAll(async () => { + console.log('beforeAll1-' + (++counter)); + }); + test.describe('suite2', () => { + test.beforeAll(async () => { + console.log('beforeAll2'); + }); + test('one', async ({}) => { + console.log('test'); + }); + }); + }); + `, + }); + expect(report.output).toContain('beforeAll1-1\nbeforeAll2\ntest'); +}); + +test('beforeEach should be able to skip a test', async ({ runInlineTest }) => { + const { passed, skipped, exitCode } = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test.beforeEach(async ({}, testInfo) => { + testInfo.skip(testInfo.title === 'test2'); + }); + test('test1', async () => {}); + test('test2', async () => {}); + `, + }); + expect(exitCode).toBe(0); + expect(passed).toBe(1); + expect(skipped).toBe(1); +}); + +test('beforeAll from a helper file should throw', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'my-test.ts': ` + export const test = folio.test; + test.beforeAll(() => {}); + `, + 'playwright.config.ts': ` + import { test } from './my-test'; + `, + 'a.test.ts': ` + import { test } from './my-test'; + test('should work', async () => { + }); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('beforeAll hook can only be called in a test file'); +}); diff --git a/tests/playwright-test/json-reporter.spec.ts b/tests/playwright-test/json-reporter.spec.ts new file mode 100644 index 0000000000..9fc80e909e --- /dev/null +++ b/tests/playwright-test/json-reporter.spec.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { test, expect } from './playwright-test-fixtures'; + +test('should support spec.ok', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('math works!', async ({}) => { + expect(1 + 1).toBe(2); + }); + test('math fails!', async ({}) => { + expect(1 + 1).toBe(3); + }); + ` + }, { }); + expect(result.exitCode).toBe(1); + expect(result.report.suites[0].specs[0].ok).toBe(true); + expect(result.report.suites[0].specs[1].ok).toBe(false); +}); + +test('should report projects', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + retries: 2, + projects: [ + { + timeout: 5000, + name: 'p1', + metadata: { foo: 'bar' }, + }, + { + timeout: 8000, + name: 'p2', + metadata: { bar: 42 }, + } + ] + }; + `, + 'a.test.js': ` + const { test } = folio; + test('math works!', async ({}) => { + expect(1 + 1).toBe(2); + }); + ` + }, { }); + expect(result.exitCode).toBe(0); + const projects = result.report.config.projects; + const testDir = testInfo.outputDir.split(path.sep).join(path.posix.sep); + + expect(projects[0].name).toBe('p1'); + expect(projects[0].retries).toBe(2); + expect(projects[0].timeout).toBe(5000); + expect(projects[0].metadata).toEqual({ foo: 'bar' }); + expect(projects[0].testDir).toBe(testDir); + + expect(projects[1].name).toBe('p2'); + expect(projects[1].retries).toBe(2); + expect(projects[1].timeout).toBe(8000); + expect(projects[1].metadata).toEqual({ bar: 42 }); + expect(projects[1].testDir).toBe(testDir); + + expect(result.report.suites[0].specs[0].tests[0].projectName).toBe('p1'); + expect(result.report.suites[0].specs[0].tests[1].projectName).toBe('p2'); +}); diff --git a/tests/playwright-test/junit-reporter.spec.ts b/tests/playwright-test/junit-reporter.spec.ts new file mode 100644 index 0000000000..8c3419b903 --- /dev/null +++ b/tests/playwright-test/junit-reporter.spec.ts @@ -0,0 +1,193 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import xml2js from 'xml2js'; +import { test, expect } from './playwright-test-fixtures'; + +test('should render expected', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { test } = folio; + test('two', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'junit' }); + const xml = parseXML(result.output); + expect(xml['testsuites']['$']['tests']).toBe('2'); + expect(xml['testsuites']['$']['failures']).toBe('0'); + expect(xml['testsuites']['testsuite'].length).toBe(2); + expect(xml['testsuites']['testsuite'][0]['$']['name']).toBe('a.test.js'); + expect(xml['testsuites']['testsuite'][0]['$']['tests']).toBe('1'); + expect(xml['testsuites']['testsuite'][0]['$']['failures']).toBe('0'); + expect(xml['testsuites']['testsuite'][0]['$']['skipped']).toBe('0'); + expect(xml['testsuites']['testsuite'][1]['$']['name']).toBe('b.test.js'); + expect(result.exitCode).toBe(0); +}); + +test('should render unexpected', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(0); + }); + `, + }, { reporter: 'junit' }); + const xml = parseXML(result.output); + expect(xml['testsuites']['$']['tests']).toBe('1'); + expect(xml['testsuites']['$']['failures']).toBe('1'); + const failure = xml['testsuites']['testsuite'][0]['testcase'][0]['failure'][0]; + expect(failure['$']['message']).toContain('a.test.js'); + expect(failure['$']['message']).toContain('one'); + expect(failure['$']['type']).toBe('FAILURE'); + expect(failure['_']).toContain('expect(1).toBe(0)'); + expect(result.exitCode).toBe(1); +}); + +test('should render unexpected after retry', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(0); + }); + `, + }, { retries: 3, reporter: 'junit' }); + expect(result.output).toContain(`tests="1"`); + expect(result.output).toContain(`failures="1"`); + expect(result.output).toContain(` { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}, testInfo) => { + expect(testInfo.retry).toBe(3); + }); + `, + }, { retries: 3, reporter: 'junit' }); + expect(result.output).not.toContain('Retry #1'); + expect(result.exitCode).toBe(0); +}); + +test('should render stdout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import colors from 'colors/safe'; + const { test } = folio; + test('one', async ({}) => { + console.log(colors.yellow('Hello world')); + test.expect("abc").toBe('abcd'); + }); + `, + }, { reporter: 'junit' }); + const xml = parseXML(result.output); + const suite = xml['testsuites']['testsuite'][0]; + expect(suite['system-out'].length).toBe(1); + expect(suite['system-out'][0]).toContain('Hello world'); + expect(suite['system-out'][0]).not.toContain('u00'); + expect(suite['testcase'][0]['failure'][0]['_']).toContain(`> 9 | test.expect("abc").toBe('abcd');`); + expect(result.exitCode).toBe(1); +}); + +test('should render stdout without ansi escapes', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [ ['junit', { stripANSIControlSequences: true }] ], + }; + `, + 'a.test.ts': ` + import colors from 'colors/safe'; + const { test } = folio; + test('one', async ({}) => { + console.log(colors.yellow('Hello world')); + }); + `, + }, { reporter: '' }); + const xml = parseXML(result.output); + const suite = xml['testsuites']['testsuite'][0]; + expect(suite['system-out'].length).toBe(1); + expect(suite['system-out'][0].trim()).toBe('Hello world'); + expect(result.exitCode).toBe(0); +}); + +test('should render skipped', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async () => { + console.log('Hello world'); + }); + test('two', async () => { + test.skip(); + console.log('Hello world'); + }); + `, + }, { retries: 3, reporter: 'junit' }); + const xml = parseXML(result.output); + expect(xml['testsuites']['testsuite'][0]['$']['tests']).toBe('2'); + expect(xml['testsuites']['testsuite'][0]['$']['failures']).toBe('0'); + expect(xml['testsuites']['testsuite'][0]['$']['skipped']).toBe('1'); + expect(result.exitCode).toBe(0); +}); + +test('should render projects', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [ { name: 'project1' }, { name: 'project2' } ] }; + `, + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'junit' }); + const xml = parseXML(result.output); + expect(xml['testsuites']['$']['tests']).toBe('2'); + expect(xml['testsuites']['$']['failures']).toBe('0'); + expect(xml['testsuites']['testsuite'].length).toBe(1); + expect(xml['testsuites']['testsuite'][0]['$']['name']).toBe('a.test.js'); + expect(xml['testsuites']['testsuite'][0]['$']['tests']).toBe('2'); + expect(xml['testsuites']['testsuite'][0]['$']['failures']).toBe('0'); + expect(xml['testsuites']['testsuite'][0]['$']['skipped']).toBe('0'); + expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['name']).toBe('one'); + expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toContain('[project1] one'); + expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toContain('a.test.js:6:7'); + expect(xml['testsuites']['testsuite'][0]['testcase'][1]['$']['name']).toBe('one'); + expect(xml['testsuites']['testsuite'][0]['testcase'][1]['$']['classname']).toContain('[project2] one'); + expect(xml['testsuites']['testsuite'][0]['testcase'][1]['$']['classname']).toContain('a.test.js:6:7'); + expect(result.exitCode).toBe(0); +}); + +function parseXML(xml: string): any { + let result: any; + xml2js.parseString(xml, (err, r) => result = r); + return result; +} diff --git a/tests/playwright-test/line-reporter.spec.ts b/tests/playwright-test/line-reporter.spec.ts new file mode 100644 index 0000000000..ab3822c475 --- /dev/null +++ b/tests/playwright-test/line-reporter.spec.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +test('render unexpected after retry', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}) => { + expect(1).toBe(0); + }); + `, + }, { retries: 3, reporter: 'line' }); + const text = stripAscii(result.output); + expect(text).toContain('1 failed'); + expect(text).toContain('1) a.test'); + expect(text).not.toContain('2) a.test'); + expect(text).toContain('Retry #1'); + expect(text).toContain('Retry #2'); + expect(text).toContain('Retry #3'); + expect(result.exitCode).toBe(1); +}); + +test('render flaky', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('one', async ({}, testInfo) => { + expect(testInfo.retry).toBe(3); + }); + `, + }, { retries: 3, reporter: 'line' }); + const text = stripAscii(result.output); + expect(text).toContain('1 flaky'); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/list-mode.spec.ts b/tests/playwright-test/list-mode.spec.ts new file mode 100644 index 0000000000..20f0ec3562 --- /dev/null +++ b/tests/playwright-test/list-mode.spec.ts @@ -0,0 +1,34 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import { test, expect } from './playwright-test-fixtures'; + +test('should have relative always-posix paths', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('math works!', async ({}) => { + expect(1 + 1).toBe(2); + }); + ` + }, { 'list': true }); + expect(result.exitCode).toBe(0); + expect(result.report.config.rootDir.indexOf(path.win32.sep)).toBe(-1); + expect(result.report.suites[0].specs[0].file).toBe('a.test.js'); + expect(result.report.suites[0].specs[0].line).toBe(6); + expect(result.report.suites[0].specs[0].column).toBe(7); +}); diff --git a/tests/playwright-test/list-reporter.spec.ts b/tests/playwright-test/list-reporter.spec.ts new file mode 100644 index 0000000000..b164ea42c1 --- /dev/null +++ b/tests/playwright-test/list-reporter.spec.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +test('render each test with project name', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [ + { name: 'foo' }, + { name: 'bar' }, + ] }; + `, + 'a.test.ts': ` + const { test } = folio; + test('fails', async ({}) => { + expect(1).toBe(0); + }); + test('passes', async ({}) => { + expect(0).toBe(0); + }); + `, + }, { reporter: 'list' }); + const text = stripAscii(result.output); + expect(text).toContain('a.test.ts:6:7 › [foo] fails'); + expect(text).toContain('a.test.ts:6:7 › [bar] fails'); + expect(text).toContain('a.test.ts:9:7 › [foo] passes'); + expect(text).toContain('a.test.ts:9:7 › [bar] passes'); + expect(result.exitCode).toBe(1); +}); diff --git a/tests/playwright-test/match-grep.spec.ts b/tests/playwright-test/match-grep.spec.ts new file mode 100644 index 0000000000..033115fa61 --- /dev/null +++ b/tests/playwright-test/match-grep.spec.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +const files = { + 'match-grep/b.test.ts': ` + const { test } = folio; + test('test AA', () => { + expect(1 + 1).toBe(2); + }); + + test('test BB', () => { + expect(1 + 1).toBe(2); + }); + + test('test CC', () => { + expect(1 + 1).toBe(2); + }); + `, + 'match-grep/fdir/c.test.ts': ` + const { test } = folio; + test('test AA', () => { + expect(1 + 1).toBe(2); + }); + + test('test BB', () => { + expect(1 + 1).toBe(2); + }); + + test('test CC', () => { + expect(1 + 1).toBe(2); + }); + `, + 'match-grep/adir/a.test.ts': ` + const { test } = folio; + test('test AA', () => { + expect(1 + 1).toBe(2); + }); + + test('test BB', () => { + expect(1 + 1).toBe(2); + }); + + test('test CC', () => { + expect(1 + 1).toBe(2); + }); + `, +}; + +test('should grep test name', async ({ runInlineTest }) => { + const result = await runInlineTest(files, { 'grep': 'test [A-B]' }); + expect(result.passed).toBe(6); + expect(result.exitCode).toBe(0); +}); + +test('should grep test name with //', async ({ runInlineTest }) => { + const result = await runInlineTest(files, { 'grep': '/B$/' }); + expect(result.passed).toBe(3); + expect(result.exitCode).toBe(0); +}); + +test('should grep test name with //', async ({ runInlineTest }) => { + const result = await runInlineTest(files, { 'grep': '/TesT c/i' }); + expect(result.passed).toBe(3); + expect(result.exitCode).toBe(0); +}); + +test('should grep by project name', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [ + { name: 'foo' }, + { name: 'bar' }, + ]}; + `, + 'a.spec.ts': ` + folio.test('should work', () => {}); + `, + }, { 'grep': 'foo]' }); + expect(result.passed).toBe(1); + expect(result.skipped).toBe(0); + expect(result.failed).toBe(0); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/max-failures.spec.ts b/tests/playwright-test/max-failures.spec.ts new file mode 100644 index 0000000000..62cec564c3 --- /dev/null +++ b/tests/playwright-test/max-failures.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('max-failures should work', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + for (let i = 0; i < 10; ++i) { + test('fail_' + i, () => { + expect(true).toBe(false); + }); + } + `, + 'b.spec.js': ` + const { test } = folio; + for (let i = 0; i < 10; ++i) { + test('fail_' + i, () => { + expect(true).toBe(false); + }); + } + ` + }, { 'max-failures': 8 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(8); + expect(result.output.split('\n').filter(l => l.includes('expect(')).length).toBe(16); +}); + +test('-x should work', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + for (let i = 0; i < 10; ++i) { + test('fail_' + i, () => { + expect(true).toBe(false); + }); + } + `, + 'b.spec.js': ` + const { test } = folio; + for (let i = 0; i < 10; ++i) { + test('fail_' + i, () => { + expect(true).toBe(false); + }); + } + ` + }, { '-x': true }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output.split('\n').filter(l => l.includes('expect(')).length).toBe(2); +}); diff --git a/tests/playwright-test/options.spec.ts b/tests/playwright-test/options.spec.ts new file mode 100644 index 0000000000..1120433a96 --- /dev/null +++ b/tests/playwright-test/options.spec.ts @@ -0,0 +1,166 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should merge options', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const test = folio.test.extend({ + foo: 'foo', + bar: 'bar', + }); + + test.use({ foo: 'foo2' }); + test.use({ bar: 'bar2' }); + test('test', ({ foo, bar }) => { + expect(foo).toBe('foo2'); + expect(bar).toBe('bar2'); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should run tests with different test options in the same worker', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + foo: 'foo', + }); + `, + 'a.test.ts': ` + import { test } from './helper'; + test('test', ({ foo }, testInfo) => { + expect(foo).toBe('foo'); + expect(testInfo.workerIndex).toBe(0); + }); + + test.describe('suite1', () => { + test.use({ foo: 'bar' }); + test('test1', ({ foo }, testInfo) => { + expect(foo).toBe('bar'); + expect(testInfo.workerIndex).toBe(0); + }); + + test.describe('suite2', () => { + test.use({ foo: 'baz' }); + test('test2', ({ foo }, testInfo) => { + expect(foo).toBe('baz'); + expect(testInfo.workerIndex).toBe(0); + }); + }); + }); + ` + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +}); + +test('should run tests with different worker options', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + foo: [undefined, { scope: 'worker' }], + }); + `, + 'a.test.ts': ` + import { test } from './helper'; + test('test', ({ foo }, testInfo) => { + expect(foo).toBe(undefined); + console.log('\\n%%test=' + testInfo.workerIndex); + }); + + test.describe('suite1', () => { + test.use({ foo: 'bar' }); + test('test1', ({ foo }, testInfo) => { + expect(foo).toBe('bar'); + console.log('\\n%%test1=' + testInfo.workerIndex); + }); + + test.describe('suite2', () => { + test.use({ foo: 'baz' }); + test('test2', ({ foo }, testInfo) => { + expect(foo).toBe('baz'); + console.log('\\n%%test2=' + testInfo.workerIndex); + }); + }); + + test('test3', ({ foo }, testInfo) => { + expect(foo).toBe('bar'); + console.log('\\n%%test3=' + testInfo.workerIndex); + }); + }); + `, + 'b.test.ts': ` + import { test } from './helper'; + test.use({ foo: 'qux' }); + test('test4', ({ foo }, testInfo) => { + expect(foo).toBe('qux'); + console.log('\\n%%test4=' + testInfo.workerIndex); + }); + ` + }, { workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(5); + + const workerIndexMap = new Map(); + const allWorkers = new Set(); + for (const line of result.output.split('\n')) { + if (line.startsWith('%%')) { + const [ name, workerIndex ] = line.substring(2).split('='); + allWorkers.add(workerIndex); + workerIndexMap.set(name, workerIndex); + } + } + + expect(workerIndexMap.size).toBe(5); + expect(workerIndexMap.get('test1')).toBe(workerIndexMap.get('test3')); + expect(allWorkers.size).toBe(4); + for (let i = 0; i < 4; i++) + expect(allWorkers.has(String(i))); +}); + +test('should use options from the config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + foo: 'foo', + }); + `, + 'playwright.config.ts': ` + module.exports = { use: { foo: 'bar' } }; + `, + 'a.test.ts': ` + import { test } from './helper'; + test('test1', ({ foo }) => { + expect(foo).toBe('bar'); + }); + + test.describe('suite1', () => { + test.use({ foo: 'baz' }); + + test('test2', ({ foo }) => { + expect(foo).toBe('baz'); + }); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); diff --git a/tests/playwright-test/override-timeout.spec.ts b/tests/playwright-test/override-timeout.spec.ts new file mode 100644 index 0000000000..5e6668046b --- /dev/null +++ b/tests/playwright-test/override-timeout.spec.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should consider dynamically set value', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { timeout: 100 }; + `, + 'a.test.js': ` + const { test } = folio; + test('pass', ({}, testInfo) => { + expect(testInfo.timeout).toBe(100); + }) + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should allow different timeouts', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { projects: [ + { timeout: 200 }, + { timeout: 100 }, + ] }; + `, + 'a.test.js': ` + const { test } = folio; + test('pass', ({}, testInfo) => { + console.log('timeout:' + testInfo.timeout); + }); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(result.output).toContain('timeout:100'); + expect(result.output).toContain('timeout:200'); +}); + +test('should prioritize value set via command line', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { timeout: 100 }; + `, + 'a.test.js': ` + const { test } = folio; + test('pass', ({}, testInfo) => { + expect(testInfo.timeout).toBe(1000); + }) + ` + }, { timeout: 1000 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts new file mode 100644 index 0000000000..933a818dc7 --- /dev/null +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -0,0 +1,262 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestInfo, test as base } from '../config/test-runner'; +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import type { ReportFormat } from '../../src/test/reporters/json'; +import rimraf from 'rimraf'; +import { promisify } from 'util'; + +const removeFolderAsync = promisify(rimraf); + +type RunResult = { + exitCode: number, + output: string, + passed: number, + failed: number, + flaky: number, + skipped: number, + report: ReportFormat, + results: any[], +}; + +type TSCResult = { + output: string; + exitCode: number; +}; + +type Files = { [key: string]: string | Buffer }; +type Params = { [key: string]: string | number | boolean | string[] }; +type Env = { [key: string]: string | number | boolean | undefined }; + +async function writeFiles(testInfo: TestInfo, files: Files) { + const baseDir = testInfo.outputPath(); + + const internalPath = JSON.stringify(path.join(__dirname, 'playwright-test-internal')); + const headerJS = ` + const folio = require(${internalPath}); + `; + const headerTS = ` + import * as folio from ${internalPath}; + `; + + const hasConfig = Object.keys(files).some(name => name.includes('.config.')); + if (!hasConfig) { + files = { + ...files, + 'playwright.config.ts': ` + module.exports = { projects: [ {} ] }; + `, + }; + } + + await Promise.all(Object.keys(files).map(async name => { + const fullName = path.join(baseDir, name); + await fs.promises.mkdir(path.dirname(fullName), { recursive: true }); + const isTypeScriptSourceFile = name.endsWith('ts') && !name.endsWith('d.ts'); + const header = isTypeScriptSourceFile ? headerTS : headerJS; + if (/(spec|test)\.(js|ts)$/.test(name)) { + const fileHeader = header + 'const { expect } = folio;\n'; + await fs.promises.writeFile(fullName, fileHeader + files[name]); + } else if (/\.(js|ts)$/.test(name) && !name.endsWith('d.ts')) { + await fs.promises.writeFile(fullName, header + files[name]); + } else { + await fs.promises.writeFile(fullName, files[name]); + } + })); + + return baseDir; +} + +async function runTSC(baseDir: string): Promise { + const tscProcess = spawn('npx', ['tsc', '-p', baseDir], { + cwd: baseDir, + shell: true, + }); + let output = ''; + tscProcess.stderr.on('data', chunk => { + output += String(chunk); + if (process.env.PW_RUNNER_DEBUG) + process.stderr.write(String(chunk)); + }); + tscProcess.stdout.on('data', chunk => { + output += String(chunk); + if (process.env.PW_RUNNER_DEBUG) + process.stdout.write(String(chunk)); + }); + const status = await new Promise(x => tscProcess.on('close', x)); + return { + exitCode: status, + output, + }; +} + +async function runFolio(baseDir: string, params: any, env: Env): Promise { + const paramList = []; + let additionalArgs = ''; + for (const key of Object.keys(params)) { + if (key === 'args') { + additionalArgs = params[key]; + continue; + } + for (const value of Array.isArray(params[key]) ? params[key] : [params[key]]) { + const k = key.startsWith('-') ? key : '--' + key; + paramList.push(params[key] === true ? `${k}` : `${k}=${value}`); + } + } + const outputDir = path.join(baseDir, 'test-results'); + const reportFile = path.join(outputDir, 'report.json'); + const args = [path.join(__dirname, '..', '..', 'lib', 'cli', 'cli.js'), 'test']; + args.push( + '--output=' + outputDir, + '--reporter=dot,json', + '--workers=2', + ...paramList + ); + if (additionalArgs) + args.push(...additionalArgs); + const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'playwright-test-cache-')); + const testProcess = spawn('node', args, { + env: { + ...process.env, + ...env, + PLAYWRIGHT_JSON_OUTPUT_NAME: reportFile, + PWTEST_CACHE_DIR: cacheDir, + PWTEST_CLI_ALLOW_TEST_COMMAND: '1', + }, + cwd: baseDir + }); + let output = ''; + testProcess.stderr.on('data', chunk => { + output += String(chunk); + if (process.env.PW_RUNNER_DEBUG) + process.stderr.write(String(chunk)); + }); + testProcess.stdout.on('data', chunk => { + output += String(chunk); + if (process.env.PW_RUNNER_DEBUG) + process.stdout.write(String(chunk)); + }); + const status = await new Promise(x => testProcess.on('close', x)); + await removeFolderAsync(cacheDir); + + const outputString = output.toString(); + const summary = (re: RegExp) => { + let result = 0; + let match = re.exec(outputString); + while (match) { + result += (+match[1]); + match = re.exec(outputString); + } + return result; + }; + const passed = summary(/(\d+) passed/g); + const failed = summary(/(\d+) failed/g); + const flaky = summary(/(\d+) flaky/g); + const skipped = summary(/(\d+) skipped/g); + let report; + try { + report = JSON.parse(fs.readFileSync(reportFile).toString()); + } catch (e) { + output += '\n' + e.toString(); + } + + const results = []; + function visitSuites(suites?: ReportFormat['suites']) { + if (!suites) + return; + for (const suite of suites) { + for (const spec of suite.specs) { + for (const test of spec.tests) + results.push(...test.results); + } + visitSuites(suite.suites); + } + } + if (report) + visitSuites(report.suites); + + return { + exitCode: status, + output, + passed, + failed, + flaky, + skipped, + report, + results, + }; +} + +type Fixtures = { + writeFiles: (files: Files) => Promise; + runInlineTest: (files: Files, params?: Params, env?: Env) => Promise; + runTSC: (files: Files) => Promise; +}; + +export const test = base.extend({ + writeFiles: async ({}, use, testInfo) => { + await use(files => writeFiles(testInfo, files)); + }, + + runInlineTest: async ({}, use, testInfo: TestInfo) => { + let runResult: RunResult | undefined; + await use(async (files: Files, params: Params = {}, env: Env = {}) => { + const baseDir = await writeFiles(testInfo, files); + runResult = await runFolio(baseDir, params, env); + return runResult; + }); + if (testInfo.status !== testInfo.expectedStatus && runResult) + console.log(runResult.output); + }, + + runTSC: async ({}, use, testInfo) => { + let tscResult: TSCResult | undefined; + await use(async files => { + const baseDir = await writeFiles(testInfo, { ...files, 'tsconfig.json': JSON.stringify(TSCONFIG) }); + tscResult = await runTSC(baseDir); + return tscResult; + }); + if (testInfo.status !== testInfo.expectedStatus && tscResult) + console.log(tscResult.output); + }, +}); + +const TSCONFIG = { + 'compilerOptions': { + 'target': 'ESNext', + 'moduleResolution': 'node', + 'module': 'commonjs', + 'strict': true, + 'esModuleInterop': true, + 'allowSyntheticDefaultImports': true, + 'rootDir': '.', + 'lib': ['esnext', 'dom', 'DOM.Iterable'] + }, + 'exclude': [ + 'node_modules' + ] +}; + +export { expect } from '../config/test-runner'; + +const asciiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g'); +export function stripAscii(str: string): string { + return str.replace(asciiRegex, ''); +} diff --git a/tests/playwright-test/playwright-test-internal.d.ts b/tests/playwright-test/playwright-test-internal.d.ts new file mode 100644 index 0000000000..3fd487cdfc --- /dev/null +++ b/tests/playwright-test/playwright-test-internal.d.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TestType } from '../../types/testInternal'; +import type { Expect } from '../../types/testExpect'; +export type { Project, Config, TestStatus, TestInfo, WorkerInfo, TestType, Fixtures, TestFixture, WorkerFixture } from '../../types/testInternal'; +export const test: TestType<{}, {}>; +export default test; +export const expect: Expect; diff --git a/tests/playwright-test/playwright-test-internal.js b/tests/playwright-test/playwright-test-internal.js new file mode 100644 index 0000000000..ca49267b1d --- /dev/null +++ b/tests/playwright-test/playwright-test-internal.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = require('../../lib/test/internal'); diff --git a/tests/playwright-test/playwright-test.config.ts b/tests/playwright-test/playwright-test.config.ts new file mode 100644 index 0000000000..707360209a --- /dev/null +++ b/tests/playwright-test/playwright-test.config.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '../config/test-runner'; + +const config: Config = { + testDir: __dirname, + testIgnore: 'assets/**', + timeout: 20000, + forbidOnly: !!process.env.CI, + projects: [ + { name: 'playwright-test' }, + ] +}; + +export default config; diff --git a/tests/playwright-test/repeat-each.spec.ts b/tests/playwright-test/repeat-each.spec.ts new file mode 100644 index 0000000000..078387283d --- /dev/null +++ b/tests/playwright-test/repeat-each.spec.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should repeat from command line', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + test('test', ({}, testInfo) => { + console.log('REPEAT ' + testInfo.repeatEachIndex); + expect(1).toBe(1); + }); + ` + }, { 'repeat-each': 3 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.output).toContain('REPEAT 0'); + expect(result.output).toContain('REPEAT 1'); + expect(result.output).toContain('REPEAT 2'); + expect(result.output).not.toContain('REPEAT 3'); +}); + +test('should repeat based on config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { projects: [ + { name: 'no-repeats' }, + { repeatEach: 2, name: 'two-repeats' }, + ] }; + `, + 'a.test.js': ` + const { test } = folio; + test('my test', ({}, testInfo) => {}); + ` + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + const names = result.report.suites[0].specs[0].tests.map(test => test.projectName); + expect(names).toEqual(['no-repeats', 'two-repeats', 'two-repeats']); +}); diff --git a/tests/playwright-test/retry.spec.ts b/tests/playwright-test/retry.spec.ts new file mode 100644 index 0000000000..837d866c96 --- /dev/null +++ b/tests/playwright-test/retry.spec.ts @@ -0,0 +1,186 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, stripAscii } from './playwright-test-fixtures'; + +test('should retry failures', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'retry-failures.spec.js': ` + const { test } = folio; + test('flake', async ({}, testInfo) => { + // Passes on the second run. + expect(testInfo.retry).toBe(1); + }); + ` + }, { retries: 10 }); + expect(result.exitCode).toBe(0); + expect(result.flaky).toBe(1); + expect(result.results.length).toBe(2); + expect(result.results[0].workerIndex).toBe(0); + expect(result.results[0].retry).toBe(0); + expect(result.results[0].status).toBe('failed'); + expect(result.results[1].workerIndex).toBe(1); + expect(result.results[1].retry).toBe(1); + expect(result.results[1].status).toBe('passed'); +}); + +test('should retry based on config', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { projects: [ + { retries: 0, name: 'no-retries' }, + { retries: 2, name: 'two-retries' }, + ] }; + `, + 'a.test.js': ` + const { test } = folio; + test('pass', ({}, testInfo) => { + // Passes on the third run. + expect(testInfo.retry).toBe(2); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.flaky).toBe(1); + expect(result.failed).toBe(1); + expect(result.results.length).toBe(4); +}); + +test('should retry timeout', async ({ runInlineTest }) => { + const { exitCode, passed, failed, output } = await runInlineTest({ + 'one-timeout.spec.js': ` + const { test } = folio; + test('timeout', async () => { + await new Promise(f => setTimeout(f, 10000)); + }); + ` + }, { timeout: 100, retries: 2 }); + expect(exitCode).toBe(1); + expect(passed).toBe(0); + expect(failed).toBe(1); + expect(stripAscii(output).split('\n')[0]).toBe('××T'); +}); + +test('should fail on unexpected pass with retries', async ({ runInlineTest }) => { + const { exitCode, failed, output } = await runInlineTest({ + 'unexpected-pass.spec.js': ` + const { test } = folio; + test('succeeds', () => { + test.fail(); + expect(1 + 1).toBe(2); + }); + ` + }, { retries: 1 }); + expect(exitCode).toBe(1); + expect(failed).toBe(1); + expect(output).toContain('passed unexpectedly'); +}); + +test('should not retry unexpected pass', async ({ runInlineTest }) => { + const { exitCode, passed, failed, output } = await runInlineTest({ + 'unexpected-pass.spec.js': ` + const { test } = folio; + test('succeeds', () => { + test.fail(); + expect(1 + 1).toBe(2); + }); + ` + }, { retries: 2 }); + expect(exitCode).toBe(1); + expect(passed).toBe(0); + expect(failed).toBe(1); + expect(stripAscii(output).split('\n')[0]).toBe('F'); +}); + +test('should not retry expected failure', async ({ runInlineTest }) => { + const { exitCode, passed, failed, output } = await runInlineTest({ + 'expected-failure.spec.js': ` + const { test } = folio; + test('fails', () => { + test.fail(); + expect(1 + 1).toBe(3); + }); + + test('non-empty remaining',() => { + expect(1 + 1).toBe(2); + }); + ` + }, { retries: 2 }); + expect(exitCode).toBe(0); + expect(passed).toBe(2); + expect(failed).toBe(0); + expect(stripAscii(output).split('\n')[0]).toBe('··'); +}); + +test('should retry unhandled rejection', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'unhandled-rejection.spec.js': ` + const { test } = folio; + test('unhandled rejection', async () => { + setTimeout(() => { + throw new Error('Unhandled rejection in the test'); + }); + await new Promise(f => setTimeout(f, 20)); + }); + ` + }, { retries: 2 }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(stripAscii(result.output).split('\n')[0]).toBe('××F'); + expect(result.output).toContain('Unhandled rejection'); +}); + +test('should retry beforeAll failure', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + test.beforeAll(async () => { + throw new Error('BeforeAll is bugged!'); + }); + test('passing test', async () => { + }); + ` + }, { retries: 2 }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(stripAscii(result.output).split('\n')[0]).toBe('××F'); + expect(result.output).toContain('BeforeAll is bugged!'); +}); + +test('should retry worker fixture setup failure', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + worker: [ async () => { + throw new Error('worker setup is bugged!'); + }, { scope: 'worker' } ] + }); + `, + 'a.spec.ts': ` + import { test } from './helper'; + test('passing test', async ({ worker }) => { + }); + ` + }, { retries: 2 }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(stripAscii(result.output).split('\n')[0]).toBe('××F'); + expect(result.output).toContain('worker setup is bugged!'); +}); diff --git a/tests/playwright-test/shard.spec.ts b/tests/playwright-test/shard.spec.ts new file mode 100644 index 0000000000..c35080a374 --- /dev/null +++ b/tests/playwright-test/shard.spec.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +const tests = { + 'a.spec.ts': ` + const { test } = folio; + test('test1', async () => { + console.log('test1-done'); + }); + test('test2', async () => { + console.log('test2-done'); + }); + test('test3', async () => { + console.log('test3-done'); + }); + `, + 'b.spec.ts': ` + const { test } = folio; + test('test4', async () => { + console.log('test4-done'); + }); + `, +}; + +test('should respect shard=1/2', async ({ runInlineTest }) => { + const result = await runInlineTest(tests, { shard: '1/2' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.skipped).toBe(1); + expect(result.output).toContain('test1-done'); + expect(result.output).toContain('test2-done'); + expect(result.output).toContain('test3-done'); +}); + +test('should respect shard=2/2', async ({ runInlineTest }) => { + const result = await runInlineTest(tests, { shard: '2/2' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.skipped).toBe(3); + expect(result.output).toContain('test4-done'); +}); diff --git a/tests/playwright-test/stdio.spec.ts b/tests/playwright-test/stdio.spec.ts new file mode 100644 index 0000000000..a513c263e3 --- /dev/null +++ b/tests/playwright-test/stdio.spec.ts @@ -0,0 +1,62 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should get top level stdio', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + console.log('\\n%% top level stdout'); + console.error('\\n%% top level stderr'); + test('is a test', () => { + console.log('\\n%% stdout in a test'); + console.error('\\n%% stderr in a test'); + }); + ` + }); + expect(result.output.split('\n').filter(x => x.startsWith('%%'))).toEqual([ + '%% top level stdout', + '%% top level stderr', + '%% top level stdout', // top level logs appear twice, because the file is required twice + '%% top level stderr', + '%% stdout in a test', + '%% stderr in a test' + ]); +}); + +test('should get stdio from env afterAll', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + fixture: [ async ({}, run) => { + console.log('\\n%% worker setup'); + await run(); + console.log('\\n%% worker teardown'); + }, { scope: 'worker' } ] + }); + `, + 'a.spec.js': ` + const { test } = require('./helper'); + test('is a test', async ({fixture}) => {}); + ` + }); + expect(result.output.split('\n').filter(x => x.startsWith('%%'))).toEqual([ + '%% worker setup', + '%% worker teardown' + ]); +}); + diff --git a/tests/playwright-test/test-extend.spec.ts b/tests/playwright-test/test-extend.spec.ts new file mode 100644 index 0000000000..3843494c96 --- /dev/null +++ b/tests/playwright-test/test-extend.spec.ts @@ -0,0 +1,178 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('test.extend should work', async ({ runInlineTest }) => { + const { output, passed } = await runInlineTest({ + 'helper.ts': ` + global.logs = []; + + function createDerivedFixtures(suffix) { + return { + derivedWorker: [async ({ baseWorker }, run) => { + global.logs.push('beforeAll-' + suffix); + await run(); + global.logs.push('afterAll-' + suffix); + if (suffix.includes('base')) + console.log(global.logs.join('\\n')); + }, { scope: 'worker' }], + + derivedTest: async ({ baseTest, derivedWorker }, run) => { + global.logs.push('beforeEach-' + suffix); + await run(); + global.logs.push('afterEach-' + suffix); + }, + }; + } + + export const base = folio.test.declare(); + export const test1 = base.extend(createDerivedFixtures('e1')); + export const test2 = base.extend(createDerivedFixtures('e2')); + `, + 'playwright.config.ts': ` + import { base } from './helper'; + + function createBaseFixtures(suffix) { + return { + baseWorker: [async ({}, run) => { + global.logs.push('beforeAll-' + suffix); + await run(); + global.logs.push('afterAll-' + suffix); + if (suffix.includes('base')) + console.log(global.logs.join('\\n')); + }, { scope: 'worker' }], + + baseTest: async ({ derivedWorker }, run) => { + global.logs.push('beforeEach-' + suffix); + await run(); + global.logs.push('afterEach-' + suffix); + }, + }; + } + + module.exports = { projects: [ + { define: { test: base, fixtures: createBaseFixtures('base1') } }, + { define: { test: base, fixtures: createBaseFixtures('base2') } }, + ] }; + `, + 'a.test.ts': ` + import { test1, test2 } from './helper'; + test1('should work', async ({ derivedTest }) => { + global.logs.push('test1'); + }); + test2('should work', async ({ derivedTest }) => { + global.logs.push('test2'); + }); + `, + }); + expect(passed).toBe(4); + expect(output).toContain([ + 'beforeAll-base1', + 'beforeAll-e1', + 'beforeEach-base1', + 'beforeEach-e1', + 'test1', + 'afterEach-e1', + 'afterEach-base1', + 'afterAll-e1', + 'afterAll-base1', + ].join('\n')); + expect(output).toContain([ + 'beforeAll-base1', + 'beforeAll-e2', + 'beforeEach-base1', + 'beforeEach-e2', + 'test2', + 'afterEach-e2', + 'afterEach-base1', + 'afterAll-e2', + 'afterAll-base1', + ].join('\n')); + expect(output).toContain([ + 'beforeAll-base2', + 'beforeAll-e1', + 'beforeEach-base2', + 'beforeEach-e1', + 'test1', + 'afterEach-e1', + 'afterEach-base2', + 'afterAll-e1', + 'afterAll-base2', + ].join('\n')); + expect(output).toContain([ + 'beforeAll-base2', + 'beforeAll-e2', + 'beforeEach-base2', + 'beforeEach-e2', + 'test2', + 'afterEach-e2', + 'afterEach-base2', + 'afterAll-e2', + 'afterAll-base2', + ].join('\n')); +}); + +test('test.declare should be inserted at the right place', async ({ runInlineTest }) => { + const { output, passed } = await runInlineTest({ + 'helper.ts': ` + const test1 = folio.test.extend({ + foo: async ({}, run) => { + console.log('before-foo'); + await run('foo'); + console.log('after-foo'); + }, + }); + export const test2 = test1.declare<{ bar: string }>(); + export const test3 = test2.extend({ + baz: async ({ bar }, run) => { + console.log('before-baz'); + await run(bar + 'baz'); + console.log('after-baz'); + }, + }); + `, + 'playwright.config.ts': ` + import { test2 } from './helper'; + const fixtures = { + bar: async ({ foo }, run) => { + console.log('before-bar'); + await run(foo + 'bar'); + console.log('after-bar'); + }, + }; + module.exports = { + define: { test: test2, fixtures }, + }; + `, + 'a.test.js': ` + const { test3 } = require('./helper'); + test3('should work', async ({baz}) => { + console.log('test-' + baz); + }); + `, + }); + expect(passed).toBe(1); + expect(output).toContain([ + 'before-foo', + 'before-bar', + 'before-baz', + 'test-foobarbaz', + 'after-baz', + 'after-bar', + 'after-foo', + ].join('\n')); +}); diff --git a/tests/playwright-test/test-ignore.spec.ts b/tests/playwright-test/test-ignore.spec.ts new file mode 100644 index 0000000000..b1773f573d --- /dev/null +++ b/tests/playwright-test/test-ignore.spec.ts @@ -0,0 +1,262 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; +import * as path from 'path'; + +const tests = { + 'a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'c.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` +}; + +test('should run all three tests', async ({ runInlineTest }) => { + const result = await runInlineTest(tests); + expect(result.passed).toBe(3); + expect(result.exitCode).toBe(0); +}); + +test('should ignore a test', async ({ runInlineTest }) => { + const result = await runInlineTest({ + ...tests, + 'playwright.config.ts': ` + module.exports = { testIgnore: 'b.test.ts' }; + `, + }); + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); +}); + +test('should ignore a folder', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { testIgnore: 'folder/**' }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'folder/a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'folder/b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'folder/c.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should ignore a node_modules', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'node_modules/a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'node_modules/b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'folder/c.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); +}); + +test('should filter tests', async ({ runInlineTest }) => { + const result = await runInlineTest({ + ...tests, + 'playwright.config.ts': ` + module.exports = { testIgnore: 'c.test.*' }; + `, + }); + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); +}); + +test('should use a different test match', async ({ runInlineTest }) => { + const result = await runInlineTest({ + ...tests, + 'playwright.config.ts': ` + module.exports = { testMatch: '[a|b].test.ts' }; + `, + }); + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); +}); + +test('should use an array for testMatch', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { testMatch: ['b.test.ts', /\\${path.sep}a.[tes]{4}.TS$/i] }; + `, + 'dir/a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'c.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.passed).toBe(2); + expect(result.report.suites.map(s => s.file).sort()).toEqual(['b.test.ts', 'dir/a.test.ts']); + expect(result.exitCode).toBe(0); +}); + +test('should match absolute path', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { testDir: path.join(__dirname, 'dir'), testMatch: /dir\\${path.sep}a/ }; + `, + 'dir/a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'dir/b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.passed).toBe(1); + expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']); + expect(result.exitCode).toBe(0); +}); + +test('should match cli string argument', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + import * as path from 'path'; + module.exports = { testDir: path.join(__dirname, 'dir') }; + `, + 'dir/a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'dir/b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }, { args: [`dir\\${path.sep}a`] }); + expect(result.passed).toBe(1); + expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']); + expect(result.exitCode).toBe(0); +}); + +test('should match regex string argument', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'dir/filea.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'dir/fileb.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'filea.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }, { args: ['/filea.*ts/'] }); + expect(result.passed).toBe(2); + expect(result.report.suites.map(s => s.file).sort()).toEqual(['dir/filea.test.ts', 'filea.test.ts']); + expect(result.exitCode).toBe(0); +}); + +test('should match by directory', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'dir-a/file.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'dir-b/file1.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'dir-b/file2.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'file.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }, { args: ['dir-b'] }); + expect(result.passed).toBe(2); + expect(result.report.suites.map(s => s.file).sort()).toEqual(['dir-b/file1.test.ts', 'dir-b/file2.test.ts']); + expect(result.exitCode).toBe(0); +}); + +test('should ignore node_modules even with custom testIgnore', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { testIgnore: 'a.test.ts' }; + `, + 'a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'node_modules/a.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'node_modules/b.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + `, + 'folder/c.test.ts': ` + const { test } = folio; + test('pass', ({}) => {}); + ` + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); \ No newline at end of file diff --git a/tests/playwright-test/test-info.spec.ts b/tests/playwright-test/test-info.spec.ts new file mode 100644 index 0000000000..69cd6e0ea9 --- /dev/null +++ b/tests/playwright-test/test-info.spec.ts @@ -0,0 +1,54 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should work directly', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('test 1', async ({}, testInfo) => { + expect(testInfo.title).toBe('test 1'); + }); + test('test 2', async ({}, testInfo) => { + expect(testInfo.title).toBe('test 2'); + }); + `, + }); + expect(result.exitCode).toBe(0); +}); + +test('should work via fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + title: async ({}, run, testInfo) => { + await run(testInfo.title); + }, + }); + `, + 'a.test.js': ` + const { test } = require('./helper'); + test('test 1', async ({title}) => { + expect(title).toBe('test 1'); + }); + test('test 2', async ({title}) => { + expect(title).toBe('test 2'); + }); + `, + }); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts new file mode 100644 index 0000000000..1697176362 --- /dev/null +++ b/tests/playwright-test/test-modifiers.spec.ts @@ -0,0 +1,216 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('test modifiers should work', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + foo: true, + }); + `, + 'a.test.ts': ` + import { test } from './helper'; + + test('passed1', async ({foo}) => { + }); + test('passed2', async ({foo}) => { + test.skip(false); + }); + test('passed3', async () => { + test.fixme(undefined); + }); + test('passed4', async () => { + test.fixme(undefined, 'reason') + }); + test('passed5', async ({foo}) => { + test.skip(false); + }); + + test('skipped1', async ({foo}) => { + test.skip(); + }); + test('skipped2', async ({foo}) => { + test.skip('reason'); + }); + test('skipped3', async ({foo}) => { + test.skip(foo); + }); + test('skipped4', async ({foo}) => { + test.skip(foo, 'reason'); + }); + test('skipped5', async () => { + test.fixme(); + }); + test('skipped6', async () => { + test.fixme(true, 'reason'); + }); + + test('failed1', async ({foo}) => { + test.fail(); + expect(true).toBe(false); + }); + test('failed2', async ({foo}) => { + test.fail('reason'); + expect(true).toBe(false); + }); + test('failed3', async ({foo}) => { + test.fail(foo); + expect(true).toBe(false); + }); + test('failed4', async ({foo}) => { + test.fail(foo, 'reason'); + expect(true).toBe(false); + }); + + test.describe('suite1', () => { + test.skip(); + test('suite1', () => {}); + }); + + test.describe('suite2', () => { + test.skip(true); + test('suite2', () => {}); + }); + + test.describe('suite3', () => { + test.skip(({ foo }) => foo, 'reason'); + test('suite3', () => {}); + }); + + test.describe('suite3', () => { + test.skip(({ foo }) => !foo, 'reason'); + test('suite4', () => {}); + }); + `, + }); + + const expectTest = (title: string, expectedStatus: string, status: string, annotations: any) => { + const spec = result.report.suites[0].specs.find(s => s.title === title) || + result.report.suites[0].suites.find(s => s.specs[0].title === title).specs[0]; + const test = spec.tests[0]; + expect(test.expectedStatus).toBe(expectedStatus); + expect(test.results[0].status).toBe(status); + expect(test.annotations).toEqual(annotations); + }; + expectTest('passed1', 'passed', 'passed', []); + expectTest('passed2', 'passed', 'passed', []); + expectTest('passed3', 'passed', 'passed', []); + expectTest('passed4', 'passed', 'passed', []); + expectTest('passed5', 'passed', 'passed', []); + expectTest('skipped1', 'skipped', 'skipped', [{ type: 'skip' }]); + expectTest('skipped2', 'skipped', 'skipped', [{ type: 'skip' }]); + expectTest('skipped3', 'skipped', 'skipped', [{ type: 'skip' }]); + expectTest('skipped4', 'skipped', 'skipped', [{ type: 'skip', description: 'reason' }]); + expectTest('skipped5', 'skipped', 'skipped', [{ type: 'fixme' }]); + expectTest('skipped6', 'skipped', 'skipped', [{ type: 'fixme', description: 'reason' }]); + expectTest('failed1', 'failed', 'failed', [{ type: 'fail' }]); + expectTest('failed2', 'failed', 'failed', [{ type: 'fail' }]); + expectTest('failed3', 'failed', 'failed', [{ type: 'fail' }]); + expectTest('failed4', 'failed', 'failed', [{ type: 'fail', description: 'reason' }]); + expectTest('suite1', 'skipped', 'skipped', [{ type: 'skip' }]); + expectTest('suite2', 'skipped', 'skipped', [{ type: 'skip' }]); + expectTest('suite3', 'skipped', 'skipped', [{ type: 'skip', description: 'reason' }]); + expectTest('suite4', 'passed', 'passed', []); + expect(result.passed).toBe(10); + expect(result.skipped).toBe(9); +}); + +test('test modifiers should check types', async ({runTSC}) => { + const result = await runTSC({ + 'helper.ts': ` + export const test = folio.test.extend<{ foo: boolean }>({ + foo: async ({}, use, testInfo) => { + testInfo.skip(); + testInfo.fixme(false); + testInfo.slow(true, 'reason'); + testInfo.fail(false, 'reason'); + // @ts-expect-error + testInfo.skip('reason'); + // @ts-expect-error + testInfo.fixme('foo', 'reason'); + // @ts-expect-error + testInfo.slow(() => true); + use(true); + }, + }); + `, + 'a.test.ts': ` + import { test } from './helper'; + + test('passed1', async ({foo}) => { + test.skip(); + }); + test('passed2', async ({foo}) => { + test.skip(foo); + }); + test('passed2', async ({foo}) => { + test.skip(foo, 'reason'); + }); + test('passed3', async ({foo}) => { + test.skip(({foo}) => foo); + }); + test('passed3', async ({foo}) => { + test.skip(({foo}) => foo, 'reason'); + }); + test('passed3', async ({foo}) => { + // @ts-expect-error + test.skip('foo', 'bar'); + }); + test('passed3', async ({foo}) => { + // @ts-expect-error + test.skip(({ bar }) => bar, 'reason'); + }); + test('passed3', async ({foo}) => { + // @ts-expect-error + test.skip(42); + }); + `, + }); + expect(result.exitCode).toBe(0); +}); + +test('should skip inside fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const test = folio.test.extend({ + foo: async ({}, run, testInfo) => { + testInfo.skip(true, 'reason'); + await run(); + }, + }); + + test('skipped', async ({ foo }) => { + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.skipped).toBe(1); + expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'skip', description: 'reason' }]); +}); + +test('modifier with a function should throw in the test', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + folio.test('skipped', async ({}) => { + folio.test.skip(() => true); + }); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('test.skip() with a function can only be called inside describe block'); +}); diff --git a/tests/playwright-test/test-output-dir.spec.ts b/tests/playwright-test/test-output-dir.spec.ts new file mode 100644 index 0000000000..4b65d8f9b8 --- /dev/null +++ b/tests/playwright-test/test-output-dir.spec.ts @@ -0,0 +1,233 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { test, expect } from './playwright-test-fixtures'; + +test('should work and remove non-failures on CI', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'dir/my-test.spec.js': ` + const { test } = folio; + test('test 1', async ({}, testInfo) => { + if (testInfo.retry) { + expect(testInfo.outputDir).toContain('dir-my-test-test-1-retry' + testInfo.retry); + expect(testInfo.outputPath('foo', 'bar')).toContain(require('path').join('dir-my-test-test-1-retry' + testInfo.retry, 'foo', 'bar')); + require('fs').writeFileSync(testInfo.outputPath('file.txt'), 'content', 'utf-8'); + } else { + expect(testInfo.outputDir).toContain('dir-my-test-test-1'); + expect(testInfo.outputPath()).toContain('dir-my-test-test-1'); + expect(testInfo.outputPath('foo', 'bar')).toContain(require('path').join('dir-my-test-test-1', 'foo', 'bar')); + require('fs').writeFileSync(testInfo.outputPath('file.txt'), 'content', 'utf-8'); + } + expect(require('fs').existsSync(testInfo.outputDir)).toBe(true); + if (testInfo.retry < 2) + throw new Error('Give me retries'); + }); + `, + }, { retries: 2 }, { CI: '1' }); + expect(result.exitCode).toBe(0); + + expect(result.results[0].status).toBe('failed'); + expect(result.results[0].retry).toBe(0); + // Should only fail the last retry check. + expect(result.results[0].error.message).toBe('Give me retries'); + + expect(result.results[1].status).toBe('failed'); + expect(result.results[1].retry).toBe(1); + // Should only fail the last retry check. + expect(result.results[1].error.message).toBe('Give me retries'); + + expect(result.results[2].status).toBe('passed'); + expect(result.results[2].retry).toBe(2); + + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1-retry1'))).toBe(true); + // Last retry is successfull, so output dir should be removed. + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1-retry2'))).toBe(false); +}); + +test('should include repeat token', async ({runInlineTest}) => { + const result = await runInlineTest({ + 'a.spec.js': ` + const { test } = folio; + test('test', ({}, testInfo) => { + if (testInfo.repeatEachIndex) + expect(testInfo.outputPath('')).toContain('repeat' + testInfo.repeatEachIndex); + else + expect(testInfo.outputPath('')).not.toContain('repeat' + testInfo.repeatEachIndex); + }); + ` + }, { 'repeat-each': 3 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +}); + +test('should include the project name', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + auto: [ async ({}, run, testInfo) => { + await run(); + }, { auto: true } ] + }); + export const test2 = folio.test.extend({ + auto: [ async ({}, run, testInfo) => { + testInfo.snapshotSuffix = 'suffix'; + await run(); + }, { auto: true } ] + }); + `, + 'playwright.config.ts': ` + module.exports = { projects: [ + {}, + { name: 'foo' }, + { name: 'foo' }, + { name: 'Bar space!' }, + ] }; + `, + 'my-test.spec.js': ` + const { test, test2 } = require('./helper'); + test('test 1', async ({}, testInfo) => { + console.log(testInfo.outputPath('bar.txt').replace(/\\\\/g, '/')); + console.log(testInfo.snapshotPath('bar.txt').replace(/\\\\/g, '/')); + if (testInfo.retry !== 1) + throw new Error('Give me a retry'); + }); + test2('test 2', async ({}, testInfo) => { + console.log(testInfo.outputPath('bar.txt').replace(/\\\\/g, '/')); + console.log(testInfo.snapshotPath('bar.txt').replace(/\\\\/g, '/')); + }); + `, + }, { retries: 1 }); + expect(result.exitCode).toBe(0); + expect(result.results[0].status).toBe('failed'); + expect(result.results[1].status).toBe('passed'); + + // test1, run with empty + expect(result.output).toContain('test-results/my-test-test-1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar.txt'); + expect(result.output).toContain('test-results/my-test-test-1-retry1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar.txt'); + + // test1, run with foo #1 + expect(result.output).toContain('test-results/my-test-test-1-foo1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt'); + expect(result.output).toContain('test-results/my-test-test-1-foo1-retry1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt'); + + // test1, run with foo #2 + expect(result.output).toContain('test-results/my-test-test-1-foo2/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt'); + expect(result.output).toContain('test-results/my-test-test-1-foo2-retry1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt'); + + // test1, run with bar + expect(result.output).toContain('test-results/my-test-test-1-Bar-space-/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-Bar-space-.txt'); + expect(result.output).toContain('test-results/my-test-test-1-Bar-space--retry1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-Bar-space-.txt'); + + // test2, run with empty + expect(result.output).toContain('test-results/my-test-test-2/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-suffix.txt'); + + // test2, run with foo #1 + expect(result.output).toContain('test-results/my-test-test-2-foo1/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo-suffix.txt'); + + // test2, run with foo #2 + expect(result.output).toContain('test-results/my-test-test-2-foo2/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo-suffix.txt'); + + // test2, run with bar + expect(result.output).toContain('test-results/my-test-test-2-Bar-space-/bar.txt'); + expect(result.output).toContain('my-test.spec.js-snapshots/bar-Bar-space--suffix.txt'); +}); + +test('should remove output dirs for projects run', async ({runInlineTest}, testInfo) => { + const paths: string[] = []; + const files: string[] = []; + + for (let i = 0; i < 3; i++) { + const p = testInfo.outputPath('path' + i); + await fs.promises.mkdir(p, { recursive: true }); + const f = path.join(p, 'my-file.txt'); + await fs.promises.writeFile(f, 'contents', 'utf-8'); + paths.push(p); + files.push(f); + } + + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { projects: [ + { outputDir: ${JSON.stringify(paths[0])} }, + { outputDir: ${JSON.stringify(paths[2])} }, + ] }; + `, + 'a.test.js': ` + const { test } = folio; + test('my test', ({}, testInfo) => {}); + ` + }, { output: '' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + + expect(fs.existsSync(files[0])).toBe(false); + expect(fs.existsSync(files[1])).toBe(true); + expect(fs.existsSync(files[2])).toBe(false); +}); + +test('should remove folders with preserveOutput=never', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { preserveOutput: 'never' }; + `, + 'dir/my-test.spec.js': ` + const { test } = folio; + test('test 1', async ({}, testInfo) => { + require('fs').writeFileSync(testInfo.outputPath('file.txt'), 'content', 'utf-8'); + if (testInfo.retry < 2) + throw new Error('Give me retries'); + }); + `, + }, { retries: 2 }); + expect(result.exitCode).toBe(0); + expect(result.results.length).toBe(3); + + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1'))).toBe(false); + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1-retry1'))).toBe(false); + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1-retry2'))).toBe(false); +}); + +test('should not remove folders on non-CI', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'dir/my-test.spec.js': ` + const { test } = folio; + test('test 1', async ({}, testInfo) => { + require('fs').writeFileSync(testInfo.outputPath('file.txt'), 'content', 'utf-8'); + if (testInfo.retry < 2) + throw new Error('Give me retries'); + }); + `, + }, { 'retries': 2 }, { CI: '' }); + expect(result.exitCode).toBe(0); + expect(result.results.length).toBe(3); + + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1-retry1'))).toBe(true); + expect(fs.existsSync(testInfo.outputPath('test-results', 'dir-my-test-test-1-retry2'))).toBe(true); +}); diff --git a/tests/playwright-test/timeout.spec.ts b/tests/playwright-test/timeout.spec.ts new file mode 100644 index 0000000000..3041bce479 --- /dev/null +++ b/tests/playwright-test/timeout.spec.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should run fixture teardown on timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'helper.ts': ` + export const test = folio.test.extend({ + foo: async ({}, run, testInfo) => { + await run(); + console.log('STATUS:' + testInfo.status); + } + }); + `, + 'c.spec.ts': ` + import { test } from './helper'; + test('works', async ({ foo }) => { + await new Promise(f => setTimeout(f, 100000)); + }); + ` + }, { timeout: 1000 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('STATUS:timedOut'); +}); + +test('should respect test.setTimeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const { test } = folio; + test('fails', async ({}) => { + await new Promise(f => setTimeout(f, 1500)); + }); + test('passes', async ({}) => { + await new Promise(f => setTimeout(f, 500)); + test.setTimeout(2000); + await new Promise(f => setTimeout(f, 1000)); + }); + + test.describe('suite', () => { + test.beforeEach(() => { + test.setTimeout(2000); + }); + test('passes2', async ({}, testInfo) => { + expect(testInfo.timeout).toBe(2000); + await new Promise(f => setTimeout(f, 1500)); + }); + }); + ` + }, { timeout: 1000 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.passed).toBe(2); + expect(result.output).toContain('Timeout of 1000ms exceeded'); +}); + +test('should timeout when calling test.setTimeout too late', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const { test } = folio; + test('fails', async ({}) => { + await new Promise(f => setTimeout(f, 500)); + test.setTimeout(100); + await new Promise(f => setTimeout(f, 1)); + }); + ` + }, { timeout: 1000 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.passed).toBe(0); + expect(result.output).toContain('Timeout of 100ms exceeded'); +}); + +test('should respect test.slow', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const { test } = folio; + test('fails', async ({}) => { + await new Promise(f => setTimeout(f, 1500)); + }); + test('passes', async ({}) => { + test.slow(); + await new Promise(f => setTimeout(f, 1500)); + }); + + test.describe('suite', () => { + test.slow(); + test('passes2', async ({}, testInfo) => { + expect(testInfo.timeout).toBe(3000); + await new Promise(f => setTimeout(f, 1500)); + }); + }); + ` + }, { timeout: 1000 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.passed).toBe(2); + expect(result.output).toContain('Timeout of 1000ms exceeded'); +}); diff --git a/tests/playwright-test/types-2.spec.ts b/tests/playwright-test/types-2.spec.ts new file mode 100644 index 0000000000..aaaea0b70b --- /dev/null +++ b/tests/playwright-test/types-2.spec.ts @@ -0,0 +1,107 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('basics should work', async ({runTSC}) => { + const result = await runTSC({ + 'a.spec.ts': ` + const { test } = folio; + test.describe('suite', () => { + test.beforeEach(async () => {}); + test('my test', async({}, testInfo) => { + expect(testInfo.title).toBe('my test'); + testInfo.annotations[0].type; + }); + }); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('can pass sync functions everywhere', async ({runTSC}) => { + const result = await runTSC({ + 'a.spec.ts': ` + const test = folio.test.extend<{ foo: string }>({ + foo: ({}, use) => use('bar'), + }); + test.beforeEach(({ foo }) => {}); + test.afterEach(({ foo }) => {}); + test.beforeAll(() => {}); + test.afterAll(() => {}); + test('my test', ({ foo }) => {}); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('can return anything from hooks', async ({runTSC}) => { + const result = await runTSC({ + 'a.spec.ts': ` + const { test } = folio; + test.beforeEach(() => '123'); + test.afterEach(() => 123); + test.beforeAll(() => [123]); + test.afterAll(() => ({ a: 123 })); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('test.declare should check types', async ({runTSC}) => { + const result = await runTSC({ + 'helper.ts': ` + export const test = folio.test; + export const test1 = test.declare<{ foo: string }>(); + export const test2 = test1.extend<{ bar: number }>({ + bar: async ({ foo }, run) => { await run(parseInt(foo)); } + }); + export const test3 = test1.extend<{ bar: number }>({ + // @ts-expect-error + bar: async ({ baz }, run) => { await run(42); } + }); + `, + 'playwright.config.ts': ` + import { test1 } from './helper'; + const configs: folio.Config[] = []; + configs.push({}); + configs.push({ + define: { + test: test1, + fixtures: { foo: 'foo' } + }, + }); + + configs.push({ + // @ts-expect-error + define: { test: {}, fixtures: {} }, + }); + module.exports = configs; + `, + 'a.spec.ts': ` + import { test, test1, test2, test3 } from './helper'; + // @ts-expect-error + test('my test', async ({ foo }) => {}); + test1('my test', async ({ foo }) => {}); + // @ts-expect-error + test1('my test', async ({ foo, bar }) => {}); + test2('my test', async ({ foo, bar }) => {}); + // @ts-expect-error + test2('my test', async ({ foo, baz }) => {}); + ` + }); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/types.spec.ts b/tests/playwright-test/types.spec.ts new file mode 100644 index 0000000000..fa01606e97 --- /dev/null +++ b/tests/playwright-test/types.spec.ts @@ -0,0 +1,165 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('sanity', async ({runTSC}) => { + const result = await runTSC({ + 'a.spec.ts': ` + const { test } = folio; + // @ts-expect-error + test.foo(); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('should check types of fixtures', async ({runTSC}) => { + const result = await runTSC({ + 'helper.ts': ` + export type MyOptions = { foo: string, bar: number }; + export const test = folio.test.extend<{ foo: string }, { bar: number }>({ + foo: 'foo', + bar: [ 42, { scope: 'worker' } ], + }); + + const good1 = test.extend<{}>({ foo: async ({ bar }, run) => run('foo') }); + const good2 = test.extend<{}>({ bar: ({}, run) => run(42) }); + const good3 = test.extend<{}>({ bar: ({}, run) => run(42) }); + const good4 = test.extend<{}>({ bar: async ({ bar }, run) => run(42) }); + const good5 = test.extend<{}>({ foo: async ({ foo }, run) => run('foo') }); + const good6 = test.extend<{ baz: boolean }>({ + baz: false, + foo: async ({ baz }, run) => run('foo') + }); + const good7 = test.extend<{ baz: boolean }>({ + baz: [ false, { auto: true } ], + }); + + // @ts-expect-error + const fail1 = test.extend<{}>({ foo: 42 }); + // @ts-expect-error + const fail2 = test.extend<{}>({ bar: async ({ foo }, run) => run(42) }); + // @ts-expect-error + const fail3 = test.extend<{}>({ baz: 42 }); + // @ts-expect-error + const fail4 = test.extend<{}>({ foo: async ({ foo }, run) => run(42) }); + // @ts-expect-error + const fail5 = test.extend<{}>({ bar: async ({}, run) => run('foo') }); + const fail6 = test.extend<{ baz: boolean }>({ + // @ts-expect-error + baz: [ true, { scope: 'worker' } ], + }); + const fail7 = test.extend<{}, { baz: boolean }>({ + // @ts-expect-error + baz: [ true, { scope: 'test' } ], + }); + const fail8 = test.extend<{}, { baz: boolean }>({ + // @ts-expect-error + baz: true, + }); + `, + 'playwright.config.ts': ` + import { MyOptions } from './helper'; + const configs1: folio.Config[] = []; + configs1.push({ use: { foo: '42', bar: 42 } }); + configs1.push({ use: { foo: '42', bar: 42 }, timeout: 100 }); + + const configs2: folio.Config[] = []; + configs2.push({ use: { foo: '42', bar: 42 } }); + // @ts-expect-error + folio.runTests({ use: { foo: '42', bar: 42 } }, {}); + // @ts-expect-error + configs2.push({ use: { bar: '42' } }); + // @ts-expect-error + configs2.push(new Env2()); + // @ts-expect-error + configs2.push({ use: { foo: 42, bar: 42 } }); + // @ts-expect-error + configs2.push({ beforeAll: async () => { return {}; } }); + // TODO: next line should not compile. + configs2.push({ timeout: 100 }); + // @ts-expect-error + configs2.push('alias'); + // TODO: next line should not compile. + configs2.push({}); + `, + 'a.spec.ts': ` + import { test } from './helper'; + test.use({ foo: 'foo' }); + test.use({}); + + // @ts-expect-error + test.use({ foo: 42 }); + // @ts-expect-error + test.use({ baz: 'baz' }); + + test('my test', async ({ foo, bar }) => { + bar += parseInt(foo); + }); + test('my test', ({ foo, bar }) => { + bar += parseInt(foo); + }); + test('my test', () => {}); + // @ts-expect-error + test('my test', async ({ a }) => { + }); + + // @ts-expect-error + test.beforeEach(async ({ a }) => {}); + test.beforeEach(async ({ foo, bar }) => {}); + test.beforeEach(() => {}); + + // @ts-expect-error + test.beforeAll(async ({ a }) => {}); + // @ts-expect-error + test.beforeAll(async ({ foo, bar }) => {}); + test.beforeAll(async ({ bar }) => {}); + test.beforeAll(() => {}); + + // @ts-expect-error + test.afterEach(async ({ a }) => {}); + test.afterEach(async ({ foo, bar }) => {}); + test.afterEach(() => {}); + + // @ts-expect-error + test.afterAll(async ({ a }) => {}); + // @ts-expect-error + test.afterAll(async ({ foo, bar }) => {}); + test.afterAll(async ({ bar }) => {}); + test.afterAll(() => {}); + ` + }); + expect(result.exitCode).toBe(0); +}); + +test('config should allow void/empty options', async ({runTSC}) => { + const result = await runTSC({ + 'playwright.config.ts': ` + const configs: folio.Config[] = []; + configs.push({}); + configs.push({ timeout: 100 }); + configs.push(); + configs.push({ use: { foo: 42 }}); + `, + 'a.spec.ts': ` + const { test } = folio; + test('my test', async () => { + }); + ` + }); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/worker-index.spec.ts b/tests/playwright-test/worker-index.spec.ts new file mode 100644 index 0000000000..8426cec619 --- /dev/null +++ b/tests/playwright-test/worker-index.spec.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should run in parallel', async ({ runInlineTest }) => { + const result = await runInlineTest({ + '1.spec.ts': ` + import * as fs from 'fs'; + import * as path from 'path'; + const { test } = folio; + test('succeeds', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + // First test waits for the second to start to work around the race. + while (true) { + if (fs.existsSync(path.join(testInfo.project.outputDir, 'parallel-index.txt'))) + break; + await new Promise(f => setTimeout(f, 100)); + } + }); + `, + '2.spec.ts': ` + import * as fs from 'fs'; + import * as path from 'path'; + const { test } = folio; + test('succeeds', async ({}, testInfo) => { + // First test waits for the second to start to work around the race. + fs.mkdirSync(testInfo.project.outputDir, { recursive: true }); + fs.writeFileSync(path.join(testInfo.project.outputDir, 'parallel-index.txt'), 'TRUE'); + expect(testInfo.workerIndex).toBe(1); + }); + `, + }); + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); +}); + +test('should reuse worker for multiple tests', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test } = folio; + test('succeeds', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + + test('succeeds', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + + test('succeeds', async ({}, testInfo) => { + expect(testInfo.workerIndex).toBe(0); + }); + `, + }); + expect(result.passed).toBe(3); + expect(result.exitCode).toBe(0); +}); + +test('should not reuse worker for different suites', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [{}, {}, {}] }; + `, + 'a.test.js': ` + const { test } = folio; + test('succeeds', async ({}, testInfo) => { + console.log('workerIndex-' + testInfo.workerIndex); + }); + `, + }); + expect(result.passed).toBe(3); + expect(result.exitCode).toBe(0); + expect(result.results.map(r => r.workerIndex).sort()).toEqual([0, 1, 2]); + expect(result.output).toContain('workerIndex-0'); + expect(result.output).toContain('workerIndex-1'); + expect(result.output).toContain('workerIndex-2'); +}); diff --git a/types/test.d.ts b/types/test.d.ts index 67538c7d27..a08e975cea 100644 --- a/types/test.d.ts +++ b/types/test.d.ts @@ -15,7 +15,8 @@ */ import type { Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials } from './types'; -import type { Project, Config } from 'folio'; +import type { Project, Config, TestType } from './testInternal'; +import type { Expect } from './testExpect'; /** * The name of the browser supported by Playwright. @@ -279,9 +280,7 @@ export type PlaywrightTestArgs = { export type PlaywrightTestProject = Project; export type PlaywrightTestConfig = Config; -export * from 'folio'; - -import type { TestType } from 'folio'; +export type { Project, Config, TestStatus, TestInfo, WorkerInfo, TestType, Fixtures, TestFixture, WorkerFixture } from './testInternal'; /** * These tests are executed in Playwright environment that launches the browser @@ -289,3 +288,5 @@ import type { TestType } from 'folio'; */ export const test: TestType; export default test; + +export const expect: Expect; diff --git a/types/testExpect.d.ts b/types/testExpect.d.ts new file mode 100644 index 0000000000..43ad05351f --- /dev/null +++ b/types/testExpect.d.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type expect from 'expect'; +import type { ExpectedAssertionsErrors } from 'expect/build/types'; + +export declare type AsymmetricMatcher = Record; + +export declare type Expect = { + (actual: T): folio.Matchers; + + // Sourced from node_modules/expect/build/types.d.ts + assertions(arg0: number): void; + extend(arg0: any): void; + extractExpectedAssertionsErrors: () => ExpectedAssertionsErrors; + getState(): expect.MatcherState; + hasAssertions(): void; + setState(state: Partial): void; + any(expectedObject: any): AsymmetricMatcher; + anything(): AsymmetricMatcher; + arrayContaining(sample: Array): AsymmetricMatcher; + objectContaining(sample: Record): AsymmetricMatcher; + stringContaining(expected: string): AsymmetricMatcher; + stringMatching(expected: string | RegExp): AsymmetricMatcher; +}; + +declare global { + export namespace jest { + export interface Matchers extends expect.Matchers { + + } + } + export namespace folio { + export interface Matchers extends jest.Matchers { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: folio.Matchers; + /** + * Use resolves to unwrap the value of a fulfilled promise so any other + * matcher can be chained. If the promise is rejected the assertion fails. + */ + resolves: folio.Matchers>; + /** + * Unwraps the reason of a rejected promise so any other matcher can be chained. + * If the promise is fulfilled the assertion fails. + */ + rejects: folio.Matchers>; + /** + * Match snapshot + */ + toMatchSnapshot(options?: { + name?: string, + threshold?: number + }): R; + /** + * Match snapshot + */ + toMatchSnapshot(name: string, options?: { + threshold?: number + }): R; + } + } +} + +export { }; diff --git a/types/testInternal.d.ts b/types/testInternal.d.ts new file mode 100644 index 0000000000..e973db910b --- /dev/null +++ b/types/testInternal.d.ts @@ -0,0 +1,868 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Expect } from './testExpect'; +export type { Expect } from './testExpect'; + +export type ReporterDescription = + ['dot'] | + ['line'] | + ['list'] | + ['junit'] | ['junit', { outputFile?: string, stripANSIControlSequences?: boolean }] | + ['json'] | ['json', { outputFile?: string }] | + ['null'] | + [string] | [string, any]; + +export type Shard = { total: number, current: number } | null; +export type PreserveOutput = 'always' | 'never' | 'failures-only'; +export type UpdateSnapshots = 'all' | 'none' | 'missing'; + +type FixtureDefine = { test: TestType, fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs> }; + +/** + * Test run configuration. + */ +interface ProjectBase { + /** + * Any JSON-serializable metadata that will be put directly to the test report. + */ + metadata?: any; + + /** + * The project name, shown in the title of each test. + */ + name?: string; + + /** + * Output directory for files created during the test run. + */ + outputDir?: string; + + /** + * The number of times to repeat each test, useful for debugging flaky tests. + */ + repeatEach?: number; + + /** + * The maximum number of retry attempts given to failed tests. + */ + retries?: number; + + /** + * Directory that will be recursively scanned for test files. + */ + testDir?: string; + + /** + * Files matching one of these patterns are not executed as test files. + * Matching is performed against the absolute file path. + * Strings are treated as glob patterns. + */ + testIgnore?: string | RegExp | (string | RegExp)[]; + + /** + * Only the files matching one of these patterns are executed as test files. + * Matching is performed against the absolute file path. + * Strings are treated as glob patterns. + */ + testMatch?: string | RegExp | (string | RegExp)[]; + + /** + * Timeout for each test in milliseconds. + */ + timeout?: number; +} + +/** + * Test run configuration. + */ +export interface Project extends ProjectBase { + /** + * Fixtures defined for abstract tests created with `test.declare()` method. + */ + define?: FixtureDefine | FixtureDefine[]; + + /** + * Fixture overrides for this run. Useful for specifying options. + */ + use?: Fixtures<{}, {}, TestArgs, WorkerArgs>; +} +export type FullProject = Required>; + +/** + * Testing configuration. + */ +interface ConfigBase { + /** + * Whether to exit with an error if any tests are marked as `test.only`. Useful on CI. + */ + forbidOnly?: boolean; + + /** + * Path to the global setup file. This file will be required and run before all the tests. + * It must export a single function. + */ + globalSetup?: string; + + /** + * Path to the global teardown file. This file will be required and run after all the tests. + * It must export a single function. + */ + globalTeardown?: string; + + /** + * Maximum time in milliseconds the whole test suite can run. + */ + globalTimeout?: number; + + /** + * Filter to only run tests with a title matching one of the patterns. + */ + grep?: RegExp | RegExp[]; + + /** + * The maximum number of test failures for this test run. After reaching this number, + * testing will stop and exit with an error. Setting to zero (default) disables this behavior. + */ + maxFailures?: number; + + /** + * Whether to preserve test output in the `outputDir`: + * - `'always'` - preserve output for all tests; + * - `'never'` - do not preserve output for any tests; + * - `'failures-only'` - only preserve output for failed tests. + */ + preserveOutput?: PreserveOutput; + + /** + * Reporter to use. Available options: + * - `'list'` - default reporter, prints a single line per test; + * - `'dot'` - minimal reporter that prints a single character per test run, useful on CI; + * - `'line'` - uses a single line for all successfull runs, useful for large test suites; + * - `'json'` - outputs a json file with information about the run; + * - `'junit'` - outputs an xml file with junit-alike information about the run; + * - `'null'` - no reporter, test run will be silent. + * + * It is possible to pass multiple reporters. A common pattern is using one terminal reporter + * like `'line'` or `'list'`, and one file reporter like `'json'` or `'junit'`. + */ + reporter?: 'dot' | 'line' | 'list' | 'junit' | 'json' | 'null' | ReporterDescription[]; + + /** + * Whether to suppress stdio output from the tests. + */ + quiet?: boolean; + + /** + * Shard tests and execute only the selected shard. + * Specify in the one-based form `{ total: 5, current: 2 }`. + */ + shard?: Shard; + + /** + * Whether to update expected snapshots with the actual results produced by the test run. + */ + updateSnapshots?: UpdateSnapshots; + + /** + * The maximum number of concurrent worker processes to use for parallelizing tests. + */ + workers?: number; +} + +/** + * Testing configuration. + */ +export interface Config extends ConfigBase, Project { + /** + * Projects specify test files that are executed with a specific configuration. + */ + projects?: Project[]; +} + +export interface FullConfig { + forbidOnly: boolean; + globalSetup: string | null; + globalTeardown: string | null; + globalTimeout: number; + grep: RegExp | RegExp[]; + maxFailures: number; + preserveOutput: PreserveOutput; + projects: FullProject[]; + reporter: ReporterDescription[]; + rootDir: string; + quiet: boolean; + shard: Shard; + updateSnapshots: UpdateSnapshots; + workers: number; +} + +export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped'; + +/** + * Information common for all tests run in the same worker process. + */ +export interface WorkerInfo { + /** + * Testing configuration. + */ + config: FullConfig; + + /** + * Specific project configuration for this worker. + * Different projects are always run in separate processes. + */ + project: FullProject; + + /** + * Unique worker index. Also available as `process.env.TEST_WORKER_INDEX`. + */ + workerIndex: number; +} + +/** + * Information about a particular test run. + */ +export interface TestInfo extends WorkerInfo { + /** + * Test title as passed to `test('my test title', testFunction)`. + */ + title: string; + + /** + * Path to the file where test is declared. + */ + file: string; + + /** + * Line number in the test file where the test is declared. + */ + line: number; + + /** + * Column number in the test file where the test is declared. + */ + column: number; + + /** + * The test function as passed to `test('my test title', testFunction)`. + */ + fn: Function; + + /** + * Call this method to skip the current test. + */ + skip(): void; + skip(condition: boolean): void; + skip(condition: boolean, description: string): void; + + /** + * Call this method to mark the current test as "needs to be fixed". The test will not be run. + */ + fixme(): void; + fixme(condition: boolean): void; + fixme(condition: boolean, description: string): void; + + /** + * Call this method to mark the current test as "expected to fail". The test will be run and must fail. + */ + fail(): void; + fail(condition: boolean): void; + fail(condition: boolean, description: string): void; + + /** + * Call this method to mark the current test as slow. The default timeout will be trippled. + */ + slow(): void; + slow(condition: boolean): void; + slow(condition: boolean, description: string): void; + + /** + * Call this method to set a custom timeout for the current test. + */ + setTimeout(timeout: number): void; + + /** + * The expected status for the test: + * - `'passed'` for most tests; + * - `'failed'` for tests marked with `test.fail()`; + * - `'skipped'` for tests marked with `test.skip()` or `test.fixme()`. + */ + expectedStatus: TestStatus; + + /** + * Timeout in milliseconds for this test. + */ + timeout: number; + + /** + * Annotations collected for this test. + */ + annotations: { type: string, description?: string }[]; + + /** + * When tests are run multiple times, each run gets a unique `repeatEachIndex`. + */ + repeatEachIndex: number; + + /** + * When the test is retried after a failure, `retry` indicates the attempt number. + * Zero for the first (non-retry) run. + * + * The maximum number of retries is configurable with `retries` field in the config. + */ + retry: number; + + /** + * The number of milliseconds this test took to finish. + * Only available after the test has finished. + */ + duration: number; + + /** + * The result of the run. + * Only available after the test has finished. + */ + status?: TestStatus; + + /** + * The error thrown by the test if any. + * Only available after the test has finished. + */ + error?: any; + + /** + * Output written to `process.stdout` or `console.log` from the test. + * Only available after the test has finished. + */ + stdout: (string | Buffer)[]; + + /** + * Output written to `process.stderr` or `console.error` from the test. + * Only available after the test has finished. + */ + stderr: (string | Buffer)[]; + + /** + * Suffix used to differentiate snapshots between multiple test configurations. + * For example, if snapshots depend on the platform, you can set `testInfo.snapshotSuffix = process.platform`, + * and `expect(value).toMatchSnapshot(snapshotName)` will use different snapshots depending on the platform. + */ + snapshotSuffix: string; + + /** + * Absolute path to the output directory for this specific test run. + * Each test gets its own directory. + */ + outputDir: string; + + /** + * Returns a path to a snapshot file. + */ + snapshotPath: (snapshotName: string) => string; + + /** + * Returns a path inside the `outputDir` where the test can safely put a temporary file. + * Guarantees that tests running in parallel will not interfere with each other. + * + * ```js + * const file = testInfo.outputPath('temporary-file.txt'); + * await fs.promises.writeFile(file, 'Put some data to the file', 'utf8'); + * ``` + */ + outputPath: (...pathSegments: string[]) => string; +} + +interface SuiteFunction { + (name: string, inner: () => void): void; +} + +interface TestFunction { + (name: string, inner: (args: TestArgs, testInfo: TestInfo) => Promise | void): void; +} + +/** + * Call this function to declare a test. + * + * ```js + * test('my test title', async () => { + * // Test code goes here. + * }); + * ``` + */ +export interface TestType extends TestFunction { + /** + * Use `test.only()` instead of `test()` to ignore all other tests and only run this one. + * Useful for debugging a particular test. + * + * ```js + * test.only('my test title', async () => { + * // Only this test will run. + * }); + * ``` + * + * All tests marked as `test.only()` will be run, so you can mark multiple of them. + */ + only: TestFunction; + + /** + * Declare a block of related tests. + * + * ```js + * test.decribe('my test suite', () => { + * test('one test', async () => { + * // Test code goes here. + * }); + * + * test('another test', async () => { + * // Test code goes here. + * }); + * }); + * ``` + * + * Any `beforeEach`, `afterEach`, `beforeAll` and `afterAll` hooks declared inside the `test.decribe()` block + * will only affect the tests from this block. + */ + describe: SuiteFunction & { + /** + * Use `test.describe.only()` instead of `test.describe()` to ignore all other tests and only run this block. + * Useful for debugging a few tests. + */ + only: SuiteFunction; + }; + + /** + * Skip running this test. + * + * ```js + * test('my test title', async () => { + * test.skip(); + * // Test code goes here. It will not be executed. + * }); + * ``` + */ + skip(): void; + + /** + * Skip running this test when `condition` is true. + * + * ```js + * test('my test title', async () => { + * test.skip(process.platform === 'darwin'); + * // Test code goes here. It will not be executed on MacOS. + * }); + * ``` + */ + skip(condition: boolean): void; + + /** + * Skip running this test when `condition` is true. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test('my test title', async () => { + * test.skip(process.platform === 'darwin', 'Dependency "foo" is crashing on MacOS'); + * // Test code goes here. It will not be executed on MacOS. + * }); + * ``` + */ + skip(condition: boolean, description: string): void; + + /** + * Skip running tests in the `describe` block based on some condition. + * + * ```js + * test.describe('my tests', () => { + * test.skip(() => process.platform === 'darwin'); + * + * // Declare tests below - they will not be executed on MacOS. + * }); + * ``` + */ + skip(callback: (args: TestArgs & WorkerArgs) => boolean): void; + + /** + * Skip running tests in the `describe` block based on some condition. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test.describe('my tests', () => { + * test.skip(() => process.platform === 'darwin', 'Dependency "foo" is crashing on MacOS'); + * + * // Declare tests below - they will not be executed on MacOS. + * }); + * ``` + */ + skip(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void; + + /** + * Skip running this test, with intention to fix it later. + * + * ```js + * test('my test title', async () => { + * test.fixme(); + * // Test code goes here. It will not be executed. + * }); + * ``` + */ + fixme(): void; + + /** + * Skip running this test when `condition` is true, with intention to fix it later. + * + * ```js + * test('my test title', async () => { + * test.fixme(process.platform === 'darwin'); + * // Test code goes here. It will not be executed on MacOS. + * }); + * ``` + */ + fixme(condition: boolean): void; + + /** + * Skip running this test when `condition` is true, with intention to fix it later. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test('my test title', async () => { + * test.fixme(process.platform === 'darwin', 'Dependency "foo" is crashing on MacOS'); + * // Test code goes here. It will not be executed on MacOS. + * }); + * ``` + */ + fixme(condition: boolean, description: string): void; + + /** + * Skip running tests in the `describe` block based on some condition, with intention to fix it later. + * + * ```js + * test.describe('my tests', () => { + * test.fixme(() => process.platform === 'darwin'); + * + * // Declare tests below - they will not be executed on MacOS. + * }); + * ``` + */ + fixme(callback: (args: TestArgs & WorkerArgs) => boolean): void; + + /** + * Skip running tests in the `describe` block based on some condition, with intention to fix it later. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test.describe('my tests', () => { + * test.fixme(() => process.platform === 'darwin', 'Dependency "foo" is crashing on MacOS'); + * + * // Declare tests below - they will not be executed on MacOS. + * }); + * ``` + */ + fixme(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void; + + /** + * Mark the test as "expected to fail". It will be run and should fail. + * When "expected to fail" test acceidentally passes, test runner will exit with an error. + * + * ```js + * test('my test title', async () => { + * test.fail(); + * // Test code goes here. + * }); + * ``` + */ + fail(): void; + + /** + * Mark the test as "expected to fail", when `condition` is true. It will be run and should fail. + * When "expected to fail" test acceidentally passes, test runner will exit with an error. + * + * ```js + * test('my test title', async () => { + * test.fail(process.platform === 'darwin'); + * // Test code goes here. It should fail on MacOS. + * }); + * ``` + */ + fail(condition: boolean): void; + + /** + * Mark the test as "expected to fail", when `condition` is true. It will be run and should fail. + * When "expected to fail" test acceidentally passes, test runner will exit with an error. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test('my test title', async () => { + * test.fail(process.platform === 'darwin', 'Could not find resources - see issue #1234'); + * // Test code goes here. It should fail on MacOS. + * }); + * ``` + */ + fail(condition: boolean, description: string): void; + + /** + * Mark tests in the `describe` block as "expected to fail" based on some condition. + * The tests will be run and should fail. + * When "expected to fail" test acceidentally passes, test runner will exit with an error. + * + * ```js + * test.describe('my tests', () => { + * test.fail(() => process.platform === 'darwin'); + * + * // Declare tests below - they should fail on MacOS. + * }); + * ``` + */ + fail(callback: (args: TestArgs & WorkerArgs) => boolean): void; + + /** + * Mark tests in the `describe` block as "expected to fail" based on some condition. + * The tests will be run and should fail. + * When "expected to fail" test acceidentally passes, test runner will exit with an error. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test.describe('my tests', () => { + * test.fail(() => process.platform === 'darwin', 'Could not find resources - see issue #1234'); + * + * // Declare tests below - they should fail on MacOS. + * }); + * ``` + */ + fail(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void; + + /** + * Triples the default timeout for this test. + * + * ```js + * test('my test title', async () => { + * test.slow(); + * // Test code goes here. + * }); + * ``` + */ + slow(): void; + + /** + * Triples the default timeout for this test, when `condition` is true. + * + * ```js + * test('my test title', async () => { + * test.slow(process.platform === 'darwin'); + * // Test code goes here. It will be given triple timeout on MacOS. + * }); + * ``` + */ + slow(condition: boolean): void; + + /** + * Triples the default timeout for this test, when `condition` is true. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test('my test title', async () => { + * test.slow(process.platform === 'darwin', 'Dependency "foo" is slow on MacOS'); + * // Test code goes here. It will be given triple timeout on MacOS. + * }); + * ``` + */ + slow(condition: boolean, description: string): void; + + /** + * Give all tests in the `describe` block triple timeout, based on some condition. + * + * ```js + * test.describe('my tests', () => { + * test.slow(() => process.platform === 'darwin'); + * + * // Declare tests below - they will be given triple timeout on MacOS. + * }); + * ``` + */ + slow(callback: (args: TestArgs & WorkerArgs) => boolean): void; + + /** + * Give all tests in the `describe` block triple timeout, based on some condition. + * Put a reason in `description` to easily remember it later. + * + * ```js + * test.describe('my tests', () => { + * test.slow(() => process.platform === 'darwin', 'Dependency "foo" is slow on MacOS'); + * + * // Declare tests below - they will be given triple timeout on MacOS. + * }); + * ``` + */ + slow(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void; + + /** + * Set a custom timeout for the test. + * + * ```js + * test('my test title', async () => { + * // Give this test 20 seconds. + * test.setTimeout(20000); + * // Test code goes here. + * }); + * ``` + */ + setTimeout(timeout: number): void; + + /** + * Declare a hook that will be run before each test. + * It may use all the available fixtures. + * + * ```js + * test.beforeEach(async ({ fixture }, testInfo) => { + * // Do some work here. + * }); + * ``` + * + * When called inside a `test.describe()` block, the hook only applies to the tests from the block. + */ + beforeEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; + + /** + * Declare a hook that will be run after each test. + * It may use all the available fixtures. + * + * ```js + * test.afterEach(async ({ fixture }, testInfo) => { + * // Do some work here. + * }); + * ``` + * + * When called inside a `test.describe()` block, the hook only applies to the tests from the block. + */ + afterEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; + + /** + * Declare a hook that will be run once before all tests in the file. + * It may use all worker-scoped fixtures. + * + * ```js + * test.beforeAll(async ({ workerFixture }, workerInfo) => { + * // Do some work here. + * }); + * ``` + * + * When called inside a `test.describe()` block, the hook only applies to the tests from the block. + */ + beforeAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise | any): void; + + /** + * Declare a hook that will be run once after all tests in the file. + * It may use all worker-scoped fixtures. + * + * ```js + * test.afterAll(async ({ workerFixture }, workerInfo) => { + * // Do some work here. + * }); + * ``` + * + * When called inside a `test.describe()` block, the hook only applies to the tests from the block. + */ + afterAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise | any): void; + + /** + * Declare fixtures/options to be used for tests in this file. + * + * ```js + * test.use({ myOption: 'foo' }); + * + * test('my test title', async ({ myFixtureThatUsesMyOption }) => { + * // Test code goes here. + * }); + * ``` + * + * When called inside a `test.describe()` block, fixtures/options only apply to the tests from the block. + */ + use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; + + /** + * Use `test.expect(value).toBe(expected)` to assert something in the test. + * See [expect library](https://jestjs.io/docs/expect) documentation for more details. + */ + expect: Expect; + + declare(): TestType; + + /** + * Extend the test with fixtures. These fixtures will be invoked for test when needed, + * can perform setup/teardown and provide a resource to the test. + * + * ```ts + * import base from 'folio'; + * import rimraf from 'rimraf'; + * + * const test = base.extend<{ dirCount: number, dirs: string[] }>({ + * // Define an option that can be configured in tests with `test.use()`. + * // Provide a default value. + * dirCount: 1, + * + * // Define a fixture that provides some useful functionality to the test. + * // In this example, it will create some temporary directories. + * dirs: async ({ dirCount }, use, testInfo) => { + * // Our fixture uses the "dirCount" option that can be configured by the test. + * const dirs = []; + * for (let i = 0; i < dirCount; i++) { + * // Create an isolated directory. + * const dir = testInfo.outputPath('dir-' + i); + * await fs.promises.mkdir(dir, { recursive: true }); + * dirs.push(dir); + * } + * + * // Use the list of directories in the test. + * await use(dirs); + * + * // Cleanup if needed. + * for (const dir of dirs) + * await new Promise(done => rimraf(dir, done)); + * }, + * }); + * + * + * // Tests in this file need two temporary directories. + * test.use({ dirCount: 2 }); + * + * test('my test title', async ({ dirs }) => { + * // Test code goes here. + * // It can use "dirs" right away - the fixture has already run and created two temporary directories. + * }); + * ``` + */ + extend(fixtures: Fixtures): TestType; +} + +type KeyValue = { [key: string]: any }; +export type TestFixture = (args: Args, use: (r: R) => Promise, testInfo: TestInfo) => any; +export type WorkerFixture = (args: Args, use: (r: R) => Promise, workerInfo: WorkerInfo) => any; +type TestFixtureValue = R | TestFixture; +type WorkerFixtureValue = R | WorkerFixture; +export type Fixtures = { + [K in keyof PW]?: WorkerFixtureValue; +} & { + [K in keyof PT]?: TestFixtureValue; +} & { + [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean }]; +} & { + [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean }]; +}; + +export type Location = { file: string, line: number, column: number }; +export type FixturesWithLocation = { + fixtures: Fixtures; + location: Location; +}; diff --git a/utils/check_deps.js b/utils/check_deps.js index 3b12c67d01..5907e38513 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -163,6 +163,9 @@ DEPS['src/server/trace/common/'] = ['src/server/snapshot/', ...DEPS['src/server/ DEPS['src/server/trace/recorder/'] = ['src/server/trace/common/', ...DEPS['src/server/trace/common/']]; DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/', ...DEPS['src/server/trace/common/']]; +// No dependencies for test runner. +DEPS['src/test/'] = ['src/test/**']; + checkDeps().catch(e => { console.error(e && e.stack ? e.stack : e); process.exit(1);