diff --git a/.gitignore b/.gitignore index 7350c12..7205149 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules/ /test-results/ cucumber-report.html assets -features @rerun.txt index.html +browser-details.json +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..add5956 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + // Configure project-level settings for the official Cucumber extension in Visual Studio Code. + // The settings now include subdirectories under 'features' and 'steps' + "cucumber.features": [ + "src/test/features/**/*.feature" + ], + "cucumber.glue": [ + "src/test/steps/**/*.ts" + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb103e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Koushik Chatterjee and Alexandru Sicoe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/config/cucumber.js b/config/cucumber.js index 26afa85..155600f 100644 --- a/config/cucumber.js +++ b/config/cucumber.js @@ -1,48 +1,43 @@ +const tags = process.env.npm_config_tags || ""; + +const commonConfig = { + formatOptions: { + snippetInterface: "async-await" + }, + paths: [ + "src/test/features/**/*" + ], + publishQuiet: true, + dryRun: false, + require: [ + "src/test/steps/**/*.ts", + "src/hooks/hooks.ts" + ], + requireModule: [ + "ts-node/register" + ], + format: [ + "progress-bar", + "html:test-results/cucumber-report.html", + "json:test-results/cucumber-report.json", + "rerun:@rerun.txt" + ], +}; + module.exports = { default: { - tags: process.env.npm_config_TAGS || "", - formatOptions: { - snippetInterface: "async-await" - }, - paths: [ - "src/test/features/" - ], - publishQuiet: true, - dryRun: false, - require: [ - "src/test/steps/*.ts", - "src/hooks/hooks.ts" - ], - requireModule: [ - "ts-node/register" - ], - format: [ - "progress-bar", - "html:test-results/cucumber-report.html", - "json:test-results/cucumber-report.json", - "rerun:@rerun.txt" - ], - parallel: 1 + ...commonConfig, + tags: tags, + parallel: 2, + }, + debug: { + ...commonConfig, + tags: `${tags ? `@debug and ${tags}` : '@debug'}`, + parallel: 1, }, rerun: { - formatOptions: { - snippetInterface: "async-await" - }, - publishQuiet: true, - dryRun: false, - require: [ - "src/test/steps/*.ts", - "src/hooks/hooks.ts" - ], - requireModule: [ - "ts-node/register" - ], - format: [ - "progress-bar", - "html:test-results/cucumber-report.html", - "json:test-results/cucumber-report.json", - "rerun:@rerun.txt" - ], + ...commonConfig, + paths: [], parallel: 2 } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9a27d2b..91cba0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,16 @@ "devDependencies": { "@cucumber/cucumber": "^9.0.1", "@playwright/test": "1.35.0", + "@types/fs-extra": "^11.0.4", + "chalk": "^4.1.2", + "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.0.3", + "express": "^4.19.2", "fs-extra": "^11.1.1", + "ms": "^2.1.3", "multiple-cucumber-html-reporter": "^3.3.0", + "opn": "^6.0.0", "ts-node": "^10.9.1", "winston": "^3.8.2" } @@ -326,6 +332,25 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.191", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", @@ -350,6 +375,19 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -407,6 +445,12 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -439,6 +483,45 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -455,6 +538,34 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", @@ -593,12 +704,61 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -654,13 +814,46 @@ } } }, - "node_modules/define-lazy-prop": { + "node_modules/debug/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==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "engines": { - "node": ">=8" + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, "node_modules/diff": { @@ -681,6 +874,12 @@ "node": ">=12" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -693,6 +892,15 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -702,6 +910,33 @@ "stackframe": "^1.3.4" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -711,6 +946,72 @@ "node": ">=0.8.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/extsprintf": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", @@ -741,6 +1042,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/find": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", @@ -756,6 +1090,24 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "dev": true }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -790,6 +1142,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -825,6 +1205,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -852,6 +1244,82 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -886,27 +1354,21 @@ "node": ">=10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "dev": true }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -953,18 +1415,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1072,22 +1522,79 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">= 0.6" } }, - "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/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/multiple-cucumber-html-reporter": { @@ -1105,6 +1612,42 @@ "uuid": "^9.0.0" } }, + "node_modules/multiple-cucumber-html-reporter/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/multiple-cucumber-html-reporter/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multiple-cucumber-html-reporter/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/multiple-cucumber-html-reporter/node_modules/luxon": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", @@ -1114,6 +1657,23 @@ "node": ">=12" } }, + "node_modules/multiple-cucumber-html-reporter/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1131,6 +1691,15 @@ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -1150,6 +1719,27 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1168,21 +1758,26 @@ "fn.name": "1.x.x" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "deprecated": "The package has been renamed to `open`", "dev": true, "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "is-wsl": "^1.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" } }, "node_modules/pad-right": { @@ -1197,6 +1792,15 @@ "node": ">=0.10.0" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1215,6 +1819,12 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "node_modules/playwright-core": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.0.tgz", @@ -1242,6 +1852,58 @@ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==", "dev": true }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1360,6 +2022,12 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/seed-random": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", @@ -1381,6 +2049,83 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1402,6 +2147,24 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -1445,6 +2208,15 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1552,6 +2324,15 @@ "node": ">=8.17.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -1619,6 +2400,19 @@ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", "dev": true }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", @@ -1642,6 +2436,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/upper-case-first": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", @@ -1663,6 +2466,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -1678,6 +2490,15 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -2063,6 +2884,25 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "requires": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.191", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", @@ -2087,6 +2927,16 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -2126,6 +2976,12 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -2155,6 +3011,43 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2171,6 +3064,25 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, "capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", @@ -2293,12 +3205,49 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, + "cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2332,12 +3281,37 @@ "dev": true, "requires": { "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, - "define-lazy-prop": { + "depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true }, "diff": { @@ -2352,6 +3326,12 @@ "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", "dev": true }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2364,6 +3344,12 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, "error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -2373,12 +3359,95 @@ "stackframe": "^1.3.4" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "extsprintf": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", @@ -2400,6 +3469,38 @@ "escape-string-regexp": "^1.0.5" } }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "find": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", @@ -2415,6 +3516,18 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "dev": true }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, "fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -2439,6 +3552,25 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2462,6 +3594,15 @@ "ini": "2.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2483,6 +3624,58 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -2511,18 +3704,18 @@ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "dev": true }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2551,15 +3744,6 @@ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2659,6 +3843,45 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2669,9 +3892,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "multiple-cucumber-html-reporter": { @@ -2689,11 +3912,43 @@ "uuid": "^9.0.0" }, "dependencies": { + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "luxon": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", "dev": true + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } } } }, @@ -2714,6 +3969,12 @@ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, "no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -2730,6 +3991,21 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2748,15 +4024,21 @@ "fn.name": "1.x.x" } }, - "open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", "dev": true, "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true + } } }, "pad-right": { @@ -2768,6 +4050,12 @@ "repeat-string": "^1.5.2" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2780,6 +4068,12 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "playwright-core": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.0.tgz", @@ -2798,6 +4092,43 @@ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==", "dev": true }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2878,6 +4209,12 @@ "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", "dev": true }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "seed-random": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", @@ -2893,6 +4230,78 @@ "lru-cache": "^6.0.0" } }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2908,6 +4317,18 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -2945,6 +4366,12 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3030,6 +4457,12 @@ "rimraf": "^3.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -3075,6 +4508,16 @@ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typescript": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", @@ -3088,6 +4531,12 @@ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, "upper-case-first": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", @@ -3109,6 +4558,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, "uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -3121,6 +4576,12 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, "verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", diff --git a/package.json b/package.json index 26d9246..d30687d 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,14 @@ "description": "Playwright Cucumber TS framework - The easiest way to learn", "main": "index.js", "scripts": { - "debug": "cross-env ENV=prod PWDEBUG=1 DEBUG=pw:api cucumber-js --config=config/cucumber.js", + "debug": "cross-env ENV=prod PWDEBUG=1 DEBUG=pw:api cucumber-js -p debug --config=config/cucumber.js", "pretest": "npx ts-node src/helper/report/init.ts", "test": "cross-env ENV=prod FORCE_COLOR=0 cucumber-js --config=config/cucumber.js || true", "posttest": "npx ts-node src/helper/report/report.ts", - "test:failed": "cucumber-js -p rerun @rerun.txt" + "test:failed": "cross-env ENV=prod cucumber-js -p rerun --config=config/cucumber.js @rerun.txt", + "test:only": "npm run test --tags='@only'", + "test:ignore": "npm run test --tags='not @ignore'", + "open:report": "npx ts-node src/helper/report/report-server.ts" }, "keywords": [ "cucumber", @@ -20,11 +23,17 @@ "devDependencies": { "@cucumber/cucumber": "^9.0.1", "@playwright/test": "1.35.0", + "@types/fs-extra": "^11.0.4", + "chalk": "^4.1.2", + "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.0.3", + "express": "^4.19.2", "fs-extra": "^11.1.1", + "ms": "^2.1.3", "multiple-cucumber-html-reporter": "^3.3.0", + "opn": "^6.0.0", "ts-node": "^10.9.1", "winston": "^3.8.2" } -} \ No newline at end of file +} diff --git a/readme.md b/readme.md index 1893dae..8fa4c93 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,8 @@ TypeScript is a powerful superset of JavaScript that adds optional static typing 5. Retry failed tests on CI 6. Github Actions integrated with downloadable report 7. Page object model +8. Open Playwright trace files directly from the hosted report +9. Multiple tab support - see `PageManager` class. ## Sample report ![image](https://github.com/ortoniKC/Playwright_Cucumber_TS/assets/58769833/da2d9f5a-85e7-4695-8ce2-3378b692afc4) @@ -47,10 +49,14 @@ TypeScript is a powerful superset of JavaScript that adds optional static typing "src/test/features/featurename.feature" ] ``` -7. Use tags to run a specific or collection of specs -``` -npm run test --TAGS="@test or @add" -``` +7. Use tags to run a specific or collection of specs: `npm run test --tags="@test or @add"` +8. Run on a specific browser: `npm run test --browser="firefox"` +9. To debug tests, use the `npm run debug` command. This will run tests in debug mode. +10. After running tests, you can view a report using the `npm run open:report` +11. If tests fail, you can rerun them using `npm run test:failed` +12. To run only the tests tagged with `@only`, use the `npm run test:only` +13. To run all tests except those tagged with `@ignore`, use the `npm run test:ignore` + ### Folder structure 0. `src\pages` -> All the page (UI screen) @@ -69,3 +75,54 @@ npm run test --TAGS="@test or @add" ## Tutorials 1. Learn Playwright - [Playwright - TS](https://youtube.com/playlist?list=PL699Xf-_ilW7EyC6lMuU4jelKemmS6KgD) 2. BDD in detail - [TS binding](https://youtube.com/playlist?list=PL699Xf-_ilW6KgK-S1l9ynOnBGiZl2Bsk) + +## Test Statuses and Reporting +Initially, the code was set to capture screenshots, videos, and trace files only for scenarios with +`result.status === 'PASSED'`. +However, to provide more comprehensive reporting capabilities, the code has been updated to handle various Cucumber statuses and capture the appropriate test artifacts accordingly. + +### Cucumber Statuses +Cucumber provides several statuses that indicate the outcome of a scenario or step during test execution. Here's a brief overview of each status: + +1. __PASSED__: + - __*Meaning*__: The scenario or step has executed successfully without any failures. + - __*Test Execution*__: The test runs to completion. +2. __FAILED__: + - __*Meaning*__: The scenario or step has encountered a failure during execution. This could be due to an assertion failure, exception, or an explicit failure step. + - __*Test Execution*__: The test runs but encounters a failure. +3. __PENDING__: + - __*Meaning*__: The scenario or step has a pending implementation. It indicates that the step definition for the corresponding step is missing or not yet implemented. + - __*Test Execution*__: The test does not run for the pending steps. +4. __SKIPPED__: + - __*Meaning*__: The scenario or step has been skipped explicitly using tags or annotations. It is intentionally not executed. + __*Test Execution*__: The test does not run for the skipped scenarios or steps. +5. __UNDEFINED__: + - __*Meaning*__: The scenario or step does not have a corresponding step definition. It indicates that the step implementation is missing. + - __*Test Execution*__: The test does not run for the undefined steps. +6. __AMBIGUOUS__: + - __*Meaning*__: There are multiple step definitions that match a given step in a scenario. Cucumber cannot determine which step definition to execute. + - __*Test Execution*__: The test execution is halted when an ambiguous step is encountered. +7. __UNKNOWN__: + - __*Meaning*__: This status is used when Cucumber encounters an unknown or unexpected situation during test execution. + - __*Test Execution*__: The behavior may vary depending on the specific circumstances that led to the UNKNOWN status. + +### Policy for capturing screenshots, videos, trace files +Based on the Cucumber statuses, the following policy has been implemented for capturing screenshots, videos, and trace files: + +- __PASSED__ and __FAILED__: Capture screenshots, videos, and trace files. +- __UNKNOWN__: Capture screenshots, videos, and trace files to aid in investigating the unexpected situation. +- __PENDING__ or __SKIPPED__: Skip capturing screenshots, videos, and trace files. +- __UNDEFINED__ or __AMBIGUOUS__: Skip capturing screenshots and videos, but capture trace files for debugging purposes. + +Feel free to modify this behaviour in `hooks.ts` / `ArtifactManager.ts` as per your project needs + +### Disabling Screenshots, Video, Logging, and Tracing + +You can disable screenshots, video recording, logging, and tracing on a per-test basis using the following tags: + +- `@disable:screenshot`: Disables screenshot capturing for the tagged test. +- `@disable:video`: Disables video recording for the tagged test. +- `@disable:log`: Disables logging for the tagged test. +- `@disable:trace`: Disables tracing for the tagged test. + +To use these tags, simply add them to your feature / scenario in your `.feature` file. diff --git a/src/helper/browsers/browserManager.ts b/src/helper/browsers/browserManager.ts index 38552cb..654b07f 100644 --- a/src/helper/browsers/browserManager.ts +++ b/src/helper/browsers/browserManager.ts @@ -1,10 +1,14 @@ import { LaunchOptions, chromium, firefox, webkit } from "@playwright/test"; +import * as fs from 'fs-extra'; + +export const browserDetailsPath = 'browser-details.json'; + const options: LaunchOptions = { headless: !true } -export const invokeBrowser = () => { - const browserType = process.env.npm_config_BROWSER || "chrome"; + +function launchBrowser(browserType) { switch (browserType) { case "chrome": return chromium.launch(options); @@ -15,5 +19,16 @@ export const invokeBrowser = () => { default: throw new Error("Please set the proper browser!") } +} + +export async function invokeBrowser() { + const browserType = process.env.npm_config_browser || "chrome"; + let browser = await launchBrowser(browserType) + let browserDetails = { + name: browserType, + version: browser.version() + } + await fs.writeJson(browserDetailsPath, browserDetails); + return browser } \ No newline at end of file diff --git a/src/helper/env/.env.prod b/src/helper/env/.env.prod index 642773d..c97284e 100644 --- a/src/helper/env/.env.prod +++ b/src/helper/env/.env.prod @@ -1,3 +1,4 @@ BASEURL = https://bookcart.azurewebsites.net/ +REPORT_PORT = 3000 BROWSER = chrome HEAD = true \ No newline at end of file diff --git a/src/helper/env/.env.staging b/src/helper/env/.env.staging index ec518d3..37587cb 100644 --- a/src/helper/env/.env.staging +++ b/src/helper/env/.env.staging @@ -1,3 +1,4 @@ BASEURL = https://staging.bookcart.azurewebsites.net/ +REPORT_PORT = 3000 BROWSER = chrome HEAD = true \ No newline at end of file diff --git a/src/helper/env/env.ts b/src/helper/env/env.ts index e2fc42d..0e9b87b 100644 --- a/src/helper/env/env.ts +++ b/src/helper/env/env.ts @@ -10,4 +10,5 @@ export const getEnv = () => { console.error("NO ENV PASSED!") } + process.env.REPORT_PORT = process.env.REPORT_PORT || '3000'; } \ No newline at end of file diff --git a/src/helper/report/init.ts b/src/helper/report/init.ts index 2d03a58..641e104 100644 --- a/src/helper/report/init.ts +++ b/src/helper/report/init.ts @@ -1,8 +1,10 @@ -const fs = require("fs-extra"); +import * as fs from 'fs-extra'; +import { browserDetailsPath } from '../browsers/browserManager'; + try { fs.ensureDir("test-results"); fs.emptyDir("test-results"); - + fs.remove(browserDetailsPath) } catch (error) { console.log("Folder not created! " + error); } \ No newline at end of file diff --git a/src/helper/report/report-server.ts b/src/helper/report/report-server.ts new file mode 100644 index 0000000..2dfdff6 --- /dev/null +++ b/src/helper/report/report-server.ts @@ -0,0 +1,46 @@ +import * as chalk from 'chalk'; +import * as express from 'express'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as cors from 'cors'; +import { getEnv } from '../env/env'; +import * as opn from 'opn' + +getEnv(); + +const app = express(); +app.use(cors({ + origin: 'https://trace.playwright.dev', +})); + +const traceDirPath = path.join(__dirname, '../../../test-results/trace'); +const reportDirPath = path.join(__dirname, '../../../test-results/reports'); + +app.use('/report', express.static(reportDirPath)); + +app.get('/', (req, res) => { + res.redirect('/report'); +}); + +// Custom middleware for downloading specific trace files +app.get('/trace/:filename', (req, res, next) => { + const filePath = path.join(traceDirPath, req.params.filename); + if (fs.existsSync(filePath) && filePath.endsWith('.zip')) { + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename=${req.params.filename}`); + res.sendFile(filePath); + } else { + next(); + } +}); + +const port = process.env.REPORT_PORT +app.listen(port, () => { + console.log(chalk.green('=====================================================================================')); + console.log('\t\tšŸš€ View test report at: ' + chalk.blue(`http://localhost:${port}/report/ šŸš€`)); + console.log(chalk.green('=====================================================================================')); + + console.log('\n' + 'Press CTRL+C to stop the server') + '\n'; + + opn(`http://localhost:${port}/report/`); +}); \ No newline at end of file diff --git a/src/helper/report/report.ts b/src/helper/report/report.ts index 7d23311..4e8e378 100644 --- a/src/helper/report/report.ts +++ b/src/helper/report/report.ts @@ -1,4 +1,27 @@ -const report = require("multiple-cucumber-html-reporter"); +import * as report from 'multiple-cucumber-html-reporter'; +import * as os from 'os'; +import * as fs from 'fs-extra'; +import { browserDetailsPath } from '../browsers/browserManager'; +import * as chalk from 'chalk'; + +const browserDetails = fs.existsSync(browserDetailsPath) + ? fs.readJsonSync(browserDetailsPath) + : { + name: 'N/A', + version: 'N/A' + }; + +// Converts platform name to report format to display proper OS logo +function translatePlatformName(platform): string { + switch (platform) { + case 'darwin': + return 'osx'; + case 'win32': + return 'windows'; + default: + return platform; + } +} report.generate({ jsonDir: "test-results", @@ -8,13 +31,13 @@ report.generate({ displayDuration: false, metadata: { browser: { - name: "chrome", - version: "112", + name: browserDetails.name, + version: browserDetails.version, }, - device: "Koushik - PC", + device: os.hostname(), platform: { - name: "Windows", - version: "10", + name: translatePlatformName(os.platform()), + version: os.release() }, }, customData: { @@ -25,4 +48,6 @@ report.generate({ { label: "Cycle", value: "Smoke-1" } ], }, -}); \ No newline at end of file +}); + +console.log('All tests have run. To view the report, run the following command: ' + chalk.red('npm run open:report') + '\n'); \ No newline at end of file diff --git a/src/helper/types/env.d.ts b/src/helper/types/env.d.ts index fa60884..fea9227 100644 --- a/src/helper/types/env.d.ts +++ b/src/helper/types/env.d.ts @@ -6,7 +6,8 @@ declare global { BROWSER: "chrome" | "firefox" | "webkit", ENV: "staging" | "prod" | "test", BASEURL: string, - HEAD: "true" | "false" + HEAD: "true" | "false", + REPORT_PORT: string } } } \ No newline at end of file diff --git a/src/helper/util/helpers.ts b/src/helper/util/helpers.ts new file mode 100644 index 0000000..f470540 --- /dev/null +++ b/src/helper/util/helpers.ts @@ -0,0 +1,3 @@ +export function normalizeUrl(url: string): string { + return new URL(url).href; +} \ No newline at end of file diff --git a/src/helper/wrapper/PlaywrightWrappers.ts b/src/helper/wrapper/PlaywrightWrapper.ts similarity index 100% rename from src/helper/wrapper/PlaywrightWrappers.ts rename to src/helper/wrapper/PlaywrightWrapper.ts diff --git a/src/hooks/ArtifactManager.ts b/src/hooks/ArtifactManager.ts new file mode 100644 index 0000000..84cfeac --- /dev/null +++ b/src/hooks/ArtifactManager.ts @@ -0,0 +1,78 @@ +import { IWorld, Status } from "@cucumber/cucumber"; +import FixtureManager from "./FixtureManager"; +import * as fs from 'fs-extra'; + +export default class ArtifactManager { + shouldAttachMedia: boolean; + shouldAttachTrace: boolean; + img: Buffer | null; + videoPath: string | null; + + constructor(private world: IWorld, private fx: FixtureManager) { + this.img = null; + this.videoPath = null; + + const { scenario } = this.fx + this.shouldAttachMedia = + scenario.Status === Status.PASSED || + scenario.Status === Status.FAILED || + scenario.Status === Status.UNKNOWN; + this.shouldAttachTrace = + scenario.Status !== Status.PENDING && + scenario.Status !== Status.SKIPPED; + } + + async takeScreenshot(): Promise { + const { scenario, pageManager } = this.fx + const currentPage = pageManager.Page; + + if (scenario.hasTag('@disable:screenshot')) return; + if (!currentPage) return; + this.img = await currentPage.screenshot({ + path: `./test-results/screenshots/${this.fx.scenario.DashedName}.png`, + type: "png", + }); + this.videoPath = await currentPage.video()?.path(); + } + + async attachMedia() { + const { scenario } = this.fx + + if (!scenario.hasTag('@disable:screenshot') && this.img) { + await this.world.attach(this.img, "image/png"); + } + if (!scenario.hasTag('@disable:video') && this.videoPath) { + await this.world.attach(fs.createReadStream(this.videoPath), "video/webm"); + } + } + + async attachLogs(maxLines: number = 100) { + const { scenario } = this.fx + const logFilePath = `test-results/logs/${this.fx.scenario.DashedName}/log.log`; + + if (scenario.hasTag('@disable:log')) return; + if (!await fs.pathExists(logFilePath)) return; + const logContent = await fs.readFile(logFilePath, 'utf-8'); + const logLines = logContent.split('\n'); + const totalLines = logLines.length; + + if (totalLines <= 1) return; + const displayedLines = Math.min(totalLines, maxLines); + const truncatedMessage = totalLines > maxLines ? '\n[Log truncated due to excessive length]' : ''; + const displayedLogContent = logLines.slice(0, displayedLines).join('\n'); + + const logInfo = `Logs (${displayedLines}/${totalLines} lines):\n${displayedLogContent}${truncatedMessage}`; + + await this.world.attach(logInfo, 'text/plain'); + } + + async attachTrace() { + const { scenario } = this.fx + if (scenario.hasTag('@disable:trace')) return; + + const traceFileURL = `http://localhost:${process.env.REPORT_PORT}/trace/${this.fx.scenario.DashedName}.zip`; + const traceURL = `https://trace.playwright.dev/?trace=${traceFileURL}`; + const traceLink = `Open /trace/${this.fx.scenario.DashedName}`; + await this.world.attach(`Trace file: ${traceLink}`, "text/html"); + } +} \ No newline at end of file diff --git a/src/hooks/FixtureManager.ts b/src/hooks/FixtureManager.ts new file mode 100644 index 0000000..a54cb39 --- /dev/null +++ b/src/hooks/FixtureManager.ts @@ -0,0 +1,134 @@ +import * as path from 'path'; +import { Logger, createLogger } from "winston"; +import { options } from "../helper/util/logger"; +import { Browser, BrowserContext, Page } from "@playwright/test"; +import { invokeBrowser } from "../helper/browsers/browserManager"; +import ScenarioManager from './ScenarioManager'; +import { ITestCaseHookParameter } from '@cucumber/cucumber'; +import PageManager from './PageManager'; + +export interface IFixture { + browser: Browser; + context: BrowserContext; + pageManager: PageManager; + page: Page; + logger: Logger; + scenario: ScenarioManager; +} + +export default class FixtureManager implements IFixture { + browser: Browser; + context: BrowserContext; + pageManager: PageManager; + page: Page; + logger: Logger; + scenario: ScenarioManager; + + constructor() { + this.pageManager = new PageManager(); + } + + /** + * Provides access to the current test fixture. + * + * This getter returns an object conforming to the IFixture interface, + * segregating the interface from the FixtureManager implementation. + * This allows consumers to interact with the test fixture without + * needing to know about the underlying FixtureManager details. + * + * @returns {IFixture} The current test fixture. + */ + get Fixture(): IFixture { + return { + browser: this.browser, + context: this.context, + pageManager: this.pageManager, + page: this.pageManager.Page, + logger: this.logger, + scenario: this.scenario, + } + } + + setScenario(s: ITestCaseHookParameter) { + this.scenario = new ScenarioManager(s); + } + + clearScenario() { + this.scenario = null; + } + + async openBrowser() { + this.browser = await invokeBrowser() + } + + async closeBrowser() { + await this.browser.close(); + } + + async openContext() { + this.context = await this.browser.newContext({ + storageState: this.scenario.hasTag('@auth') ? getStorageState(this.scenario.DashedName) : undefined, + recordVideo: this.scenario.hasTag('@disable:video') ? undefined : { dir: "test-results/videos" } + }); + } + + async newPage() { + await this.pageManager.newPage(this.context); + } + + async closeAllPages() { + await this.pageManager.closeAllPages(); + } + + async createLogger() { + this.logger = createLogger(options(this.scenario.DashedName)); + } + + async closeContext() { + await this.context.close(); + } + + async startTracing() { + if (this.scenario.hasTag('@disable:trace')) return; + await this.context.tracing.start({ + name: this.scenario.DashedName, + title: this.scenario.Title, + sources: true, + screenshots: true, + snapshots: true + }); + } + + async stopTracing() { + if (this.scenario.hasTag('@disable:trace')) return; + const tracePath = path.join(__dirname, `../../test-results/trace/${this.scenario.DashedName}.zip`); + await this.context.tracing.stop({ path: tracePath }); + } +} + +type StorageState = string | { + cookies: { + name: string; + value: string; + domain: string; + path: string; + expires: number; + httpOnly: boolean; + secure: boolean; + sameSite: "Strict" | "Lax" | "None"; + }[]; + origins: { + origin: string; + localStorage: { + name: string; + value: string; + }[]; + }[]; +}; + +function getStorageState(user: string): StorageState { + if (user.endsWith("admin")) + return "src/helper/auth/admin.json"; + else if (user.endsWith("lead")) + return "src/helper/auth/lead.json"; +} \ No newline at end of file diff --git a/src/hooks/PageManager.ts b/src/hooks/PageManager.ts new file mode 100644 index 0000000..06cf53b --- /dev/null +++ b/src/hooks/PageManager.ts @@ -0,0 +1,149 @@ +import { BrowserContext, Page } from "@playwright/test"; +import { normalizeUrl } from "../helper/util/helpers"; + +export default class PageManager { + private pages: Page[]; + private currentPage: Page; + + constructor() { + this.pages = []; + this.currentPage = null; + } + + get Pages(): Page[] { + return this.pages; + } + + get Page(): Page { + return this.currentPage; + } + + async newPage(context: BrowserContext): Promise { + const page = await context.newPage(); + this.pages.push(page); + this.currentPage = page; + return page; + } + + async selectPage(pageOrIndex: Page | number): Promise { + const page = await this.getPage(pageOrIndex); + if (!page.isClosed()) { + await page.bringToFront(); + this.currentPage = page; + } else { + throw new Error(`Cannot select a closed page: ${page}`); + } + } + + async closePage(pageOrIndex: Page | number = this.currentPage): Promise { + const page = await this.getPage(pageOrIndex); + if (page) { + await page.close(); + const index = this.pages.indexOf(page); + if (index !== -1) { + this.pages.splice(index, 1); + } + if (this.currentPage === page) { + this.currentPage = this.pages[0] || null; + } + } + } + + async closeAllPages(): Promise { + await Promise.all(this.pages.map((page) => page.close())); + this.pages = []; + this.currentPage = null; + } + + async closeAllOtherPages(pageOrIndex: Page | number = this.currentPage): Promise { + const currentPage = await this.getPage(pageOrIndex); + if (currentPage) { + await Promise.all(this.pages.filter((page) => page !== currentPage).map((page) => page.close())); + this.pages = [currentPage]; + this.currentPage = currentPage; + } + } + + async hasPage(pageOrIndex: Page | number): Promise { + try { + await this.getPage(pageOrIndex); + return true; + } catch (error) { + return false; + } + } + + async getPage(pageOrIndex: Page | number): Promise { + if (typeof pageOrIndex === "number") { + const index = pageOrIndex; + if (index >= 0 && index < this.pages.length) { + return this.pages[index]; + } else { + throw new Error(`Invalid page index: ${index}`); + } + } else { + const page = pageOrIndex; + if (this.pages.includes(page)) { + return page; + } else { + throw new Error(`Page not found: ${page}`); + } + } + } + + async getPageByURL(url: string): Promise { + for (const page of this.pages) { + if (await normalizeUrl(page.url()) === normalizeUrl(url)) { + return page; + } + } + return undefined; + } + + async getPageByTitle(title: string): Promise { + for (const page of this.pages) { + if (await page.title() === title) { + return page; + } + } + return undefined; + } + + async selectPageByURL(url: string): Promise { + const page = await this.getPageByURL(url); + if (page) { + await this.selectPage(page); + } else { + throw new Error(`No page found with URL: ${url}`); + } + } + + async selectPageByTitle(title: string): Promise { + const page = await this.getPageByTitle(title); + if (page) { + await this.selectPage(page); + } else { + throw new Error(`No page found with title: ${title}`); + } + } + + getPageCount(): number { + return this.pages.length; + } + + getPageIndex(page: Page = this.currentPage): number { + return this.pages.indexOf(page); + } + + async switchToNextPage(): Promise { + const currentIndex = this.getPageIndex(); + const nextIndex = (currentIndex + 1) % this.pages.length; + await this.selectPage(nextIndex); + } + + async switchToPreviousPage(): Promise { + const currentIndex = this.getPageIndex(); + const previousIndex = (currentIndex - 1 + this.pages.length) % this.pages.length; + await this.selectPage(previousIndex); + } +} \ No newline at end of file diff --git a/src/hooks/ScenarioManager.ts b/src/hooks/ScenarioManager.ts new file mode 100644 index 0000000..09aa7f6 --- /dev/null +++ b/src/hooks/ScenarioManager.ts @@ -0,0 +1,29 @@ +import { ITestCaseHookParameter } from "@cucumber/cucumber"; + +export default class ScenarioManager { + + constructor(private cucumberScenario: ITestCaseHookParameter) { } + + get DashedName() { + return this.formatScenarioName(); + } + + get Title() { + return this.cucumberScenario.pickle.name; + } + + get Status() { + return this.cucumberScenario.result?.status; + } + + hasTag(tagName: string) { + const { pickle } = this.cucumberScenario; + return pickle.tags.some((tag) => tag.name === tagName); + } + + private formatScenarioName() { + const { pickle } = this.cucumberScenario; + const pickleName = pickle.name.split(' ').join('-'); // Replace spaces with dashes + return `${pickleName}_${pickle.id}`; + } +} \ No newline at end of file diff --git a/src/hooks/hooks.ts b/src/hooks/hooks.ts index a0f8f69..fc05700 100644 --- a/src/hooks/hooks.ts +++ b/src/hooks/hooks.ts @@ -1,95 +1,47 @@ -import { BeforeAll, AfterAll, Before, After, Status } from "@cucumber/cucumber"; -import { Browser, BrowserContext } from "@playwright/test"; -import { fixture } from "./pageFixture"; -import { invokeBrowser } from "../helper/browsers/browserManager"; +import { BeforeAll, AfterAll, Before, After } from "@cucumber/cucumber"; import { getEnv } from "../helper/env/env"; -import { createLogger } from "winston"; -import { options } from "../helper/util/logger"; -const fs = require("fs-extra"); +import PlaywrightWorld from "../test/worlds/PlaywrightWorld"; +import FixtureManager from "./FixtureManager"; +import ArtifactManager from "./ArtifactManager"; -let browser: Browser; -let context: BrowserContext; +let fx: FixtureManager BeforeAll(async function () { getEnv(); - browser = await invokeBrowser(); -}); -// It will trigger for not auth scenarios -Before({ tags: "not @auth" }, async function ({ pickle }) { - const scenarioName = pickle.name + pickle.id - context = await browser.newContext({ - recordVideo: { - dir: "test-results/videos", - }, - }); - await context.tracing.start({ - name: scenarioName, - title: pickle.name, - sources: true, - screenshots: true, snapshots: true - }); - const page = await context.newPage(); - fixture.page = page; - fixture.logger = createLogger(options(scenarioName)); + fx = new FixtureManager(); + await fx.openBrowser(); }); +Before(async function (this: PlaywrightWorld, scenario) { + fx.setScenario(scenario); + + await fx.openContext(); + await fx.startTracing(); + if (!fx.scenario.hasTag("@pageless")) { + await fx.newPage(); + } + await fx.createLogger(); -// It will trigger for auth scenarios -Before({ tags: '@auth' }, async function ({ pickle }) { - const scenarioName = pickle.name + pickle.id - context = await browser.newContext({ - storageState: getStorageState(pickle.name), - recordVideo: { - dir: "test-results/videos", - }, - }); - await context.tracing.start({ - name: scenarioName, - title: pickle.name, - sources: true, - screenshots: true, snapshots: true - }); - const page = await context.newPage(); - fixture.page = page; - fixture.logger = createLogger(options(scenarioName)); + this.fixture = fx.Fixture; }); -After(async function ({ pickle, result }) { - let videoPath: string; - let img: Buffer; - const path = `./test-results/trace/${pickle.id}.zip`; - if (result?.status == Status.PASSED) { - img = await fixture.page.screenshot( - { path: `./test-results/screenshots/${pickle.name}.png`, type: "png" }) - videoPath = await fixture.page.video().path(); - } - await context.tracing.stop({ path: path }); - await fixture.page.close(); - await context.close(); - if (result?.status == Status.PASSED) { - await this.attach( - img, "image/png" - ); - await this.attach( - fs.readFileSync(videoPath), - 'video/webm' - ); - const traceFileLink = `Open ${path}` - await this.attach(`Trace file: ${traceFileLink}`, 'text/html'); +After(async function (this: PlaywrightWorld, scenario) { + fx.setScenario(scenario); + const art = new ArtifactManager(this, fx); - } + if (art.shouldAttachMedia) await art.takeScreenshot(); + await fx.stopTracing(); + await fx.closeAllPages(); + await fx.closeContext(); + + if (art.shouldAttachMedia) await art.attachMedia(); + if (art.shouldAttachTrace) await art.attachTrace(); + await art.attachLogs(); + + fx.clearScenario(); }); AfterAll(async function () { - await browser.close(); + await fx.closeBrowser(); }) - -function getStorageState(user: string): string | { cookies: { name: string; value: string; domain: string; path: string; expires: number; httpOnly: boolean; secure: boolean; sameSite: "Strict" | "Lax" | "None"; }[]; origins: { origin: string; localStorage: { name: string; value: string; }[]; }[]; } { - if (user.endsWith("admin")) - return "src/helper/auth/admin.json"; - else if (user.endsWith("lead")) - return "src/helper/auth/lead.json"; -} - - diff --git a/src/hooks/pageFixture.ts b/src/hooks/pageFixture.ts deleted file mode 100644 index 1fc5e4e..0000000 --- a/src/hooks/pageFixture.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Page } from "@playwright/test"; -import { Logger } from "winston"; - -export const fixture = { - // @ts-ignore - page: undefined as Page, - logger: undefined as Logger -} \ No newline at end of file diff --git a/src/pages/headerPage.ts b/src/pages/headerPage.ts index d1c301d..b3f2c1e 100644 --- a/src/pages/headerPage.ts +++ b/src/pages/headerPage.ts @@ -1,5 +1,5 @@ import { expect, Page } from "@playwright/test"; -import PlaywrightWrapper from "../helper/wrapper/PlaywrightWrappers"; +import PlaywrightWrapper from "../helper/wrapper/PlaywrightWrapper"; export default class HeaderPage { diff --git a/src/pages/loginPage.ts b/src/pages/loginPage.ts index af68aee..5190ee4 100644 --- a/src/pages/loginPage.ts +++ b/src/pages/loginPage.ts @@ -1,5 +1,5 @@ import { expect, Page } from "@playwright/test"; -import PlaywrightWrapper from "../helper/wrapper/PlaywrightWrappers"; +import PlaywrightWrapper from "../helper/wrapper/PlaywrightWrapper"; export default class LoginPage { diff --git a/src/pages/registerPage.ts b/src/pages/registerPage.ts index 9cfcf82..09ad3be 100644 --- a/src/pages/registerPage.ts +++ b/src/pages/registerPage.ts @@ -1,5 +1,5 @@ import { expect, Page } from "@playwright/test"; -import PlaywrightWrapper from "../helper/wrapper/PlaywrightWrappers"; +import PlaywrightWrapper from "../helper/wrapper/PlaywrightWrapper"; export default class RegisterPage { diff --git a/src/test/features/postsApi.feature b/src/test/features/postsApi.feature new file mode 100644 index 0000000..0115131 --- /dev/null +++ b/src/test/features/postsApi.feature @@ -0,0 +1,56 @@ +@disable:video @disable:screenshot @api @only +Feature: Posts API + + Scenario: Retrieve all posts + When I send a GET request to "/posts" + Then the response status code should be 200 + And the response should contain a list of posts + + Scenario: Retrieve a single post + When I send a GET request to "/posts/18" + Then the response status code should be 200 + And the response should contain the details of the post: + | id | 18 | + | title | voluptate et itaque vero tempora molestiae | + | body | eveniet quo quis\nlaborum totam consequatur non dolor\nut et est repudiandae\nest voluptatem vel debitis et magnam | + | userId | 2 | + + Scenario: Create a new post + When I send a POST request to "/posts" with the following data: + | title | foo | + | body | bar | + | userId | 1 | + Then the response status code should be 201 + And the response should contain the newly created post: + | title | foo | + | body | bar | + | userId | 1 | + + Scenario: Update a post + When I send a PUT request to "/posts/1" with the following data: + | id | 1 | + | title | updated title | + | body | updated body | + | userId | 1 | + Then the response status code should be 200 + And the response should contain the updated post: + | id | 1 | + | title | updated title | + | body | updated body | + | userId | 1 | + + Scenario: Partially update a post + When I send a PATCH request to "/posts/1" with the following data: + | id | 1 | + | title | patched title | + | body | patched body | + Then the response status code should be 200 + And the response should contain the partially updated post: + | id | 1 | + | title | patched title | + | body | patched body | + | userId | 1 | + + Scenario: Delete a post + When I send a DELETE request to "/posts/1" + Then the response status code should be 200 diff --git a/src/test/features/unit/pageManager.feature b/src/test/features/unit/pageManager.feature new file mode 100644 index 0000000..27b0010 --- /dev/null +++ b/src/test/features/unit/pageManager.feature @@ -0,0 +1,136 @@ +@disable:video @unit @pageless @only +Feature: PageManager + + Background: + Given I have created multiple pages with the following URLs and titles: + | url | title | + | https://www.google.com | Google | + | https://www.example.com | Example Domain | + | https://www.wikipedia.org | Wikipedia | + + Scenario Outline: Select a page by index + When I select a page by index + Then the selected page should be on the expected URL "" + And the selected page should have the expected title "" + + Examples: + | index | url | title | + | 0 | https://www.google.com | Google | + | 1 | https://www.example.com | Example Domain | + | 2 | https://www.wikipedia.org | Wikipedia | + + Scenario Outline: Close page + When I close pages with indexes "<indexes>" + Then the closed pages should be closed + And the page manager should have <remainingPages> pages remaining + + Examples: + | indexes | remainingPages | + | 1 | 2 | + | 0,1 | 1 | + + Scenario: Close all pages + When I close all pages + Then all pages should be closed + And the page manager should be empty + And the current page should be set to null + + Scenario: Close all other pages + When I select a page by index <index> + And I close all other pages except the current page + Then all other pages should be closed + And the page manager should only contain the current page + + Examples: + | index | + | 0 | + | 1 | + | 2 | + + Scenario Outline: Get page by URL + When I get a page by URL "<url>" + Then the page with the matching URL should be returned + + Examples: + | url | + | https://www.google.com | + | https://www.example.com | + | https://www.wikipedia.org | + + Scenario: Get page by title + When I get a page by title "<title>" + Then the page with the matching title "<title>" should be returned + + Examples: + | title | + | Google | + | Example Domain | + | Wikipedia | + + Scenario Outline: Select page by URL + When I select a page by URL "<url>" + Then the page with the matching URL should be selected + + Examples: + | url | + | https://www.google.com | + | https://www.example.com | + | https://www.wikipedia.org | + + Scenario Outline: Select page by title + When I select a page by title "<title>" + Then the page with the matching title "<title>" should be selected + + Examples: + | title | + | Google | + | Example Domain | + | Wikipedia | + + Scenario: Get page count + When I get the page count + Then the page count should be 3 + + Scenario Outline: Get current page index + When I select a page by index <index> + And I get the current page index + Then the current page index should be <index> + + Examples: + | index | + | 0 | + | 1 | + | 2 | + + Scenario Outline: Get page index by page object + When I select a page by index <index> + And I get the page index by page object + Then the page index should be <index> + + Examples: + | index | + | 0 | + | 1 | + | 2 | + + Scenario Outline: Switch to next page + When I select a page by index <initialIndex> + And I switch to the next page + Then the current page should be at index <nextIndex> + + Examples: + | initialIndex | nextIndex | + | 0 | 1 | + | 1 | 2 | + | 2 | 0 | + + Scenario Outline: Switch to previous page + When I select a page by index <initialIndex> + And I switch to the previous page + Then the current page should be at index <previousIndex> + + Examples: + | initialIndex | previousIndex | + | 0 | 2 | + | 1 | 0 | + | 2 | 1 | diff --git a/src/test/steps/addToCartSteps.ts b/src/test/steps/addToCartSteps.ts index 1abd8dc..9a7eed2 100644 --- a/src/test/steps/addToCartSteps.ts +++ b/src/test/steps/addToCartSteps.ts @@ -1,24 +1,27 @@ import { Given, When, Then, setDefaultTimeout } from "@cucumber/cucumber"; - -setDefaultTimeout(60 * 1000 * 2) - import { expect } from "@playwright/test"; -import { fixture } from "../../hooks/pageFixture"; - +import { IFixture } from "../../hooks/FixtureManager"; +import * as ms from 'ms'; +setDefaultTimeout(ms('2 minutes')) Given('user search for a {string}', async function (book) { + let fixture = this.fixture as IFixture fixture.logger.info("Searching for a book: " + book) await fixture.page.locator("input[type='search']").type(book); await fixture.page.waitForTimeout(2000); await fixture.page.locator("mat-option[role='option'] span").click(); }); + When('user add the book to the cart', async function () { + let fixture = this.fixture as IFixture await fixture.page.locator("//button[@color='primary']").click(); const toast = fixture.page.locator("simple-snack-bar"); await expect(toast).toBeVisible(); await toast.waitFor({ state: "hidden" }) }); + Then('the cart badge should get updated', async function () { + let fixture = this.fixture as IFixture const badgeCount = await fixture.page.locator("#mat-badge-content-0").textContent(); expect(Number(badgeCount)).toBeGreaterThan(0); }); diff --git a/src/test/steps/loginSteps.ts b/src/test/steps/loginSteps.ts index be029c9..df16e8f 100644 --- a/src/test/steps/loginSteps.ts +++ b/src/test/steps/loginSteps.ts @@ -1,37 +1,43 @@ import { Given, When, Then, setDefaultTimeout } from "@cucumber/cucumber"; - import { expect } from "@playwright/test"; -import { fixture } from "../../hooks/pageFixture"; - -setDefaultTimeout(60 * 1000 * 2) +import { IFixture } from "../../hooks/FixtureManager"; +import * as ms from 'ms'; +setDefaultTimeout(ms('2 minutes')) Given('User navigates to the application', async function () { + let fixture = this.fixture as IFixture await fixture.page.goto(process.env.BASEURL); - fixture.logger.info("Navigated to the application") + fixture.logger.info("Navigated to the application"); + await fixture.page.waitForTimeout(ms('2 seconds')); + await fixture.page.waitForLoadState(); }) Given('User click on the login link', async function () { - await fixture.page.locator("//span[text()='Login']").click(); + let fixture = this.fixture as IFixture + await fixture.page.locator('mat-toolbar-row').getByRole('button', { name: 'Login' }).click() }); Given('User enter the username as {string}', async function (username) { + let fixture = this.fixture as IFixture await fixture.page.locator("input[formcontrolname='username']").type(username); }); Given('User enter the password as {string}', async function (password) { + let fixture = this.fixture as IFixture await fixture.page.locator("input[formcontrolname='password']").type(password); }) When('User click on the login button', async function () { - await fixture.page.locator("button[color='primary']").click(); + let fixture = this.fixture as IFixture + await fixture.page.locator('mat-card-actions').getByRole('button', { name: 'Login' }).click() await fixture.page.waitForLoadState(); fixture.logger.info("Waiting for 2 seconds") - await fixture.page.waitForTimeout(2000); + await fixture.page.waitForTimeout(ms('2 seconds')); }); - Then('Login should be success', async function () { - const user = fixture.page.locator("//button[contains(@class,'mat-focus-indicator mat-menu-trigger')]//span[1]"); + let fixture = this.fixture as IFixture + const user = await fixture.page.locator('a.mat-mdc-menu-trigger span.mdc-button__label > span'); await expect(user).toBeVisible(); const userName = await user.textContent(); console.log("Username: " + userName); @@ -39,6 +45,7 @@ Then('Login should be success', async function () { }) When('Login should fail', async function () { + let fixture = this.fixture as IFixture const failureMesssage = fixture.page.locator("mat-error[role='alert']"); await expect(failureMesssage).toBeVisible(); }); diff --git a/src/test/steps/postsApiSteps.ts b/src/test/steps/postsApiSteps.ts new file mode 100644 index 0000000..27c75a2 --- /dev/null +++ b/src/test/steps/postsApiSteps.ts @@ -0,0 +1,121 @@ +import { Before, Given, When, Then, setDefaultTimeout } from "@cucumber/cucumber"; +import { expect } from "@playwright/test"; +import { IFixture } from "../../hooks/FixtureManager"; +import * as ms from 'ms'; +setDefaultTimeout(ms('2 minutes')) + +const BASE_URL = 'https://jsonplaceholder.typicode.com'; + +interface Post { + id: number; + title: string; + body: string; + userId: number; +} + +Before(function () { + this.response = null; +}); + +When('I send a GET request to {string}', async function (endpoint) { + let fixture = this.fixture as IFixture + const { request } = fixture.page + const response = await request.get(BASE_URL + endpoint); + this.response = await response.json() + this.statusCode = response.status(); +}); + +When('I send a POST request to {string} with the following data:', async function (endpoint, dataTable) { + let fixture = this.fixture as IFixture + const { request } = fixture.page + const data = dataTable.rowsHash(); + const response = await request.post(BASE_URL + endpoint, { + data: data, + }); + this.response = await response.json(); + this.statusCode = response.status(); +}); + +When('I send a PUT request to {string} with the following data:', async function (endpoint, dataTable) { + let fixture = this.fixture as IFixture + const { request } = fixture.page + const data = dataTable.rowsHash(); + const response = await request.put(BASE_URL + endpoint, { + data: data, + }); + this.response = await response.json(); + this.statusCode = response.status(); +}); + +When('I send a PATCH request to {string} with the following data:', async function (endpoint, dataTable) { + let fixture = this.fixture as IFixture + const { request } = fixture.page + const data = dataTable.rowsHash(); + const response = await request.patch(BASE_URL + endpoint, { + data: data, + }); + this.response = await response.json(); + this.statusCode = response.status(); +}); + +When('I send a DELETE request to {string}', async function (endpoint) { + let fixture = this.fixture as IFixture + const { request } = fixture.page + const response = await request.delete(BASE_URL + endpoint); + this.statusCode = response.status(); +}); + +Then('the response status code should be {int}', function (statusCode) { + expect(this.statusCode).toBe(statusCode); +}); + +Then('the response should contain a list of posts', function () { + const response = this.response as Post[]; + expect(Array.isArray(response)).toBe(true); + expect(response.length).toBeGreaterThan(0); + + // Check that each post has the expected structure + response.forEach(post => { + expect(post).toHaveProperty('id'); + expect(post).toHaveProperty('userId'); + expect(post).toHaveProperty('title'); + expect(post).toHaveProperty('body'); + + // Check that id and userId are numbers + expect(typeof post.id).toBe('number'); + expect(typeof post.userId).toBe('number'); + + // Check that title and body are strings + expect(typeof post.title).toBe('string'); + expect(typeof post.body).toBe('string'); + }); +}); + +Then('the response should contain the details of the post:', function (dataTable) { + const response = this.response as Post; + const expectedData = dataTable.rowsHash() + expectedData.id = parseInt(expectedData.id); + expectedData.userId = parseInt(expectedData.userId); + expect(response).toEqual(expectedData); +}); + +Then('the response should contain the newly created post:', function (dataTable) { + const { id, ...responseWithoutId } = this.response as Post; + const expectedData = dataTable.rowsHash(); + expect(typeof id).toBe('number'); + expect(responseWithoutId).toEqual(expectedData); +}); + +Then('the response should contain the updated post:', function (dataTable) { + const response = this.response as Post; + const expectedData = dataTable.rowsHash(); + expectedData.id = parseInt(expectedData.id); + expect(response).toEqual(expectedData); +}); + +Then('the response should contain the partially updated post:', function (dataTable) { + const response = this.response as Post; + const expectedData = dataTable.rowsHash(); + expectedData.userId = parseInt(expectedData.userId); + expect(response).toEqual(expectedData); +}); \ No newline at end of file diff --git a/src/test/steps/registerUsersSteps.ts b/src/test/steps/registerUsersSteps.ts index b084da1..da19420 100644 --- a/src/test/steps/registerUsersSteps.ts +++ b/src/test/steps/registerUsersSteps.ts @@ -1,13 +1,16 @@ -import { Given, When, Then } from "@cucumber/cucumber"; +import { Given, When, Then, setDefaultTimeout } from "@cucumber/cucumber"; import RegisterPage from "../../pages/registerPage"; -import { fixture } from "../../hooks/pageFixture"; import Assert from "../../helper/wrapper/assert"; import * as data from "../../helper/util/test-data/registerUser.json"; +import { IFixture } from "../../hooks/FixtureManager"; +import * as ms from 'ms'; +setDefaultTimeout(ms('2 minutes')) let registerPage: RegisterPage; let assert: Assert; Given('I navigate to the register page', async function () { + let fixture = this.fixture as IFixture registerPage = new RegisterPage(fixture.page); assert = new Assert(fixture.page); await registerPage.navigateToRegisterPage(); diff --git a/src/test/steps/unit/pageManagerSteps.ts b/src/test/steps/unit/pageManagerSteps.ts new file mode 100644 index 0000000..8e28f0b --- /dev/null +++ b/src/test/steps/unit/pageManagerSteps.ts @@ -0,0 +1,158 @@ +import { Given, When, Then, DataTable } from "@cucumber/cucumber"; +import { expect } from "@playwright/test"; +import { IFixture } from "../../../hooks/FixtureManager"; +import PlaywrightWorld from "../../worlds/PlaywrightWorld"; +import PageManager from "../../../hooks/PageManager"; +import { normalizeUrl } from "../../../helper/util/helpers"; + +let pageManager: PageManager; + +Given("I have created multiple pages with the following URLs and titles:", async function (dataTable: DataTable) { + const fixture = this.fixture as IFixture; + const { context } = fixture; + + pageManager = fixture.pageManager; + + const data = dataTable.hashes(); + for (const { url, title } of data) { + await pageManager.newPage(context); + await pageManager.Page.goto(url); + expect(await pageManager.Page.title()).toBe(title); + } +}); + +When("I select a page by index {int}", async function (index: number) { + await pageManager.selectPage(index); +}); + +Then("the selected page should be on the expected URL {string}", async function (expectedUrl: string) { + let actualUrl = await pageManager.Page.url(); + actualUrl = normalizeUrl(actualUrl) + expectedUrl = normalizeUrl(expectedUrl) + expect(actualUrl).toBe(expectedUrl); +}); + +Then("the selected page should have the expected title {string}", async function (expectedTitle: string) { + const actualTitle = await pageManager.Page.title(); + expect(actualTitle).toBe(expectedTitle); +}); + +When("I close pages with indexes {string}", async function (indexesString: string) { + const indexes = indexesString.split(',').map(index => parseInt(index.trim(), 10)); + this.closedPages = await Promise.all(indexes.map(index => pageManager.getPage(index))); + await Promise.all(indexes.map(index => pageManager.closePage(index))); +}); + +Then("the closed pages should be closed", async function () { + for (const closedPage of this.closedPages) { + expect(closedPage).toBeDefined(); + expect(await closedPage.isClosed()).toBe(true); + } +}); + +Then("the page manager should have {int} pages remaining", function (expectedCount: number) { + const actualCount = pageManager.getPageCount(); + expect(actualCount).toBe(expectedCount); +}); + +When("I close all pages", async function () { + await pageManager.closeAllPages(); +}); + +Then("all pages should be closed", async function () { + const allClosed = (await Promise.all(pageManager.Pages.map((page) => page.isClosed()))).every((isClosed) => isClosed); + expect(allClosed).toBe(true); +}); + +Then("the page manager should be empty", function () { + expect(pageManager.getPageCount()).toBe(0); +}); + +Then("the current page should be set to null", function () { + expect(pageManager.Page).toBeNull(); +}); + +When("I close all other pages except the current page", async function () { + await pageManager.closeAllOtherPages(); +}); + +Then("all other pages should be closed", async function () { + const allOtherClosed = (await Promise.all(pageManager.Pages.filter((page) => page !== pageManager.Page).map((page) => page.isClosed()))).every((isClosed) => isClosed); + expect(allOtherClosed).toBe(true); +}); + +Then("the page manager should only contain the current page", function () { + expect(pageManager.getPageCount()).toBe(1); + expect(pageManager.Pages[0]).toBe(pageManager.Page); +}); + +When("I get a page by URL {string}", async function (url: string) { + this.page = await pageManager.getPageByURL(url); +}); + +Then("the page with the matching URL should be returned", async function () { + expect(this.page).not.toBeUndefined(); + expect(await this.page.url()).toBe(this.page.url()); +}); + +When("I get a page by title {string}", async function (title: string) { + this.page = await pageManager.getPageByTitle(title); +}); + +Then("the page with the matching title {string} should be returned", async function (title: string) { + expect(this.page).not.toBeUndefined(); + expect(await this.page.title()).toBe(title); +}); + +When("I select a page by URL {string}", async function (url: string) { + await pageManager.selectPageByURL(url); +}); + +Then("the page with the matching URL should be selected", async function () { + expect(await pageManager.Page.url()).toBe(pageManager.Page.url()); +}); + +When("I select a page by title {string}", async function (title: string) { + await pageManager.selectPageByTitle(title); +}); + +Then("the page with the matching title {string} should be selected", async function (title: string) { + expect(await pageManager.Page.title()).toBe(title); +}); + +When("I get the page count", function () { + this.pageCount = pageManager.getPageCount(); +}); + +Then("the page count should be {int}", function (expectedCount: number) { + expect(this.pageCount).toBe(expectedCount); +}); + +When("I get the current page index", function () { + this.currentPageIndex = pageManager.getPageIndex(); +}); + +Then("the current page index should be {int}", function (expectedIndex: number) { + expect(this.currentPageIndex).toBe(expectedIndex); +}); + +When("I get the page index by page object", function () { + this.pageIndex = pageManager.getPageIndex(pageManager.Page); +}); + +Then("the page index should be {int}", function (expectedIndex: number) { + expect(this.pageIndex).toBe(expectedIndex); +}); + +When("I switch to the next page", async function () { + await pageManager.switchToNextPage(); +}); + +Then("the current page should be at index {int}", function (expectedIndex: number) { + const currentIndex = pageManager.getPageIndex(); + expect(currentIndex).toBe(expectedIndex); +}); + +When("I switch to the previous page", async function () { + await pageManager.switchToPreviousPage(); +}); \ No newline at end of file diff --git a/src/test/worlds/PlaywrightWorld.ts b/src/test/worlds/PlaywrightWorld.ts new file mode 100644 index 0000000..48de02c --- /dev/null +++ b/src/test/worlds/PlaywrightWorld.ts @@ -0,0 +1,18 @@ +import { IWorldOptions, setWorldConstructor, World } from '@cucumber/cucumber'; +import { IFixture } from '../../hooks/FixtureManager'; + +export interface IPlaywrightWorld extends World { + fixture: IFixture; + store: any; +} + +export default class PlaywrightWorld extends World implements IPlaywrightWorld { + fixture: IFixture; + store: any; + + constructor(options: IWorldOptions) { + super(options); + } +} + +setWorldConstructor(PlaywrightWorld); \ No newline at end of file