diff --git a/.pnp.cjs b/.pnp.cjs index d9dd11404..ffdd2ca88 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -3399,17 +3399,17 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@cucumber/cucumber", [\ - ["npm:10.4.0", {\ - "packageLocation": "./.yarn/cache/@cucumber-cucumber-npm-10.4.0-1fcea15c8b-9527f82450.zip/node_modules/@cucumber/cucumber/",\ + ["npm:10.8.0", {\ + "packageLocation": "./.yarn/cache/@cucumber-cucumber-npm-10.8.0-3268c4cd89-537bd8b891.zip/node_modules/@cucumber/cucumber/",\ "packageDependencies": [\ - ["@cucumber/cucumber", "npm:10.4.0"],\ + ["@cucumber/cucumber", "npm:10.8.0"],\ ["@cucumber/ci-environment", "npm:10.0.1"],\ ["@cucumber/cucumber-expressions", "npm:17.1.0"],\ ["@cucumber/gherkin", "npm:28.0.0"],\ - ["@cucumber/gherkin-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:5.0.1"],\ + ["@cucumber/gherkin-streams", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:5.0.1"],\ ["@cucumber/gherkin-utils", "npm:9.0.0"],\ - ["@cucumber/html-formatter", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:21.3.1"],\ - ["@cucumber/message-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:4.0.1"],\ + ["@cucumber/html-formatter", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:21.3.1"],\ + ["@cucumber/message-streams", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:4.0.1"],\ ["@cucumber/messages", "npm:24.1.0"],\ ["@cucumber/tag-expressions", "npm:6.1.0"],\ ["assertion-error-formatter", "npm:3.0.0"],\ @@ -3492,12 +3492,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:5.0.1", {\ - "packageLocation": "./.yarn/__virtual__/@cucumber-gherkin-streams-virtual-db404f246b/0/cache/@cucumber-gherkin-streams-npm-5.0.1-a9097ed93e-d52324a443.zip/node_modules/@cucumber/gherkin-streams/",\ + ["virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:5.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@cucumber-gherkin-streams-virtual-f2af7ac6e2/0/cache/@cucumber-gherkin-streams-npm-5.0.1-a9097ed93e-d52324a443.zip/node_modules/@cucumber/gherkin-streams/",\ "packageDependencies": [\ - ["@cucumber/gherkin-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:5.0.1"],\ + ["@cucumber/gherkin-streams", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:5.0.1"],\ ["@cucumber/gherkin", "npm:28.0.0"],\ - ["@cucumber/message-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:4.0.1"],\ + ["@cucumber/message-streams", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:4.0.1"],\ ["@cucumber/messages", "npm:24.1.0"],\ ["@types/cucumber__gherkin", null],\ ["@types/cucumber__message-streams", null],\ @@ -3514,6 +3514,29 @@ const RAW_RUNTIME_STATE = "@types/cucumber__messages"\ ],\ "linkType": "HARD"\ + }],\ + ["virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:5.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@cucumber-gherkin-streams-virtual-5f00396eed/0/cache/@cucumber-gherkin-streams-npm-5.0.1-a9097ed93e-d52324a443.zip/node_modules/@cucumber/gherkin-streams/",\ + "packageDependencies": [\ + ["@cucumber/gherkin-streams", "virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:5.0.1"],\ + ["@cucumber/gherkin", "npm:28.0.0"],\ + ["@cucumber/message-streams", "virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:4.0.1"],\ + ["@cucumber/messages", "npm:25.0.1"],\ + ["@types/cucumber__gherkin", null],\ + ["@types/cucumber__message-streams", null],\ + ["@types/cucumber__messages", null],\ + ["commander", "npm:9.1.0"],\ + ["source-map-support", "npm:0.5.21"]\ + ],\ + "packagePeers": [\ + "@cucumber/gherkin",\ + "@cucumber/message-streams",\ + "@cucumber/messages",\ + "@types/cucumber__gherkin",\ + "@types/cucumber__message-streams",\ + "@types/cucumber__messages"\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@cucumber/gherkin-utils", [\ @@ -3538,10 +3561,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:21.3.1", {\ - "packageLocation": "./.yarn/__virtual__/@cucumber-html-formatter-virtual-9d579f6350/0/cache/@cucumber-html-formatter-npm-21.3.1-5f1156cb5f-7c67856ab1.zip/node_modules/@cucumber/html-formatter/",\ + ["virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:21.3.1", {\ + "packageLocation": "./.yarn/__virtual__/@cucumber-html-formatter-virtual-7c69539e48/0/cache/@cucumber-html-formatter-npm-21.3.1-5f1156cb5f-7c67856ab1.zip/node_modules/@cucumber/html-formatter/",\ "packageDependencies": [\ - ["@cucumber/html-formatter", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:21.3.1"],\ + ["@cucumber/html-formatter", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:21.3.1"],\ ["@cucumber/messages", "npm:24.1.0"],\ ["@types/cucumber__messages", null]\ ],\ @@ -3560,10 +3583,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:4.0.1", {\ - "packageLocation": "./.yarn/__virtual__/@cucumber-message-streams-virtual-16cbcb1c8f/0/cache/@cucumber-message-streams-npm-4.0.1-0c98ff65d5-74080dafde.zip/node_modules/@cucumber/message-streams/",\ + ["virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:4.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@cucumber-message-streams-virtual-29ebe08837/0/cache/@cucumber-message-streams-npm-4.0.1-0c98ff65d5-74080dafde.zip/node_modules/@cucumber/message-streams/",\ "packageDependencies": [\ - ["@cucumber/message-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:4.0.1"],\ + ["@cucumber/message-streams", "virtual:3268c4cd897a50f7d745c9b5ef6383f44c00031a87f71b6c38089f845306dc839e91d2bf1684c2453c3aaf0757c557906554dce413a4b62c75d4765ff16f9b77#npm:4.0.1"],\ ["@cucumber/messages", "npm:24.1.0"],\ ["@types/cucumber__messages", null]\ ],\ @@ -3572,6 +3595,19 @@ const RAW_RUNTIME_STATE = "@types/cucumber__messages"\ ],\ "linkType": "HARD"\ + }],\ + ["virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:4.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@cucumber-message-streams-virtual-ea2347b139/0/cache/@cucumber-message-streams-npm-4.0.1-0c98ff65d5-74080dafde.zip/node_modules/@cucumber/message-streams/",\ + "packageDependencies": [\ + ["@cucumber/message-streams", "virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:4.0.1"],\ + ["@cucumber/messages", "npm:25.0.1"],\ + ["@types/cucumber__messages", null]\ + ],\ + "packagePeers": [\ + "@cucumber/messages",\ + "@types/cucumber__messages"\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@cucumber/messages", [\ @@ -3607,6 +3643,17 @@ const RAW_RUNTIME_STATE = ["uuid", "npm:9.0.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:25.0.1", {\ + "packageLocation": "./.yarn/cache/@cucumber-messages-npm-25.0.1-a26e036dcc-ed3d4554cf.zip/node_modules/@cucumber/messages/",\ + "packageDependencies": [\ + ["@cucumber/messages", "npm:25.0.1"],\ + ["@types/uuid", "npm:9.0.8"],\ + ["class-transformer", "npm:0.5.1"],\ + ["reflect-metadata", "npm:0.2.2"],\ + ["uuid", "npm:9.0.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@cucumber/tag-expressions", [\ @@ -6092,12 +6139,12 @@ const RAW_RUNTIME_STATE = ["@babel/plugin-transform-modules-commonjs", "virtual:3b04c8c38dde7165df844f9cd74e94cc47d78164564600cf56ba5f8c6011d25ef0d3afc13d610f3da970f6807c830914053ff96a43c86ba17f3ec650dfd687d3#npm:7.24.6"],\ ["@babel/preset-env", "virtual:3b04c8c38dde7165df844f9cd74e94cc47d78164564600cf56ba5f8c6011d25ef0d3afc13d610f3da970f6807c830914053ff96a43c86ba17f3ec650dfd687d3#npm:7.24.6"],\ ["@babel/preset-typescript", "virtual:3b04c8c38dde7165df844f9cd74e94cc47d78164564600cf56ba5f8c6011d25ef0d3afc13d610f3da970f6807c830914053ff96a43c86ba17f3ec650dfd687d3#npm:7.24.6"],\ - ["@cucumber/cucumber", "npm:10.4.0"],\ + ["@cucumber/cucumber", "npm:10.8.0"],\ ["@cucumber/gherkin", "npm:28.0.0"],\ - ["@cucumber/gherkin-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:5.0.1"],\ + ["@cucumber/gherkin-streams", "virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:5.0.1"],\ ["@cucumber/gherkin-utils", "npm:9.0.0"],\ - ["@cucumber/message-streams", "virtual:1fcea15c8bf2089a53fe8bf9ffc837d613c1bc61a4d4dd5395727d0484bd003ca936b69b8a0cf6bdf6c55f3297ae5255818ce58084d2abc55778d09888281fe5#npm:4.0.1"],\ - ["@cucumber/messages", "npm:24.1.0"],\ + ["@cucumber/message-streams", "virtual:606d5fd11adc18cba076ddb26a9b3adaf466a45eef985b60ea990c3ead5fe6bcc20990b0ed8d0763c4a8c861cdc2277964415bf12191df7c6923d8e78816abf5#npm:4.0.1"],\ + ["@cucumber/messages", "npm:25.0.1"],\ ["@types/babel__core", "npm:7.20.5"],\ ["@types/babel__preset-env", "npm:7.9.6"],\ ["@types/chai", "npm:4.3.12"],\ @@ -15818,6 +15865,13 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.2.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:0.2.2", {\ + "packageLocation": "./.yarn/cache/reflect-metadata-npm-0.2.2-5e0bfac201-1c93f9ac79.zip/node_modules/reflect-metadata/",\ + "packageDependencies": [\ + ["reflect-metadata", "npm:0.2.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["regenerate", [\ diff --git a/.yarn/cache/@cucumber-cucumber-npm-10.4.0-1fcea15c8b-9527f82450.zip b/.yarn/cache/@cucumber-cucumber-npm-10.8.0-3268c4cd89-537bd8b891.zip similarity index 51% rename from .yarn/cache/@cucumber-cucumber-npm-10.4.0-1fcea15c8b-9527f82450.zip rename to .yarn/cache/@cucumber-cucumber-npm-10.8.0-3268c4cd89-537bd8b891.zip index 84210e903..888a172b9 100644 Binary files a/.yarn/cache/@cucumber-cucumber-npm-10.4.0-1fcea15c8b-9527f82450.zip and b/.yarn/cache/@cucumber-cucumber-npm-10.8.0-3268c4cd89-537bd8b891.zip differ diff --git a/.yarn/cache/@cucumber-messages-npm-25.0.1-a26e036dcc-ed3d4554cf.zip b/.yarn/cache/@cucumber-messages-npm-25.0.1-a26e036dcc-ed3d4554cf.zip new file mode 100644 index 000000000..d31ef3b47 Binary files /dev/null and b/.yarn/cache/@cucumber-messages-npm-25.0.1-a26e036dcc-ed3d4554cf.zip differ diff --git a/.yarn/cache/reflect-metadata-npm-0.2.2-5e0bfac201-1c93f9ac79.zip b/.yarn/cache/reflect-metadata-npm-0.2.2-5e0bfac201-1c93f9ac79.zip new file mode 100644 index 000000000..9b9b66393 Binary files /dev/null and b/.yarn/cache/reflect-metadata-npm-0.2.2-5e0bfac201-1c93f9ac79.zip differ diff --git a/.yarn/sdks/eslint/bin/eslint.js b/.yarn/sdks/eslint/bin/eslint.js index 9ef98e400..42eab9933 100755 --- a/.yarn/sdks/eslint/bin/eslint.js +++ b/.yarn/sdks/eslint/bin/eslint.js @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require eslint/bin/eslint.js require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/eslint/lib/api.js b/.yarn/sdks/eslint/lib/api.js index 653b22bae..ea2b46a70 100644 --- a/.yarn/sdks/eslint/lib/api.js +++ b/.yarn/sdks/eslint/lib/api.js @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require eslint require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/eslint/lib/unsupported-api.js b/.yarn/sdks/eslint/lib/unsupported-api.js index 30fdf158b..f5f8e24d0 100644 --- a/.yarn/sdks/eslint/lib/unsupported-api.js +++ b/.yarn/sdks/eslint/lib/unsupported-api.js @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require eslint/use-at-your-own-risk require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/prettier/bin/prettier.cjs b/.yarn/sdks/prettier/bin/prettier.cjs index 5efad688e..00f1f7f74 100755 --- a/.yarn/sdks/prettier/bin/prettier.cjs +++ b/.yarn/sdks/prettier/bin/prettier.cjs @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require prettier/bin/prettier.cjs require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/prettier/index.cjs b/.yarn/sdks/prettier/index.cjs index 8758e367a..d546c6a75 100644 --- a/.yarn/sdks/prettier/index.cjs +++ b/.yarn/sdks/prettier/index.cjs @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require prettier require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/typescript/bin/tsc b/.yarn/sdks/typescript/bin/tsc index 454b950b7..a6bb0e2c1 100755 --- a/.yarn/sdks/typescript/bin/tsc +++ b/.yarn/sdks/typescript/bin/tsc @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require typescript/bin/tsc require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/typescript/bin/tsserver b/.yarn/sdks/typescript/bin/tsserver index d7a605684..957bed200 100755 --- a/.yarn/sdks/typescript/bin/tsserver +++ b/.yarn/sdks/typescript/bin/tsserver @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require typescript/bin/tsserver require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/typescript/lib/tsc.js b/.yarn/sdks/typescript/lib/tsc.js index 2f62fc96c..a262a77d0 100644 --- a/.yarn/sdks/typescript/lib/tsc.js +++ b/.yarn/sdks/typescript/lib/tsc.js @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require typescript/lib/tsc.js require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/.yarn/sdks/typescript/lib/tsserver.js b/.yarn/sdks/typescript/lib/tsserver.js index bbb1e4650..1dae54c1a 100644 --- a/.yarn/sdks/typescript/lib/tsserver.js +++ b/.yarn/sdks/typescript/lib/tsserver.js @@ -1,14 +1,28 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserver.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + const moduleWrapper = tsserver => { if (!process.versions.pnp) { return tsserver; @@ -214,11 +228,11 @@ const moduleWrapper = tsserver => { return tsserver; }; -if (existsSync(absPnpApiPath)) { - if (!process.versions.pnp) { - // Setup the environment to be able to require typescript/lib/tsserver.js - require(absPnpApiPath).setup(); - } +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); } // Defer to the real typescript/lib/tsserver.js your application uses diff --git a/.yarn/sdks/typescript/lib/tsserverlibrary.js b/.yarn/sdks/typescript/lib/tsserverlibrary.js index a68f028fe..7f9d7f964 100644 --- a/.yarn/sdks/typescript/lib/tsserverlibrary.js +++ b/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -1,14 +1,28 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require typescript/lib/tsserverlibrary.js + require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } + } +} + const moduleWrapper = tsserver => { if (!process.versions.pnp) { return tsserver; @@ -214,11 +228,11 @@ const moduleWrapper = tsserver => { return tsserver; }; -if (existsSync(absPnpApiPath)) { - if (!process.versions.pnp) { - // Setup the environment to be able to require typescript/lib/tsserverlibrary.js - require(absPnpApiPath).setup(); - } +const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); +// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. +// Ref https://github.com/microsoft/TypeScript/pull/55326 +if (major > 5 || (major === 5 && minor >= 5)) { + moduleWrapper(absRequire(`typescript`)); } // Defer to the real typescript/lib/tsserverlibrary.js your application uses diff --git a/.yarn/sdks/typescript/lib/typescript.js b/.yarn/sdks/typescript/lib/typescript.js index b5f4db25b..317b60b4c 100644 --- a/.yarn/sdks/typescript/lib/typescript.js +++ b/.yarn/sdks/typescript/lib/typescript.js @@ -1,18 +1,25 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire} = require(`module`); +const {createRequire, register} = require(`module`); const {resolve} = require(`path`); +const {pathToFileURL} = require(`url`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); +const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); +const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); + if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { // Setup the environment to be able to require typescript require(absPnpApiPath).setup(); + if (isPnpLoaderEnabled && register) { + register(pathToFileURL(absPnpLoaderPath)); + } } } diff --git a/packages/allure-codeceptjs/src/helpers.ts b/packages/allure-codeceptjs/src/helpers.ts index 1f0ba0838..81932b50a 100644 --- a/packages/allure-codeceptjs/src/helpers.ts +++ b/packages/allure-codeceptjs/src/helpers.ts @@ -21,7 +21,7 @@ export const extractMeta = (test: CodeceptTest & { tags: string[] }) => { return { name, value }; } - return { name: LabelName.TAG, value: tag }; + return { name: LabelName.TAG, value: tag.startsWith("@") ? tag.substring(1) : tag }; }); return { labels }; diff --git a/packages/allure-codeceptjs/src/reporter.ts b/packages/allure-codeceptjs/src/reporter.ts index 7169a12de..288eaf313 100644 --- a/packages/allure-codeceptjs/src/reporter.ts +++ b/packages/allure-codeceptjs/src/reporter.ts @@ -1,14 +1,16 @@ import { event } from "codeceptjs"; import path from "node:path"; -import { LabelName, Stage, Status } from "allure-js-commons"; +import { LabelName, Stage, Status, type StepResult } from "allure-js-commons"; import { type RuntimeMessage, extractMetadataFromString, getMessageAndTraceFromError } from "allure-js-commons/sdk"; import { FileSystemWriter, MessageWriter, ReporterRuntime, md5 } from "allure-js-commons/sdk/reporter"; import { extractMeta } from "./helpers.js"; import type { AllureCodeceptJsConfig, CodeceptError, CodeceptHook, CodeceptStep, CodeceptTest } from "./model.js"; export class AllureCodeceptJsReporter { - allureRuntime?: ReporterRuntime; - currentAllureResultUuid?: string; + allureRuntime: ReporterRuntime; + currentTestUuid?: string; + currentFixtureUuid?: string; + scopeUuids: string[] = []; currentTest: CodeceptTest | null = null; config!: AllureCodeceptJsConfig; @@ -26,11 +28,11 @@ export class AllureCodeceptJsReporter { } private closeCurrentAllureTest(test: CodeceptTest) { - if (!this.currentAllureResultUuid) { + if (!this.currentTestUuid) { return; } - this.allureRuntime!.updateTest((result) => { + this.allureRuntime.updateTest(this.currentTestUuid, (result) => { result.stage = Stage.FINISHED; // @ts-ignore @@ -40,11 +42,11 @@ export class AllureCodeceptJsReporter { // @ts-ignore result.parameters.push({ name: "Repetition", value: `${test.retryNum + 1}` }); - }, this.currentAllureResultUuid); + }); - this.allureRuntime!.stopTest({ uuid: this.currentAllureResultUuid }); - this.allureRuntime!.writeTest(this.currentAllureResultUuid); - this.currentAllureResultUuid = undefined; + this.allureRuntime.stopTest(this.currentTestUuid); + this.allureRuntime.writeTest(this.currentTestUuid); + this.currentTestUuid = undefined; } private startAllureTest(test: CodeceptTest) { @@ -54,13 +56,16 @@ export class AllureCodeceptJsReporter { // @ts-ignore const { labels } = extractMeta(test); - this.currentAllureResultUuid = this.allureRuntime!.startTest({ - name: titleMetadata.cleanTitle, - fullName, - testCaseId: md5(fullName), - }); + this.currentTestUuid = this.allureRuntime.startTest( + { + name: titleMetadata.cleanTitle, + fullName, + testCaseId: md5(fullName), + }, + this.scopeUuids, + ); - this.allureRuntime!.updateTest((result) => { + this.allureRuntime.updateTest(this.currentTestUuid, (result) => { result.labels.push(...labels); result.labels.push(...titleMetadata.labels); result.labels.push({ name: LabelName.LANGUAGE, value: "javascript" }); @@ -72,45 +77,40 @@ export class AllureCodeceptJsReporter { value: test.parent.title, }); } - }, this.currentAllureResultUuid); - } - - private closeCurrentAllureStep() { - if (!this.currentAllureResultUuid) { - return; - } - - this.allureRuntime!.updateStep((result) => { - result.stage = Stage.FINISHED; - }, this.currentAllureResultUuid); - this.allureRuntime!.stopStep({ uuid: this.currentAllureResultUuid }); + }); } registerEvents() { // Hooks - event.dispatcher.addListener(event.hook.started, this.hookStarted.bind(this)); - event.dispatcher.addListener(event.hook.passed, this.hookPassed.bind(this)); + event.dispatcher.on(event.hook.started, this.hookStarted.bind(this)); + event.dispatcher.on(event.hook.passed, this.hookPassed.bind(this)); // Suite - event.dispatcher.addListener(event.suite.before, this.suiteBefore.bind(this)); - event.dispatcher.addListener(event.suite.after, this.suiteAfter.bind(this)); + event.dispatcher.on(event.suite.before, this.suiteBefore.bind(this)); + event.dispatcher.on(event.suite.after, this.suiteAfter.bind(this)); // Test - event.dispatcher.addListener(event.test.started, this.testStarted.bind(this)); - event.dispatcher.addListener(event.test.skipped, this.testSkipped.bind(this)); - event.dispatcher.addListener(event.test.passed, this.testPassed.bind(this)); - event.dispatcher.addListener(event.test.failed, this.testFailed.bind(this)); + event.dispatcher.on(event.test.started, this.testStarted.bind(this)); + event.dispatcher.on(event.test.skipped, this.testSkipped.bind(this)); + event.dispatcher.on(event.test.passed, this.testPassed.bind(this)); + event.dispatcher.on(event.test.failed, this.testFailed.bind(this)); // Step - event.dispatcher.addListener(event.step.started, this.stepStarted.bind(this)); - event.dispatcher.addListener(event.step.passed, this.stepPassed.bind(this)); - event.dispatcher.addListener(event.step.failed, this.stepFailed.bind(this)); - event.dispatcher.addListener(event.step.comment, this.stepComment.bind(this)); + event.dispatcher.on(event.step.started, this.stepStarted.bind(this)); + event.dispatcher.on(event.step.passed, this.stepPassed.bind(this)); + event.dispatcher.on(event.step.failed, this.stepFailed.bind(this)); + event.dispatcher.on(event.step.comment, this.stepComment.bind(this)); + // run + event.dispatcher.on(event.all.after, this.afterAll.bind(this)); } suiteBefore() { - this.allureRuntime!.startScope(); + const scopeUuid = this.allureRuntime.startScope(); + this.scopeUuids.push(scopeUuid); } suiteAfter() { - this.allureRuntime!.writeScope(); + const suiteUuid = this.scopeUuids.pop(); + if (suiteUuid) { + this.allureRuntime.writeScope(suiteUuid); + } } hookStarted(hook: CodeceptHook) { @@ -118,7 +118,12 @@ export class AllureCodeceptJsReporter { const hookType = currentRunnable!.title.match(/^"(?.+)" hook/)!.groups!.hookType; const fixtureType = /before/.test(hookType) ? "before" : "after"; - this.allureRuntime!.startFixture(fixtureType, { + const scopeUuid = this.scopeUuids.length > 0 ? this.scopeUuids[this.scopeUuids.length - 1] : undefined; + if (!scopeUuid) { + return; + } + + this.currentFixtureUuid = this.allureRuntime.startFixture(scopeUuid, fixtureType, { name: currentRunnable!.title, stage: Stage.RUNNING, start: Date.now(), @@ -126,19 +131,20 @@ export class AllureCodeceptJsReporter { } hookPassed() { - this.allureRuntime!.updateFixture((result) => { + if (!this.currentFixtureUuid) { + return; + } + this.allureRuntime.updateFixture(this.currentFixtureUuid, (result) => { result.status = Status.PASSED; result.stage = Stage.FINISHED; }); - const fixtureUuid = this.allureRuntime!.stopFixture({ stop: Date.now() }); - - this.allureRuntime!.writeFixture(fixtureUuid); + this.allureRuntime.stopFixture(this.currentFixtureUuid); } testStarted(test: CodeceptTest & { tags: string[] }) { // test has been already started - if (this.currentAllureResultUuid) { + if (this.currentTestUuid) { return; } @@ -146,26 +152,26 @@ export class AllureCodeceptJsReporter { } testFailed(test: CodeceptTest, err: CodeceptError) { - if (!this.currentAllureResultUuid) { + if (!this.currentTestUuid) { return; } - this.allureRuntime!.updateTest((result) => { + this.allureRuntime.updateTest(this.currentTestUuid, (result) => { result.status = Status.FAILED; // @ts-ignore result.statusDetails = getMessageAndTraceFromError(err); - }, this.currentAllureResultUuid); + }); this.closeCurrentAllureTest(test); } testPassed(test: CodeceptTest) { - if (!this.currentAllureResultUuid) { + if (!this.currentTestUuid) { return; } - this.allureRuntime!.updateTest((result) => { + this.allureRuntime.updateTest(this.currentTestUuid, (result) => { result.status = Status.PASSED; - }, this.currentAllureResultUuid); + }); this.closeCurrentAllureTest(test); } @@ -179,48 +185,63 @@ export class AllureCodeceptJsReporter { }; }, ) { - if (!this.currentAllureResultUuid) { + if (!this.currentTestUuid) { return; } - this.allureRuntime!.updateTest((result) => { + this.allureRuntime.updateTest(this.currentTestUuid, (result) => { result.status = Status.SKIPPED; if (test.opts.skipInfo) { result.statusDetails = { message: test.opts.skipInfo.message }; } - }, this.currentAllureResultUuid); + }); this.closeCurrentAllureTest(test); } stepStarted(step: CodeceptStep) { - this.allureRuntime!.startStep( - { - name: `${step.actor} ${step.name}`, - }, - this.currentAllureResultUuid, - ); + if (!this.currentTestUuid) { + return; + } + this.allureRuntime.startStep(this.currentTestUuid, undefined, { + name: `${step.actor} ${step.name}`, + }); } stepFailed() { - this.allureRuntime!.updateStep((result) => { + this.stopCurrentStep((result) => { result.status = Status.FAILED; - }, this.currentAllureResultUuid); - this.closeCurrentAllureStep(); + result.stage = Stage.FINISHED; + }); } stepComment() { - this.allureRuntime!.updateStep((result) => { + this.stopCurrentStep((result) => { result.status = Status.PASSED; - }, this.currentAllureResultUuid); - this.closeCurrentAllureStep(); + result.stage = Stage.FINISHED; + }); } stepPassed() { - this.allureRuntime!.updateStep((result) => { + this.stopCurrentStep((result) => { result.status = Status.PASSED; - }, this.currentAllureResultUuid); - this.closeCurrentAllureStep(); + result.stage = Stage.FINISHED; + }); + } + + stopCurrentStep(updateFunc: (result: StepResult) => void) { + const currentStep = this.currentTestUuid ? this.allureRuntime.currentStep(this.currentTestUuid) : undefined; + + if (!currentStep) { + return; + } + this.allureRuntime.updateStep(currentStep, updateFunc); + this.allureRuntime.stopStep(currentStep); + } + + afterAll() { + this.allureRuntime.writeEnvironmentInfo(); + this.allureRuntime.writeCategoriesDefinitions(); } // TODO: not implemented in the new version at all @@ -235,6 +256,9 @@ export class AllureCodeceptJsReporter { // } handleRuntimeMessage(message: RuntimeMessage) { - this.allureRuntime!.applyRuntimeMessages([message], { testUuid: this.currentAllureResultUuid! }); + if (!this.currentTestUuid) { + return; + } + this.allureRuntime.applyRuntimeMessages(this.currentTestUuid, [message]); } } diff --git a/packages/allure-codeceptjs/test/fixtures/codecept.conf.js b/packages/allure-codeceptjs/test/fixtures/codecept.conf.js index 0404431e4..86ee8617e 100644 --- a/packages/allure-codeceptjs/test/fixtures/codecept.conf.js +++ b/packages/allure-codeceptjs/test/fixtures/codecept.conf.js @@ -11,6 +11,18 @@ exports.config = { require: require.resolve("allure-codeceptjs"), testMode: true, enabled: true, + environmentInfo: { + "app version": "123.0.1", + "some other key": "some other value", + }, + categories: [ + { + name: "first", + }, + { + name: "second", + }, + ], }, }, helpers: { diff --git a/packages/allure-codeceptjs/test/spec/categories.test.ts b/packages/allure-codeceptjs/test/spec/categories.test.ts new file mode 100644 index 000000000..38faf041d --- /dev/null +++ b/packages/allure-codeceptjs/test/spec/categories.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { runCodeceptJsInlineTest } from "../utils.js"; + +describe("categories", () => { + it("should support categories", async () => { + const { categories } = await runCodeceptJsInlineTest({ + "nested/login.test.js": ` + Feature("login-feature"); + Scenario("login-scenario1", async () => {}); + Scenario("login-scenario2", async () => {}); + `, + }); + + expect(categories).toEqual(expect.arrayContaining([{ name: "first" }, { name: "second" }])); + }); +}); diff --git a/packages/allure-codeceptjs/test/spec/environmentInfo.test.ts b/packages/allure-codeceptjs/test/spec/environmentInfo.test.ts new file mode 100644 index 000000000..985f6ed82 --- /dev/null +++ b/packages/allure-codeceptjs/test/spec/environmentInfo.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { runCodeceptJsInlineTest } from "../utils.js"; + +describe("environment info", () => { + it("should add environmentInfo", async () => { + const { envInfo } = await runCodeceptJsInlineTest({ + "nested/login.test.js": ` + Feature("login-feature"); + Scenario("login-scenario1", async () => {}); + Scenario("login-scenario2", async () => {}); + `, + }); + + expect(envInfo).toEqual({ "app version": "123.0.1", "some other key": "some other value" }); + }); +}); diff --git a/packages/allure-codeceptjs/test/spec/tags.test.ts b/packages/allure-codeceptjs/test/spec/tags.test.ts index 9705aaa35..426941e8b 100644 --- a/packages/allure-codeceptjs/test/spec/tags.test.ts +++ b/packages/allure-codeceptjs/test/spec/tags.test.ts @@ -24,11 +24,11 @@ it("supports codecept tags", async () => { labels: expect.arrayContaining([ { name: LabelName.TAG, - value: "@slow", + value: "slow", }, { name: LabelName.TAG, - value: "@important", + value: "important", }, { name: LabelName.OWNER, diff --git a/packages/allure-cucumberjs/package.json b/packages/allure-cucumberjs/package.json index dd74a5857..13dc6b680 100644 --- a/packages/allure-cucumberjs/package.json +++ b/packages/allure-cucumberjs/package.json @@ -46,12 +46,6 @@ "test": "vitest run" }, "dependencies": { - "@cucumber/cucumber": "^10.4.0", - "@cucumber/gherkin": "^28.0.0", - "@cucumber/gherkin-streams": "^5.0.1", - "@cucumber/gherkin-utils": "^9.0.0", - "@cucumber/message-streams": "^4.0.1", - "@cucumber/messages": "^24.1.0", "allure-js-commons": "workspace:*" }, "devDependencies": { @@ -60,6 +54,12 @@ "@babel/plugin-transform-modules-commonjs": "^7.24.6", "@babel/preset-env": "^7.24.6", "@babel/preset-typescript": "^7.24.6", + "@cucumber/cucumber": "^10.8.0", + "@cucumber/gherkin": "^28.0.0", + "@cucumber/gherkin-streams": "^5.0.1", + "@cucumber/gherkin-utils": "^9.0.0", + "@cucumber/message-streams": "^4.0.1", + "@cucumber/messages": "^25.0.1", "@types/babel__core": "^7", "@types/babel__preset-env": "^7", "@types/chai": "^4.3.6", @@ -90,5 +90,8 @@ "ts-node": "^10.9.1", "typescript": "^5.2.2", "vitest": "^1.6.0" + }, + "peerDependencies": { + "@cucumber/cucumber": "^10.8.0" } } diff --git a/packages/allure-cucumberjs/src/index.ts b/packages/allure-cucumberjs/src/index.ts index 99e8b6b1e..fed60bae7 100644 --- a/packages/allure-cucumberjs/src/index.ts +++ b/packages/allure-cucumberjs/src/index.ts @@ -1,19 +1,29 @@ -import { Before } from "@cucumber/cucumber"; +import { Before, BeforeAll, world } from "@cucumber/cucumber"; +import { includedInTestPlan } from "allure-js-commons/sdk/reporter"; +import { parseTestPlan } from "allure-js-commons/sdk/reporter"; import { setGlobalTestRuntime } from "allure-js-commons/sdk/runtime"; import { AllureCucumberWorld } from "./legacy.js"; -import { ALLURE_SETUP_REPORTER_HOOK } from "./model.js"; import { AllureCucumberTestRuntime } from "./runtime.js"; -Before({ name: ALLURE_SETUP_REPORTER_HOOK }, function () { - // TODO: we can implement testplan logic there - setGlobalTestRuntime( - // @ts-ignore - new AllureCucumberTestRuntime({ - attach: this.attach, - log: this.log, - parameters: this.parameters, - }), - ); +BeforeAll(() => { + setGlobalTestRuntime(new AllureCucumberTestRuntime()); +}); + +Before({ name: "ALLURE_FIXTURE_IGNORE" }, (scenario) => { + const testPlan = parseTestPlan(); + if (!testPlan) { + return; + } + const pickle = scenario.pickle; + const fullName = `${pickle.uri}#${pickle.name}`; + const tags = pickle.tags.map((tag) => tag.name); + + if (!includedInTestPlan(testPlan, { fullName, tags })) { + // we can't use regular message or Allure facade since we need label to be added + // to test, not fixture + world.attach(Buffer.from("allure-skip"), "application/vnd.allure.skipcucumber+json"); + return "skipped"; + } }); export { AllureCucumberTestRuntime, AllureCucumberWorld }; diff --git a/packages/allure-cucumberjs/src/reporter.ts b/packages/allure-cucumberjs/src/reporter.ts index e124fb4bb..664a03265 100644 --- a/packages/allure-cucumberjs/src/reporter.ts +++ b/packages/allure-cucumberjs/src/reporter.ts @@ -1,13 +1,19 @@ import type { IFormatterOptions, TestCaseHookDefinition } from "@cucumber/cucumber"; import { Formatter, World } from "@cucumber/cucumber"; import type * as messages from "@cucumber/messages"; -import { AttachmentContentEncoding, type PickleTag, type Tag, type TestStepResult } from "@cucumber/messages"; -import { TestStepResultStatus } from "@cucumber/messages"; +import { + AttachmentContentEncoding, + type PickleTag, + type Tag, + type TestStepResult, + TestStepResultStatus, +} from "@cucumber/messages"; import os from "node:os"; import { extname } from "node:path"; import process from "node:process"; -import { ContentType, LabelName, Stage, Status } from "allure-js-commons"; import type { Label, Link, TestResult } from "allure-js-commons"; +import { ContentType, LabelName, Stage, Status } from "allure-js-commons"; +import { getMessageAndTraceFromError } from "allure-js-commons/sdk"; import { ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE, FileSystemWriter, @@ -20,7 +26,6 @@ import { } from "allure-js-commons/sdk/reporter"; import { AllureCucumberWorld } from "./legacy.js"; import type { AllureCucumberLinkConfig, AllureCucumberReporterConfig, LabelConfig } from "./model.js"; -import { ALLURE_SETUP_REPORTER_HOOK } from "./model.js"; const { ALLURE_THREAD_NAME } = process.env; @@ -40,7 +45,9 @@ export default class AllureCucumberReporter extends Formatter { private readonly pickleMap: Map = new Map(); private readonly testCaseMap: Map = new Map(); private readonly testCaseStartedMap: Map = new Map(); - private readonly allureResultsUuids: Map = new Map(); + private readonly testResultUuids: Map = new Map(); + private readonly scopeUuids: Map = new Map(); + private readonly fixtureUuids: Map = new Map(); constructor(options: IFormatterOptions) { super(options); @@ -115,6 +122,9 @@ export default class AllureCucumberReporter extends Formatter { case !!envelope.testStepFinished: this.onTestStepFinished(envelope.testStepFinished); break; + case !!envelope.testRunFinished: + this.onTestRunFinished(); + break; } } @@ -288,10 +298,13 @@ export default class AllureCucumberReporter extends Formatter { result.labels!.push(...featureLabels, ...scenarioLabels, ...pickleLabels); result.links!.push(...featureLinks, ...scenarioLinks); - const testUuid = this.allureRuntime.startTest(result); + const scopeUuid = this.allureRuntime.startScope(); + this.scopeUuids.set(data.id, scopeUuid); + + const testUuid = this.allureRuntime.startTest(result, [scopeUuid]); + this.testResultUuids.set(data.id, testUuid); + this.testCaseStartedMap.set(data.id, data); - this.allureResultsUuids.set(data.id, testUuid); - this.allureRuntime.startScope(); if (!scenario?.examples) { return; @@ -308,19 +321,20 @@ export default class AllureCucumberReporter extends Formatter { const csvDataTable = `${csvDataTableHeader}\n${csvDataTableBody}\n`; - this.allureRuntime.writeAttachment( - "Examples", - Buffer.from(csvDataTable, "utf-8"), - { contentType: ContentType.CSV, fileExtension: ".csv" }, - testUuid, - ); + this.allureRuntime.writeAttachment(testUuid, null, "Examples", Buffer.from(csvDataTable, "utf-8"), { + contentType: ContentType.CSV, + fileExtension: ".csv", + }); }); } private onTestCaseFinished(data: messages.TestCaseFinished) { - const testUuid = this.allureResultsUuids.get(data.testCaseStartedId)!; + const testUuid = this.testResultUuids.get(data.testCaseStartedId); + if (!testUuid) { + return; + } - this.allureRuntime.updateTest((result) => { + this.allureRuntime.updateTest(testUuid, (result) => { result.status = result.steps.length > 0 ? getWorstStepResultStatus(result.steps) : Status.PASSED; result.stage = Stage.FINISHED; @@ -329,36 +343,46 @@ export default class AllureCucumberReporter extends Formatter { message: "The test doesn't have an implementation.", }; } - }, testUuid); - this.allureRuntime.stopTest({ uuid: testUuid, stop: Date.now() }); + }); + this.allureRuntime.stopTest(testUuid); this.allureRuntime.writeTest(testUuid); + this.testResultUuids.delete(data.testCaseStartedId); - this.allureRuntime.stopScope(); + const scopeUuid = this.scopeUuids.get(data.testCaseStartedId); + if (scopeUuid) { + this.allureRuntime.writeScope(scopeUuid); + this.scopeUuids.delete(data.testCaseStartedId); + } } private onTestStepStarted(data: messages.TestStepStarted) { - const testUuid = this.allureResultsUuids.get(data.testCaseStartedId)!; + const testUuid = this.testResultUuids.get(data.testCaseStartedId)!; const step = this.testStepMap.get(data.testStepId)!; - const beforeHook = step.hookId && this.beforeHooks[step.hookId]; - const afterHook = step.hookId && this.afterHooks[step.hookId]; + if (step.hookId) { + const scopeUuid = this.scopeUuids.get(data.testCaseStartedId); + if (!scopeUuid) { + return; + } - if (beforeHook && beforeHook.name === ALLURE_SETUP_REPORTER_HOOK) { - return; - } + const beforeHook = step.hookId && this.beforeHooks[step.hookId]; + const afterHook = step.hookId && this.afterHooks[step.hookId]; - if (beforeHook) { - this.allureRuntime.startFixture("before", { - name: beforeHook.name, - stage: Stage.RUNNING, - }); - return; - } + const type = beforeHook ? "before" : afterHook ? "after" : undefined; + if (!type) { + return; + } + const name = beforeHook ? beforeHook.name : afterHook ? afterHook.name : "hook"; + if (name === "ALLURE_FIXTURE_IGNORE") { + return; + } - if (afterHook) { - this.allureRuntime.startFixture("after", { - name: afterHook.name, + const fixtureUuid = this.allureRuntime.startFixture(scopeUuid, type, { + name, stage: Stage.RUNNING, }); + if (fixtureUuid) { + this.fixtureUuids.set(data.testCaseStartedId, fixtureUuid); + } return; } @@ -377,13 +401,12 @@ export default class AllureCucumberReporter extends Formatter { .map((astNodeId) => this.stepMap.get(astNodeId)) .map((stepAstNode) => stepAstNode?.keyword) .find((keyword) => keyword !== undefined) || ""; - const stepResult = { + + const stepUuid = this.allureRuntime.startStep(testUuid, undefined, { ...createStepResult(), name: `${stepKeyword}${stepPickle.text}`, start: data.timestamp.nanos, - }; - - this.allureRuntime.startStep(stepResult, testUuid); + }); if (!stepPickle.argument?.dataTable) { return; @@ -394,53 +417,52 @@ export default class AllureCucumberReporter extends Formatter { "", ); - this.allureRuntime.writeAttachment( - "Data table", - Buffer.from(csvDataTable, "utf-8"), - { - contentType: ContentType.CSV, - fileExtension: ".csv", - }, - testUuid, - ); + this.allureRuntime.writeAttachment(testUuid, stepUuid, "Data table", Buffer.from(csvDataTable, "utf-8"), { + contentType: ContentType.CSV, + fileExtension: ".csv", + }); } private onTestStepFinished(data: messages.TestStepFinished) { - const step = this.testStepMap.get(data.testStepId)!; - const beforeHook = step.hookId && this.beforeHooks[step.hookId]; - const afterHook = step.hookId && this.afterHooks[step.hookId]; - - if (beforeHook && beforeHook.name === ALLURE_SETUP_REPORTER_HOOK) { + const step = this.testStepMap.get(data.testStepId); + if (!step) { return; } const status = this.parseStatus(data.testStepResult); const stage = status !== Status.SKIPPED ? Stage.FINISHED : Stage.PENDING; - if (beforeHook || afterHook) { - this.allureRuntime.updateFixture((r) => { + if (step.hookId) { + const fixtureUuid = this.fixtureUuids.get(data.testCaseStartedId); + if (!fixtureUuid) { + return; + } + + this.allureRuntime.updateFixture(fixtureUuid, (r) => { r.stage = stage; r.status = status; if (data.testStepResult.exception) { - r.statusDetails = { + r.statusDetails = getMessageAndTraceFromError({ message: data.testStepResult.message, - trace: data.testStepResult.exception.stackTrace, - }; + stack: data.testStepResult.exception.stackTrace, + }); } }); - this.allureRuntime.writeFixture(); + // TODO stop from duration? use data.timestamp.nanos? + this.allureRuntime.stopFixture(fixtureUuid); + this.fixtureUuids.delete(data.testCaseStartedId); return; } - const testUuid = this.allureResultsUuids.get(data.testCaseStartedId)!; - const currentStep = this.allureRuntime.getCurrentStep(testUuid); + const testUuid = this.testResultUuids.get(data.testCaseStartedId)!; + const currentStep = this.allureRuntime.currentStep(testUuid); if (!currentStep) { return; } - this.allureRuntime.updateStep((r) => { + this.allureRuntime.updateStep(currentStep, (r) => { r.status = status; r.stage = stage; @@ -457,12 +479,10 @@ export default class AllureCucumberReporter extends Formatter { trace: data.testStepResult.exception.stackTrace, }; } - }, testUuid); - - this.allureRuntime.stopStep({ - uuid: testUuid, - stop: currentStep.start! + data.timestamp.nanos, }); + + // TODO stop from duration? use data.timestamp.nanos? + this.allureRuntime.stopStep(currentStep); } private onAttachment(message: messages.Attachment): void { @@ -470,34 +490,49 @@ export default class AllureCucumberReporter extends Formatter { return; } - const testUuid = this.allureResultsUuids.get(message.testCaseStartedId)!; + const fixtureUuid = this.fixtureUuids.get(message.testCaseStartedId); + const testUuid = this.testResultUuids.get(message.testCaseStartedId); + const rootUuid = fixtureUuid ?? testUuid; + if (!rootUuid) { + return; + } + + if (message.mediaType === "application/vnd.allure.skipcucumber+json") { + if (testUuid) { + this.allureRuntime.updateTest(testUuid, (result) => { + result.labels.push({ name: "ALLURE_TESTPLAN_SKIP", value: "true" }); + }); + } + return; + } if (message.mediaType === ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE) { const parsedMessage = JSON.parse(message.body); - this.allureRuntime.applyRuntimeMessages(Array.isArray(parsedMessage) ? parsedMessage : [parsedMessage], { - testUuid, - }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.allureRuntime.applyRuntimeMessages(rootUuid, Array.isArray(parsedMessage) ? parsedMessage : [parsedMessage]); return; } const encoding: BufferEncoding = message.contentEncoding === AttachmentContentEncoding.BASE64 ? "base64" : "utf-8"; - this.allureRuntime.applyRuntimeMessages( - [ - { - type: "attachment_content", - data: { - name: message.fileName ?? "Attachment", - content: Buffer.from(message.body, encoding).toString("base64"), - encoding: "base64", - contentType: message.mediaType, - fileExtension: message.fileName ? extname(message.fileName) : undefined, - wrapInStep: true, - }, + this.allureRuntime.applyRuntimeMessages(rootUuid, [ + { + type: "attachment_content", + data: { + name: message.fileName ?? "Attachment", + content: Buffer.from(message.body, encoding).toString("base64"), + encoding: "base64", + contentType: message.mediaType, + fileExtension: message.fileName ? extname(message.fileName) : undefined, + wrapInStep: true, }, - ], - { testUuid }, - ); + }, + ]); + } + + private onTestRunFinished() { + this.allureRuntime.writeCategoriesDefinitions(); + this.allureRuntime.writeEnvironmentInfo(); } } diff --git a/packages/allure-cucumberjs/src/runtime.ts b/packages/allure-cucumberjs/src/runtime.ts index acd170a4e..dcbb1f9a1 100644 --- a/packages/allure-cucumberjs/src/runtime.ts +++ b/packages/allure-cucumberjs/src/runtime.ts @@ -1,22 +1,11 @@ -import type { IWorld, IWorldOptions } from "@cucumber/cucumber"; +import { world } from "@cucumber/cucumber"; import type { RuntimeMessage } from "allure-js-commons/sdk"; import { ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE } from "allure-js-commons/sdk/reporter"; import { MessageTestRuntime } from "allure-js-commons/sdk/runtime"; -export class AllureCucumberTestRuntime extends MessageTestRuntime implements IWorld { - public readonly attach: IWorldOptions["attach"]; - public readonly log: IWorldOptions["log"]; - public readonly parameters: IWorldOptions["parameters"]; - - constructor({ attach, log, parameters }: IWorldOptions) { - super(); - this.attach = attach; - this.log = log; - this.parameters = parameters; - } - +export class AllureCucumberTestRuntime extends MessageTestRuntime { async sendMessage(message: RuntimeMessage) { - this.attach(JSON.stringify(message), ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE as string); + world.attach(JSON.stringify(message), ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE as string); await Promise.resolve(); } } diff --git a/packages/allure-cucumberjs/test/fixtures/features/testplan1.feature b/packages/allure-cucumberjs/test/fixtures/features/testplan1.feature new file mode 100644 index 000000000..a9ec51e8b --- /dev/null +++ b/packages/allure-cucumberjs/test/fixtures/features/testplan1.feature @@ -0,0 +1,11 @@ +Feature: test plan 1 + + Scenario: test 1 + Given a step + + @allure.id=87 + Scenario: test 2 + Given a step + + Scenario: test 3 + Given a step diff --git a/packages/allure-cucumberjs/test/fixtures/features/testplan2.feature b/packages/allure-cucumberjs/test/fixtures/features/testplan2.feature new file mode 100644 index 000000000..eb1a3c44c --- /dev/null +++ b/packages/allure-cucumberjs/test/fixtures/features/testplan2.feature @@ -0,0 +1,11 @@ +Feature: test plan 2 + + Scenario: test 4 + Given a step + + Scenario: test 5 + Given a step + + @allure.id=123 + Scenario: test 6 + Given a step diff --git a/packages/allure-cucumberjs/test/fixtures/support/hooksSteps.cjs b/packages/allure-cucumberjs/test/fixtures/support/hooksSteps.cjs new file mode 100644 index 000000000..578afa656 --- /dev/null +++ b/packages/allure-cucumberjs/test/fixtures/support/hooksSteps.cjs @@ -0,0 +1,17 @@ +const { Given, Before, After } = require("@cucumber/cucumber"); +const { step } = require("allure-js-commons"); + +Before(async () => { + await step("before step 1", () => {}); + await step("before step 2", () => {}); +}); + +After(async () => { + await step("after step 1", () => {}); + await step("after step 2", () => {}); +}); + +Given("a passed step", async () => { + await step("sub step 1", () => {}); + await step("sub step 2", () => {}); +}); diff --git a/packages/allure-cucumberjs/test/fixtures/support/testplan.cjs b/packages/allure-cucumberjs/test/fixtures/support/testplan.cjs new file mode 100644 index 000000000..a5c1d4a1f --- /dev/null +++ b/packages/allure-cucumberjs/test/fixtures/support/testplan.cjs @@ -0,0 +1,3 @@ +const { Given } = require("@cucumber/cucumber"); + +Given("a step", () => {}); diff --git a/packages/allure-cucumberjs/test/spec/categories.test.ts b/packages/allure-cucumberjs/test/spec/categories.test.ts new file mode 100644 index 000000000..7dc866eb2 --- /dev/null +++ b/packages/allure-cucumberjs/test/spec/categories.test.ts @@ -0,0 +1,7 @@ +import { expect, it } from "vitest"; +import { runCucumberInlineTest } from "../utils.js"; + +it("should add categories", async () => { + const { categories } = await runCucumberInlineTest(["simple"], ["simple"]); + expect(categories).toEqual(expect.arrayContaining([{ name: "first" }, { name: "second" }])); +}); diff --git a/packages/allure-cucumberjs/test/spec/dataTable.test.ts b/packages/allure-cucumberjs/test/spec/dataTable.test.ts index df8a95cfa..b175d8a47 100644 --- a/packages/allure-cucumberjs/test/spec/dataTable.test.ts +++ b/packages/allure-cucumberjs/test/spec/dataTable.test.ts @@ -6,7 +6,7 @@ it("handles data tables", async () => { const { tests, attachments } = await runCucumberInlineTest(["dataTable"], ["dataTable"]); expect(tests).toHaveLength(1); - expect(tests[0].steps).toHaveLength(3); + expect(tests[0].steps).toContainEqual( expect.objectContaining({ name: "Given a table step", diff --git a/packages/allure-cucumberjs/test/spec/environmentInfo.test.ts b/packages/allure-cucumberjs/test/spec/environmentInfo.test.ts new file mode 100644 index 000000000..39a566836 --- /dev/null +++ b/packages/allure-cucumberjs/test/spec/environmentInfo.test.ts @@ -0,0 +1,7 @@ +import { expect, it } from "vitest"; +import { runCucumberInlineTest } from "../utils.js"; + +it("should add environment info", async () => { + const { envInfo } = await runCucumberInlineTest(["simple"], ["simple"]); + expect(envInfo).toEqual({ "app version": "123.0.1", "some other key": "some other value" }); +}); diff --git a/packages/allure-cucumberjs/test/spec/hooks.test.ts b/packages/allure-cucumberjs/test/spec/hooks.test.ts index 730c51a30..f3c22db79 100644 --- a/packages/allure-cucumberjs/test/spec/hooks.test.ts +++ b/packages/allure-cucumberjs/test/spec/hooks.test.ts @@ -57,7 +57,6 @@ it("handles failed hooks", async () => { steps: expect.arrayContaining([ expect.objectContaining({ status: Status.SKIPPED, - stage: Stage.FINISHED, }), ]), }), @@ -93,3 +92,70 @@ it("handles failed hooks", async () => { ]), ); }); + +it("handles hooks with steps", async () => { + const { tests, groups } = await runCucumberInlineTest(["hooks"], ["hooksSteps"]); + + expect(tests).toHaveLength(1); + const [testResult] = tests; + + expect(testResult.steps).toEqual([ + expect.objectContaining({ + name: "Given a passed step", + status: Status.PASSED, + steps: [ + expect.objectContaining({ + name: "sub step 1", + status: Status.PASSED, + }), + expect.objectContaining({ + name: "sub step 2", + status: Status.PASSED, + }), + ], + }), + ]); + expect(groups).toHaveLength(2); + expect(groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + befores: [ + expect.objectContaining({ + status: Status.PASSED, + stage: Stage.FINISHED, + steps: [ + expect.objectContaining({ + name: "before step 1", + status: Status.PASSED, + }), + expect.objectContaining({ + name: "before step 2", + status: Status.PASSED, + }), + ], + }), + ], + afters: [], + }), + expect.objectContaining({ + befores: [], + afters: [ + expect.objectContaining({ + status: Status.PASSED, + stage: Stage.FINISHED, + steps: [ + expect.objectContaining({ + name: "after step 1", + status: Status.PASSED, + }), + expect.objectContaining({ + name: "after step 2", + status: Status.PASSED, + }), + ], + }), + ], + }), + ]), + ); +}); diff --git a/packages/allure-cucumberjs/test/spec/testplan.test.ts b/packages/allure-cucumberjs/test/spec/testplan.test.ts new file mode 100644 index 000000000..41dbefe86 --- /dev/null +++ b/packages/allure-cucumberjs/test/spec/testplan.test.ts @@ -0,0 +1,59 @@ +import { expect, it } from "vitest"; +import type { TestPlanV1 } from "allure-js-commons/sdk"; +import { runCucumberInlineTest } from "../utils.js"; + +it("should skip tests based on test plan", async () => { + const testPlan: TestPlanV1 = { + version: "1.0", + tests: [ + { + id: "87", + }, + { + id: "123", + selector: "invalid", + }, + { + selector: "features/testplan2.feature#test 5", + }, + ], + }; + + const { tests } = await runCucumberInlineTest(["testplan1", "testplan2"], ["testplan"], true, testPlan); + + expect(tests).not.toContainEqual( + expect.objectContaining({ + name: "test 1", + }), + ); + + expect(tests).toContainEqual( + expect.objectContaining({ + name: "test 2", + }), + ); + + expect(tests).not.toContainEqual( + expect.objectContaining({ + name: "test 3", + }), + ); + + expect(tests).not.toContainEqual( + expect.objectContaining({ + name: "test 4", + }), + ); + + expect(tests).toContainEqual( + expect.objectContaining({ + name: "test 5", + }), + ); + + expect(tests).toContainEqual( + expect.objectContaining({ + name: "test 6", + }), + ); +}); diff --git a/packages/allure-cucumberjs/test/utils.ts b/packages/allure-cucumberjs/test/utils.ts index 1daf22554..38fcd84d6 100644 --- a/packages/allure-cucumberjs/test/utils.ts +++ b/packages/allure-cucumberjs/test/utils.ts @@ -3,13 +3,14 @@ import { randomUUID } from "node:crypto"; import { copyFile, mkdir, rm, writeFile } from "node:fs/promises"; import { dirname, join, resolve as resolvePath } from "node:path"; import { attachment, attachmentPath, step } from "allure-js-commons"; -import type { AllureResults } from "allure-js-commons/sdk"; +import type { AllureResults, TestPlanV1 } from "allure-js-commons/sdk"; import { MessageReader } from "allure-js-commons/sdk/reporter"; export const runCucumberInlineTest = async ( features: string[], stepsDefs: string[], parallel: boolean = true, + testPlan?: TestPlanV1, ): Promise => { const fixturesPath = join(__dirname, "fixtures"); const testDir = join(__dirname, "fixtures/temp", randomUUID()); @@ -45,6 +46,15 @@ export const runCucumberInlineTest = async ( urlTemplate: "https://example.com/tasks/%s", }, }, + environmentInfo: { + "app version": "123.0.1", + "some other key": "some other value" + }, + categories: [{ + name: "first" + },{ + name: "second" + }], } } } @@ -110,6 +120,17 @@ export const runCucumberInlineTest = async ( }); } + const env: Record = {}; + if (testPlan) { + await step("testplan.json", async () => { + const data = JSON.stringify(testPlan); + const testPlanPath = join(testDir, "testplan.json"); + await writeFile(testPlanPath, data, "utf8"); + env.ALLURE_TESTPLAN_PATH = testPlanPath; + await attachment("testplan.json", data, { contentType: "application/json", fileExtension: ".json" }); + }); + } + const modulePath = await step("resolve @cucumber/cucumber", () => { return resolvePath(require.resolve("@cucumber/cucumber"), "../../bin/cucumber-js"); }); @@ -118,6 +139,7 @@ export const runCucumberInlineTest = async ( return fork(modulePath, args, { env: { ...process.env, + ...env, }, cwd: testDir, stdio: "pipe", diff --git a/packages/allure-cypress/src/index.ts b/packages/allure-cypress/src/index.ts index 8d92b964a..a37e32525 100644 --- a/packages/allure-cypress/src/index.ts +++ b/packages/allure-cypress/src/index.ts @@ -1,21 +1,26 @@ import type { AttachmentOptions, Label, Link, ParameterMode, ParameterOptions, StatusDetails } from "allure-js-commons"; -import { ContentType, Stage, Status } from "allure-js-commons"; +import { ContentType, Status } from "allure-js-commons"; import type { RuntimeMessage } from "allure-js-commons/sdk"; import { getUnfinishedStepsMessages, hasStepMessage } from "allure-js-commons/sdk"; import type { TestRuntime } from "allure-js-commons/sdk/runtime"; import { getGlobalTestRuntime, setGlobalTestRuntime } from "allure-js-commons/sdk/runtime"; import type { CypressCommand, - CypressHookStartRuntimeMessage, - CypressRuntimeMessage, + CypressCommandStartMessage, + CypressHook, + CypressHookStartMessage, + CypressMessage, CypressTest, - CypressTestStartRuntimeMessage, + CypressTestStartMessage, } from "./model.js"; import { ALLURE_REPORT_SHUTDOWN_HOOK, ALLURE_REPORT_STEP_COMMAND } from "./model.js"; import { + getHookType, getSuitePath, isCommandShouldBeSkipped, + isGlobalHook, normalizeAttachmentContentEncoding, + toReversed, uint8ArrayToBase64, } from "./utils.js"; @@ -135,7 +140,10 @@ export class AllureCypressTestRuntime implements TestRuntime { .then(() => { this.sendMessage({ type: "step_start", - data: { name, start: Date.now() }, + data: { + name, + start: Date.now(), + }, }); return Cypress.Promise.resolve(body()); @@ -145,7 +153,6 @@ export class AllureCypressTestRuntime implements TestRuntime { type: "step_stop", data: { status: Status.PASSED, - stage: Stage.FINISHED, stop: Date.now(), }, }).then(() => result); @@ -155,7 +162,9 @@ export class AllureCypressTestRuntime implements TestRuntime { stepDisplayName(name: string) { return this.sendMessageAsync({ type: "step_metadata", - data: { name }, + data: { + name, + }, }); } @@ -168,14 +177,22 @@ export class AllureCypressTestRuntime implements TestRuntime { }); } - sendMessage(message: CypressRuntimeMessage) { + sendMessage(message: CypressMessage) { const messages = Cypress.env("allureRuntimeMessages") || []; Cypress.env("allureRuntimeMessages", messages.concat(message)); } - sendMessageAsync(message: CypressRuntimeMessage): PromiseLike { - this.sendMessage(message); + sendMessageAsync({ type, data }: CypressMessage): PromiseLike { + this.sendMessage({ + type, + data: { + ...data, + // a little hack to avoid additional types definition + // @ts-ignore + cypressTestId: Cypress.state("test")?.id ?? "", + }, + }); return Cypress.Promise.resolve(); } @@ -183,6 +200,7 @@ export class AllureCypressTestRuntime implements TestRuntime { const { EVENT_RUN_BEGIN, + EVENT_RUN_END, EVENT_TEST_BEGIN, EVENT_TEST_FAIL, EVENT_TEST_PASS, @@ -211,40 +229,50 @@ const initializeAllure = () => { setGlobalTestRuntime(testRuntime); }) - .on(EVENT_HOOK_BEGIN, (hook: Mocha.Hook) => { - const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; - + .on(EVENT_HOOK_BEGIN, (hook: CypressHook) => { if (hook.title.includes(ALLURE_REPORT_SHUTDOWN_HOOK)) { return; } - return testRuntime.sendMessageAsync({ + const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; + // @ts-ignore + const testId: string | undefined = Cypress.state()?.test?.id; + + testRuntime.sendMessageAsync({ type: "cypress_hook_start", data: { + id: testId ? `${testId}:${hook.hookId}` : "", + parentId: hook.parent.id, name: hook.title, - type: /after/.test(hook.title) ? "after" : "before", + type: getHookType(hook.title), start: Date.now(), - global: /(before|after) all/.test(hook.title), + global: isGlobalHook(hook.title), }, }); }) - .on(EVENT_HOOK_END, (hook: Mocha.Hook) => { + .on(EVENT_HOOK_END, (hook: CypressHook) => { if (hook.title.includes(ALLURE_REPORT_SHUTDOWN_HOOK)) { return; } const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; - const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressRuntimeMessage[]; - const hookStartMessage = runtimeMessages - .toReversed() - .find(({ type }) => type === "cypress_hook_start") as CypressHookStartRuntimeMessage; + const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressMessage[]; + const hookStartMessage = toReversed(runtimeMessages).find( + ({ type }) => type === "cypress_hook_start", + ) as CypressHookStartMessage; + + if (!hookStartMessage.data.id) { + hookStartMessage.data.id = `${hook.id}:${hook.hookId}`; + } return testRuntime.sendMessageAsync({ type: "cypress_hook_end", data: { + id: hookStartMessage.data.id, + parentId: hook.parent.id, status: Status.PASSED, - stage: Stage.FINISHED, stop: hookStartMessage.data.start + (hook.duration ?? 0), + global: isGlobalHook(hook.title), }, }); }) @@ -254,51 +282,52 @@ const initializeAllure = () => { return testRuntime.sendMessageAsync({ type: "cypress_suite_start", data: { + id: suite.titlePath().join(" "), name: suite.title, }, }); }) - .on(EVENT_SUITE_END, () => { + .on(EVENT_SUITE_END, (suite: Mocha.Suite) => { const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; return testRuntime.sendMessageAsync({ type: "cypress_suite_end", - data: {}, + data: { + id: suite.titlePath().join(" "), + }, }); }) .on(EVENT_TEST_BEGIN, (test: CypressTest) => { const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; testRuntime.sendMessage({ - type: "cypress_start", + type: "cypress_test_start", data: { - isInteractive: Cypress.config("isInteractive"), - absolutePath: Cypress.spec.absolute, + id: test.id, specPath: getSuitePath(test).concat(test.title), filename: Cypress.spec.relative, start: test.wallClockStartedAt?.getTime() || Date.now(), }, }); }) - .on(EVENT_TEST_PASS, () => { + .on(EVENT_TEST_PASS, (test: CypressTest) => { const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; - const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressRuntimeMessage[]; - const unfinishedStepsMessages = getUnfinishedStepsMessages(runtimeMessages as RuntimeMessage[]); + const runtimeMessages = Cypress.env("allureRuntimeMessages") as RuntimeMessage[]; + const unfinishedStepsMessages = getUnfinishedStepsMessages(runtimeMessages); unfinishedStepsMessages.forEach(() => { testRuntime.sendMessage({ type: "step_stop", data: { - stage: Stage.FINISHED, status: Status.PASSED, stop: Date.now(), }, }); }); testRuntime.sendMessage({ - type: "cypress_end", + type: "cypress_test_end", data: { - stage: Stage.FINISHED, + id: test.id, status: Status.PASSED, stop: Date.now(), }, @@ -306,7 +335,7 @@ const initializeAllure = () => { }) .on(EVENT_TEST_FAIL, (test: CypressTest, err: Error) => { const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; - const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressRuntimeMessage[]; + const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressMessage[]; const startCommandMessageIdx = runtimeMessages .toReversed() .findIndex(({ type }) => type === "cypress_command_start"); @@ -324,9 +353,9 @@ const initializeAllure = () => { testRuntime.sendMessage({ type: "cypress_command_end", data: { + id: (runtimeMessages[startCommandMessageIdx] as CypressCommandStartMessage).data.id, status, statusDetails, - stage: Stage.FINISHED, }, }); } @@ -334,15 +363,17 @@ const initializeAllure = () => { if (test.hookName) { const hookStartMessage = runtimeMessages .toReversed() - .find(({ type }) => type === "cypress_hook_start") as CypressHookStartRuntimeMessage; + .find(({ type }) => type === "cypress_hook_start") as CypressHookStartMessage; return testRuntime.sendMessageAsync({ type: "cypress_hook_end", data: { + id: hookStartMessage.data.id, status, statusDetails, - stage: Stage.FINISHED, stop: hookStartMessage.data.start + (test.duration ?? 0), + parentId: hookStartMessage.data.parentId, + global: isGlobalHook(test.hookName), }, }); } @@ -350,10 +381,9 @@ const initializeAllure = () => { // the test hasn't been even started (rather due to hook error), so we need to start it manually if (!test.hookName && test.wallClockStartedAt === undefined) { testRuntime.sendMessage({ - type: "cypress_start", + type: "cypress_test_start", data: { - isInteractive: Cypress.config("isInteractive"), - absolutePath: Cypress.spec.absolute, + id: test.id, specPath: getSuitePath(test).concat(test.title), filename: Cypress.spec.relative, start: Date.now(), @@ -361,19 +391,25 @@ const initializeAllure = () => { }); } - const testStartMessage = runtimeMessages - .toReversed() - .find(({ type }) => type === "cypress_start") as CypressTestStartRuntimeMessage; + const testStartMessage = toReversed(runtimeMessages).find( + ({ type }) => type === "cypress_test_start", + ) as CypressTestStartMessage; testRuntime.sendMessage({ - type: "cypress_end", + type: "cypress_test_end", data: { + id: test.id, status, statusDetails, - stage: Stage.FINISHED, stop: testStartMessage.data.start + (test.duration ?? 0), }, }); + }) + .on(EVENT_RUN_END, () => { + // this is the only way to say reporter process messages in interactive mode without data duplication + if (Cypress.config("isInteractive")) { + cy.task("allureReportSpec", { absolute: Cypress.spec.absolute }); + } }); Cypress.Screenshot.defaults({ @@ -393,15 +429,15 @@ const initializeAllure = () => { Cypress.on("fail", (err) => { const testRuntime = getGlobalTestRuntime() as AllureCypressTestRuntime; - const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressRuntimeMessage[]; - const hasSteps = hasStepMessage(runtimeMessages as RuntimeMessage[]); + const runtimeMessages = Cypress.env("allureRuntimeMessages") as RuntimeMessage[]; + const hasSteps = hasStepMessage(runtimeMessages); // if there is no steps, don't handle the error if (!hasSteps) { throw err; } - const unfinishedStepsMessages = getUnfinishedStepsMessages(runtimeMessages as RuntimeMessage[]); + const unfinishedStepsMessages = getUnfinishedStepsMessages(runtimeMessages); if (unfinishedStepsMessages.length === 0) { throw err; @@ -413,7 +449,6 @@ const initializeAllure = () => { testRuntime.sendMessage({ type: "step_stop", data: { - stage: Stage.FINISHED, status: failedStepsStatus, stop: Date.now(), statusDetails: { @@ -436,6 +471,7 @@ const initializeAllure = () => { return testRuntime.sendMessageAsync({ type: "cypress_command_start", data: { + id: command.attributes.id, name: `Command "${command.attributes.name}"`, args: command.attributes.args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg, null, 2))), }, @@ -451,16 +487,20 @@ const initializeAllure = () => { return testRuntime.sendMessageAsync({ type: "cypress_command_end", data: { + id: command.attributes.id, status: Status.PASSED, - stage: Stage.FINISHED, }, }); }); after(ALLURE_REPORT_SHUTDOWN_HOOK, () => { - const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressRuntimeMessage[]; + const runtimeMessages = Cypress.env("allureRuntimeMessages") as CypressMessage[]; - cy.task("allureReportTest", runtimeMessages ?? [], { log: false }); + cy.task( + "allureReportTest", + { absolutePath: Cypress.spec.absolute, messages: runtimeMessages ?? [] }, + { log: false }, + ); }); }; diff --git a/packages/allure-cypress/src/model.ts b/packages/allure-cypress/src/model.ts index c2be69185..62f3ba4cb 100644 --- a/packages/allure-cypress/src/model.ts +++ b/packages/allure-cypress/src/model.ts @@ -1,8 +1,6 @@ -import type { Stage, Status, StatusDetails } from "allure-js-commons"; +import type { Status, StatusDetails } from "allure-js-commons"; import type { RuntimeMessage } from "allure-js-commons/sdk"; -// TODO: report cypress commands - export const ALLURE_REPORT_SHUTDOWN_HOOK = "__allure_report_shutdown_hook__"; export const ALLURE_REPORT_STEP_COMMAND = "__allure_report_step_command__"; @@ -10,96 +8,119 @@ export const ALLURE_REPORT_STEP_COMMAND = "__allure_report_step_command__"; export type CypressTest = Mocha.Test & { wallClockStartedAt?: Date; hookName?: string; + id: string; }; -export type CypressHook = { - name: string; - type: "before" | "after"; - start: number; - global: boolean; +export type CypressHook = Mocha.Hook & { + id: string; + hookId: string; + parent: Mocha.Suite & { + id: string; + }; }; export type CypressCommand = { attributes: { name: string; + id: string; args: any[]; }; state: "passed" | "failed" | "queued"; }; -export type CypressHookStartRuntimeMessage = { +export type CypressHookStartMessage = { type: "cypress_hook_start"; - data: CypressHook; + data: { + id: string; + parentId: string; + name: string; + type: "before" | "after"; + start: number; + global: boolean; + }; }; -export type CypressHookEndRuntimeMessage = { +export type CypressHookEndMessage = { type: "cypress_hook_end"; data: { - stage: Stage; + id: string; + parentId: string; status: Status; statusDetails?: StatusDetails; stop: number; + global: boolean; }; }; -export type CypressSuiteStartRuntimeMessage = { +export type CypressSuiteStartMessage = { type: "cypress_suite_start"; data: { + id: string; name: string; + root?: boolean; }; }; -export type CypressSuiteEndRuntimeMessage = { +export type CypressSuiteEndMessage = { type: "cypress_suite_end"; - data: any; + data: { + id: string; + root?: boolean; + }; }; -export type CypressTestStartRuntimeMessage = { - type: "cypress_start"; +export type CypressTestStartMessage = { + type: "cypress_test_start"; data: { - isInteractive: boolean; - absolutePath: string; + id: string; specPath: string[]; filename: string; start: number; }; }; -export type CypressTestEndRuntimeMessage = { - type: "cypress_end"; +export type CypressTestEndMessage = { + type: "cypress_test_end"; data: { - stage: Stage; + id: string; status: Status; statusDetails?: StatusDetails; stop: number; }; }; -// TODO: add cypress logs property -export type CypressCommandStartRuntimeMessage = { +export type CypressCommandStartMessage = { type: "cypress_command_start"; data: { + id: string; name: string; args: string[]; }; }; -export type CypressCommandEndRuntimeMessage = { +export type CypressCommandEndMessage = { type: "cypress_command_end"; data: { - stage: Stage; + id: string; status: Status; statusDetails?: StatusDetails; }; }; -export type CypressRuntimeMessage = +export type CypressMessage = | RuntimeMessage - | CypressTestStartRuntimeMessage - | CypressTestEndRuntimeMessage - | CypressHookStartRuntimeMessage - | CypressHookEndRuntimeMessage - | CypressSuiteStartRuntimeMessage - | CypressSuiteEndRuntimeMessage - | CypressCommandStartRuntimeMessage - | CypressCommandEndRuntimeMessage; + | CypressTestStartMessage + | CypressTestEndMessage + | CypressHookStartMessage + | CypressHookEndMessage + | CypressSuiteStartMessage + | CypressSuiteEndMessage + | CypressCommandStartMessage + | CypressCommandEndMessage; + +export type RunContextByAbsolutePath = { + executables: string[]; + steps: string[]; + scopes: string[]; + globalHooksMessages: (CypressHookStartMessage | CypressHookEndMessage)[]; +}; diff --git a/packages/allure-cypress/src/reporter.ts b/packages/allure-cypress/src/reporter.ts index dbba34cae..bb4fd3ba8 100644 --- a/packages/allure-cypress/src/reporter.ts +++ b/packages/allure-cypress/src/reporter.ts @@ -1,26 +1,26 @@ import type Cypress from "cypress"; import { ContentType, LabelName, Stage, Status } from "allure-js-commons"; +import type { RuntimeMessage } from "allure-js-commons/sdk"; import { extractMetadataFromString } from "allure-js-commons/sdk"; import { FileSystemWriter, ReporterRuntime, getSuiteLabels } from "allure-js-commons/sdk/reporter"; +import type { Config } from "allure-js-commons/sdk/reporter"; import type { - CypressHookEndRuntimeMessage, - CypressHookStartRuntimeMessage, - CypressRuntimeMessage, - CypressTestStartRuntimeMessage, + CypressHookEndMessage, + CypressHookStartMessage, + CypressMessage, + RunContextByAbsolutePath, } from "./model.js"; - -export type AllureCypressConfig = { - resultsDir?: string; -}; +import { last, toReversed } from "./utils.js"; export class AllureCypress { allureRuntime: ReporterRuntime; - testsUuidsByCypressAbsolutePath = new Map(); + messagesByAbsolutePath = new Map(); + runContextByAbsolutePath = new Map(); - globalHooksMessages: CypressRuntimeMessage[] = []; + globalHooksMessages: CypressMessage[] = []; - constructor(config?: AllureCypressConfig) { + constructor(config?: Config) { const { resultsDir = "./allure-results", ...rest } = config || {}; this.allureRuntime = new ReporterRuntime({ @@ -31,216 +31,219 @@ export class AllureCypress { }); } - private pushTestUuid(absolutePath: string, uuid: string) { - const currentUuids = this.testsUuidsByCypressAbsolutePath.get(absolutePath) || []; - - this.testsUuidsByCypressAbsolutePath.set(absolutePath, currentUuids.concat(uuid)); + createEmptyRunContext(absolutePath: string) { + this.runContextByAbsolutePath.set(absolutePath, { + executables: [], + steps: [], + scopes: [], + globalHooksMessages: [], + }); } attachToCypress(on: Cypress.PluginEvents) { on("task", { - allureReportTest: (messages: CypressRuntimeMessage[]) => { - let currentTestUuid: string; - let currentTestStartMessage: CypressTestStartRuntimeMessage; - - this.globalHooksMessages = []; - - messages.forEach((message, i) => { - const previousMessagesSlice = messages.slice(0, i); - let lastHookMessage!: CypressHookStartRuntimeMessage | CypressHookEndRuntimeMessage; - - for (let j = previousMessagesSlice.length - 1; j >= 0; j--) { - const previousMessage = previousMessagesSlice[j]; - - if (previousMessage.type === "cypress_hook_start") { - lastHookMessage = previousMessagesSlice[j] as CypressHookStartRuntimeMessage; - break; - } - - if (previousMessage.type === "cypress_hook_end") { - lastHookMessage = previousMessagesSlice[j] as CypressHookEndRuntimeMessage; - break; - } - } - - if (message.type === "cypress_suite_start") { - this.allureRuntime.startScope(); - return; - } - - if (message.type === "cypress_suite_end") { - this.allureRuntime.stopScope(); - return; - } - - if (message.type === "cypress_hook_start" && message.data.global) { - this.globalHooksMessages.push(message); - return; - } + allureReportTest: ({ messages, absolutePath }: { messages: CypressMessage[]; absolutePath: string }) => { + this.messagesByAbsolutePath.set(absolutePath, messages); - if (message.type === "cypress_hook_start") { - this.allureRuntime.startFixture(message.data.type, { - name: message.data.name, - start: message.data.start, - }); - return; - } - - if ( - message.type === "cypress_hook_end" && - (lastHookMessage as CypressHookStartRuntimeMessage)?.data?.global && - lastHookMessage?.type === "cypress_hook_start" - ) { - this.globalHooksMessages.push(message); - return; - } - - if (message.type === "cypress_hook_end") { - this.allureRuntime.updateFixture((r) => { - r.stage = message.data.stage; - r.status = message.data.status; - r.stop = message.data.stop; - - if (message.data.statusDetails) { - r.statusDetails = message.data.statusDetails; - } - }); - this.allureRuntime.writeFixture(); - return; - } + return null; + }, + allureReportSpec: (spec: { absolute: string }) => { + this.createEmptyRunContext(spec.absolute); + this.endSpec(spec as Cypress.Spec); - if (message.type === "cypress_command_start") { - this.allureRuntime.startStep({ - name: message.data.name, - parameters: message.data.args.map((arg, j) => ({ - name: `Argument "${j}"`, - value: arg, - })), - }); - return; - } + return null; + }, + }); + } - if (message.type === "cypress_command_end") { - this.allureRuntime.updateStep((r) => { - r.stage = message.data.stage; - r.status = message.data.status; + endRun(result: CypressCommandLine.CypressRunResult) { + result.runs.forEach((run) => { + this.createEmptyRunContext(run.spec.absolute); + this.endSpec(run.spec, run.video || undefined); + }); + } - if (message.data.statusDetails) { - r.statusDetails = message.data.statusDetails; - } - }); - this.allureRuntime.stopStep(); - return; + endSpec(spec: Cypress.Spec, cypressVideoPath?: string) { + const specMessages = this.messagesByAbsolutePath.get(spec.absolute)!; + const runContext = this.runContextByAbsolutePath.get(spec.absolute)!; + + specMessages.forEach((message, i) => { + // we add cypressTestId to messages where it's possible because the field is very useful to glue data + // @ts-ignore + // const {cypressTestId} = message.data + const previousMessagesSlice = specMessages.slice(0, i); + const lastHookMessage = toReversed(previousMessagesSlice).find( + ({ type }) => type === "cypress_hook_start" || type === "cypress_hook_end", + ) as CypressHookStartMessage | CypressHookEndMessage; + + if (message.type === "cypress_suite_start") { + const scopeUuid = this.allureRuntime.startScope(); + + runContext.scopes.push(scopeUuid); + return; + } + + if (message.type === "cypress_suite_end") { + const scopeUuid = runContext.scopes.pop()!; + + this.allureRuntime.writeScope(scopeUuid); + return; + } + + if (message.type === "cypress_hook_start" && message.data.global) { + runContext.globalHooksMessages.push(message); + return; + } + + if (message.type === "cypress_hook_start") { + const fixtureUuid = this.allureRuntime.startFixture(last(runContext.scopes)!, message.data.type, { + name: message.data.name, + start: message.data.start, + })!; + + runContext.executables.push(fixtureUuid); + return; + } + + if ( + message.type === "cypress_hook_end" && + (lastHookMessage as CypressHookEndMessage)?.data?.global && + lastHookMessage?.type === "cypress_hook_start" + ) { + runContext.globalHooksMessages.push(message); + return; + } + + if (message.type === "cypress_hook_end") { + const fixtureUuid = runContext.executables.pop()!; + + this.allureRuntime.updateFixture(fixtureUuid, (r) => { + r.stage = Stage.FINISHED; + r.status = message.data.status; + r.stop = message.data.stop; + + if (message.data.statusDetails) { + r.statusDetails = message.data.statusDetails; } - - if (message.type === "cypress_start") { - currentTestStartMessage = message; - - const suiteLabels = getSuiteLabels(message.data.specPath.slice(0, -1)); - const testTitle = message.data.specPath[message.data.specPath.length - 1]; - const titleMetadata = extractMetadataFromString(testTitle); - - currentTestUuid = this.allureRuntime.startTest({ - name: titleMetadata.cleanTitle || testTitle, - start: message.data.start, - fullName: `${message.data.filename}#${message.data.specPath.join(" ")}`, - stage: Stage.RUNNING, - }); - - this.allureRuntime.updateTest((result) => { - result.labels.push({ + }); + this.allureRuntime.stopFixture(fixtureUuid); + return; + } + + if (message.type === "cypress_test_start") { + const suiteLabels = getSuiteLabels(message.data.specPath.slice(0, -1)); + const testTitle = message.data.specPath[message.data.specPath.length - 1]; + const titleMetadata = extractMetadataFromString(testTitle); + const testUuid = this.allureRuntime.startTest( + { + name: titleMetadata.cleanTitle || testTitle, + start: message.data.start, + fullName: `${message.data.filename}#${message.data.specPath.join(" ")}`, + stage: Stage.RUNNING, + labels: [ + { name: LabelName.LANGUAGE, value: "javascript", - }); - result.labels.push({ + }, + { name: LabelName.FRAMEWORK, value: "cypress", - }); - result.labels.push(...suiteLabels); - result.labels.push(...titleMetadata.labels); - }); + }, + ...suiteLabels, + ...titleMetadata.labels, + ], + }, + runContext.scopes, + ); + + runContext.executables.push(testUuid); + return; + } + + if (message.type === "cypress_test_end") { + const testUuid = runContext.executables.pop()!; + + this.allureRuntime.updateTest(testUuid, (result) => { + result.stage = Stage.FINISHED; + result.status = message.data.status; + + if (!message.data.statusDetails) { return; } - if (message.type === "cypress_end") { - this.allureRuntime.updateTest((result) => { - result.stage = message.data.stage; - result.status = message.data.status; - - if (!message.data.statusDetails) { - return; - } - - result.statusDetails = message.data.statusDetails; - }, currentTestUuid!); - - this.allureRuntime.stopTest({ uuid: currentTestUuid!, stop: Date.now() }); - - if (currentTestStartMessage!.data.isInteractive) { - this.allureRuntime.writeTest(currentTestUuid!); - } else { - // False positive by eslint (testUuid is string) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.pushTestUuid(currentTestStartMessage!.data.absolutePath, currentTestUuid!); - } - return; - } + result.statusDetails = message.data.statusDetails; + }); - // we can get error when we try to attach screenshot to a failed test because there is no test due to error in hook - if (!this.allureRuntime.getCurrentExecutingItem()) { - return; + this.allureRuntime.stopTest(testUuid); + this.allureRuntime.writeTest(testUuid); + return; + } + + // we can get error when we try to attach screenshot to a failed test because there is no test due to error in hook + if (runContext.executables.length === 0) { + return; + } + + if (message.type === "cypress_command_start") { + const lastExecutableUuid = last(runContext.executables)!; + const lastStepUuid = last(runContext.steps); + const stepUuid = this.allureRuntime.startStep(lastExecutableUuid, lastStepUuid, { + name: message.data.name, + parameters: message.data.args.map((arg, j) => ({ + name: `Argument [${j}]`, + value: arg, + })), + })!; + + runContext.steps.push(stepUuid); + return; + } + + if (message.type === "cypress_command_end") { + const stepUuid = runContext.steps.pop()!; + + this.allureRuntime.updateStep(stepUuid, (r) => { + r.status = message.data.status; + + if (message.data.statusDetails) { + r.statusDetails = message.data.statusDetails; } - - this.allureRuntime.applyRuntimeMessages([message], { - testUuid: currentTestUuid!, - }); }); + this.allureRuntime.stopStep(stepUuid); + return; + } - return null; - }, - }); - } - - endRun(result: CypressCommandLine.CypressRunResult) { - result.runs.forEach((run) => { - this.endSpec(run.spec, run.video || undefined); + this.allureRuntime.applyRuntimeMessages(last(runContext.executables)!, [message] as RuntimeMessage[]); }); - } - - endSpec(spec: Cypress.Spec, cypressVideoPath?: string) { - const testUuids = this.testsUuidsByCypressAbsolutePath.get(spec.absolute); - - this.testsUuidsByCypressAbsolutePath.delete(spec.absolute); - - if (!testUuids) { - return; - } if (cypressVideoPath) { - this.allureRuntime.startFixture("after", { + const fixtureUuid = this.allureRuntime.startFixture(runContext.scopes[0], "after", { name: "Cypress video", status: Status.PASSED, stage: Stage.FINISHED, - }); - this.allureRuntime.writeAttachmentFromPath("Cypress video", cypressVideoPath, { + })!; + this.allureRuntime.writeAttachment(fixtureUuid, undefined, "Cypress video", cypressVideoPath, { contentType: ContentType.MP4, }); - this.allureRuntime.writeFixture(); + this.allureRuntime.stopFixture(fixtureUuid); } - if (this.globalHooksMessages.length) { - this.globalHooksMessages.forEach((message) => { + if (runContext.globalHooksMessages.length > 0) { + runContext.globalHooksMessages.forEach((message) => { if (message.type === "cypress_hook_start") { - this.allureRuntime.startFixture(message.data.type, { + const fixtureUuid = this.allureRuntime.startFixture(runContext.scopes[0], message.data.type, { name: message.data.name, start: message.data.start, - }); + })!; + + runContext.executables.push(fixtureUuid); return; } if (message.type === "cypress_hook_end") { - this.allureRuntime.updateFixture((r) => { - r.stage = message.data.stage; + const fixtureUuid = runContext.executables.pop()!; + + this.allureRuntime.updateFixture(fixtureUuid, (r) => { r.status = message.data.status; r.stop = message.data.stop; @@ -248,20 +251,16 @@ export class AllureCypress { r.statusDetails = message.data.statusDetails; } }); - this.allureRuntime.writeFixture(); + this.allureRuntime.stopFixture(fixtureUuid); } }); } - for (const uuid of testUuids) { - this.allureRuntime.writeTest(uuid); - } - - this.allureRuntime.writeScope(); + this.allureRuntime.writeScope(runContext.scopes.pop()!); } } -export const allureCypress = (on: Cypress.PluginEvents, allureConfig?: AllureCypressConfig) => { +export const allureCypress = (on: Cypress.PluginEvents, allureConfig?: Config) => { const allureCypressReporter = new AllureCypress(allureConfig); allureCypressReporter.attachToCypress(on); diff --git a/packages/allure-cypress/src/utils.ts b/packages/allure-cypress/src/utils.ts index e29ddf5a8..90d95a900 100644 --- a/packages/allure-cypress/src/utils.ts +++ b/packages/allure-cypress/src/utils.ts @@ -56,3 +56,25 @@ export const isCommandShouldBeSkipped = (command: CypressCommand) => { return false; }; + +export const toReversed = (arr: T[]): T[] => { + const result: T[] = []; + + for (let i = arr.length - 1; i >= 0; i--) { + result.push(arr[i]); + } + + return result; +}; + +export const isGlobalHook = (hookName: string) => { + return /(before|after) all/.test(hookName); +}; + +export const getHookType = (hookName: string) => { + return hookName.includes("before") ? "before" : "after"; +}; + +export const last = (arr: T[]): T | undefined => { + return arr[arr.length - 1]; +}; diff --git a/packages/allure-cypress/test/spec/commands.test.ts b/packages/allure-cypress/test/spec/commands.test.ts index 93c2b9b2a..043ba48c3 100644 --- a/packages/allure-cypress/test/spec/commands.test.ts +++ b/packages/allure-cypress/test/spec/commands.test.ts @@ -24,7 +24,7 @@ it("test with cypress command", async () => { name: String.raw`Command "log"`, parameters: expect.arrayContaining([ expect.objectContaining({ - name: String.raw`Argument "0"`, + name: String.raw`Argument [0]`, value: JSON.stringify(1, null, 2), }), ]), @@ -33,7 +33,7 @@ it("test with cypress command", async () => { name: String.raw`Command "log"`, parameters: expect.arrayContaining([ expect.objectContaining({ - name: String.raw`Argument "0"`, + name: String.raw`Argument [0]`, value: "2", }), ]), @@ -42,7 +42,7 @@ it("test with cypress command", async () => { name: String.raw`Command "log"`, parameters: expect.arrayContaining([ expect.objectContaining({ - name: String.raw`Argument "0"`, + name: String.raw`Argument [0]`, value: JSON.stringify([1, 2, 3], null, 2), }), ]), @@ -51,7 +51,7 @@ it("test with cypress command", async () => { name: String.raw`Command "log"`, parameters: expect.arrayContaining([ expect.objectContaining({ - name: String.raw`Argument "0"`, + name: String.raw`Argument [0]`, value: JSON.stringify({ foo: 1, bar: 2, baz: 3 }, null, 2), }), ]), diff --git a/packages/allure-cypress/test/spec/runtime/legacy/steps.test.ts b/packages/allure-cypress/test/spec/runtime/legacy/steps.test.ts index 1022dc19e..ec0ad6e40 100644 --- a/packages/allure-cypress/test/spec/runtime/legacy/steps.test.ts +++ b/packages/allure-cypress/test/spec/runtime/legacy/steps.test.ts @@ -149,7 +149,7 @@ it("step with cypress assertion error", async () => { it("step", () => { step("foo", () => { - cy.wrap(1).should("eq", 2); + expect(1).to.eq(2); }); }); `, diff --git a/packages/allure-cypress/test/spec/runtime/modern/steps.test.ts b/packages/allure-cypress/test/spec/runtime/modern/steps.test.ts index 7b167d9d4..6647d9109 100644 --- a/packages/allure-cypress/test/spec/runtime/modern/steps.test.ts +++ b/packages/allure-cypress/test/spec/runtime/modern/steps.test.ts @@ -149,7 +149,7 @@ it("step with cypress assertion error", async () => { it("step", () => { step("foo", () => { - cy.wrap(1).should("eq", 2); + expect(1).to.eq(2); }); }); `, diff --git a/packages/allure-jasmine/src/index.ts b/packages/allure-jasmine/src/index.ts index 3284a3d8e..fa554c282 100644 --- a/packages/allure-jasmine/src/index.ts +++ b/packages/allure-jasmine/src/index.ts @@ -24,6 +24,7 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { private readonly allureRuntime: ReporterRuntime; private currentAllureTestUuid?: string; private jasmineSuitesStack: jasmine.SuiteResult[] = []; + private scopesStack: string[] = []; constructor(config: AllureJasmineConfig) { const { testMode, resultsDir = "./allure-results", ...restConfig } = config || {}; @@ -44,7 +45,8 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { this.installHooks(); // the best place to start global container for hooks and nested suites - this.allureRuntime.startScope(); + const scopeUuid = this.allureRuntime.startScope(); + this.scopesStack.push(scopeUuid); } private getCurrentSpecPath() { @@ -72,14 +74,17 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { } handleAllureRuntimeMessages(message: RuntimeMessage) { - this.allureRuntime.applyRuntimeMessages([message], { testUuid: this.currentAllureTestUuid! }); + if (!this.currentAllureTestUuid) { + return; + } + this.allureRuntime.applyRuntimeMessages(this.currentAllureTestUuid, [message]); } jasmineStarted(): void { const allureRuntime = this.allureRuntime; const globalJasmine = globalThis.jasmine; - const currentAllureResultUuidGetter = () => this.currentAllureTestUuid; - const currentAllureStepResultGetter = () => this.allureRuntime.getCurrentStep(currentAllureResultUuidGetter()); + const currentAllureStepResultGetter = () => + this.currentAllureTestUuid ? this.allureRuntime.currentStep(this.currentAllureTestUuid) : undefined; // @ts-ignore const originalExpectationHandler = globalJasmine.Spec.prototype.addExpectationResult; @@ -88,11 +93,12 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { globalJasmine.Spec.prototype.addExpectationResult = function (passed, data, isError) { const isStepFailed = !passed && !isError; - if (currentAllureStepResultGetter() && isStepFailed) { - allureRuntime.updateStep((result) => { + const stepUuid = currentAllureStepResultGetter(); + if (stepUuid && isStepFailed) { + allureRuntime.updateStep(stepUuid, (result) => { result.status = Status.FAILED; result.stage = Stage.FINISHED; - }, currentAllureResultUuidGetter()); + }); } originalExpectationHandler.call(this, passed, data, isError); @@ -101,27 +107,37 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { suiteStarted(suite: jasmine.SuiteResult): void { this.jasmineSuitesStack.push(suite); - this.allureRuntime.startScope(); + const scopeUuid = this.allureRuntime.startScope(); + this.scopesStack.push(scopeUuid); } suiteDone(): void { this.jasmineSuitesStack.pop(); - this.allureRuntime.writeScope(); + const scopeUuid = this.scopesStack.pop(); + if (scopeUuid) { + this.allureRuntime.writeScope(scopeUuid); + } } specStarted(spec: jasmine.SpecResult): void { - this.currentAllureTestUuid = this.allureRuntime.startTest({ - name: spec.description, - fullName: this.getSpecFullName(spec), - stage: Stage.RUNNING, - }); + this.currentAllureTestUuid = this.allureRuntime.startTest( + { + name: spec.description, + fullName: this.getSpecFullName(spec), + stage: Stage.RUNNING, + }, + this.scopesStack, + ); } specDone(spec: jasmine.SpecResult): void { + if (!this.currentAllureTestUuid) { + return; + } const specPath = this.getCurrentSpecPath(); const exceptionInfo = findMessageAboutThrow(spec.failedExpectations) || findAnyError(spec.failedExpectations); - this.allureRuntime.updateTest((result) => { + this.allureRuntime.updateTest(this.currentAllureTestUuid, (result) => { const suitesLabels = getSuiteLabels(specPath); result.labels.push(...suitesLabels); @@ -156,8 +172,8 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { result.status = Status.BROKEN; return; } - }, this.currentAllureTestUuid); - this.allureRuntime.stopTest({ uuid: this.currentAllureTestUuid! }); + }); + this.allureRuntime.stopTest(this.currentAllureTestUuid); this.allureRuntime.writeTest(this.currentAllureTestUuid); this.currentAllureTestUuid = undefined; } @@ -165,8 +181,11 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { jasmineDone(): void { this.allureRuntime.writeEnvironmentInfo(); this.allureRuntime.writeCategoriesDefinitions(); - // write global container - this.allureRuntime.writeScope(); + // write global container (or any remaining scopes) + this.scopesStack.forEach((scopeUuid) => { + this.allureRuntime.writeScope(scopeUuid); + }); + this.scopesStack = []; } private installHooks(): void { @@ -198,50 +217,69 @@ export default class AllureJasmineReporter implements jasmine.CustomReporter { .then(() => { done(); - this.allureRuntime.startFixture(fixtureType, { + const scopeUuid = + this.scopesStack.length > 0 ? this.scopesStack[this.scopesStack.length - 1] : undefined; + if (scopeUuid) { + const fixtureUuid = this.allureRuntime.startFixture(scopeUuid, fixtureType, { + name: fixtureName, + stage: Stage.FINISHED, + status: Status.PASSED, + start, + }); + if (fixtureUuid) { + this.allureRuntime.stopFixture(fixtureUuid); + } + } + }) + .catch((err) => { + done.fail(err as Error); + const scopeUuid = + this.scopesStack.length > 0 ? this.scopesStack[this.scopesStack.length - 1] : undefined; + if (scopeUuid) { + const fixtureUuid = this.allureRuntime.startFixture(scopeUuid, fixtureType, { + name: fixtureName, + stage: Stage.FINISHED, + status: Status.BROKEN, + start, + }); + if (fixtureUuid) { + this.allureRuntime.stopFixture(fixtureUuid); + } + } + }); + } else { + try { + done(); + const scopeUuid = this.scopesStack.length > 0 ? this.scopesStack[this.scopesStack.length - 1] : undefined; + if (scopeUuid) { + const fixtureUuid = this.allureRuntime.startFixture(scopeUuid, fixtureType, { name: fixtureName, stage: Stage.FINISHED, status: Status.PASSED, start, }); - this.allureRuntime.stopFixture(); - }) - .catch((err) => { - done.fail(err as Error); - - this.allureRuntime.startFixture(fixtureType, { + if (fixtureUuid) { + this.allureRuntime.stopFixture(fixtureUuid); + } + } + } catch (err) { + const { message, stack } = err as Error; + const scopeUuid = this.scopesStack.length > 0 ? this.scopesStack[this.scopesStack.length - 1] : undefined; + if (scopeUuid) { + const fixtureUuid = this.allureRuntime.startFixture(scopeUuid, fixtureType, { name: fixtureName, stage: Stage.FINISHED, status: Status.BROKEN, + statusDetails: { + message, + trace: stack, + }, start, }); - this.allureRuntime.stopFixture(); - }); - } else { - try { - done(); - - this.allureRuntime.startFixture(fixtureType, { - name: fixtureName, - stage: Stage.FINISHED, - status: Status.PASSED, - start, - }); - this.allureRuntime.stopFixture(); - } catch (err) { - const { message, stack } = err as Error; - - this.allureRuntime.startFixture(fixtureType, { - name: fixtureName, - stage: Stage.FINISHED, - status: Status.BROKEN, - statusDetails: { - message, - trace: stack, - }, - start, - }); - this.allureRuntime.stopFixture(); + if (fixtureUuid) { + this.allureRuntime.stopFixture(fixtureUuid); + } + } throw err; } diff --git a/packages/allure-jest/src/environmentFactory.ts b/packages/allure-jest/src/environmentFactory.ts index 222d0c042..2c28fe71d 100644 --- a/packages/allure-jest/src/environmentFactory.ts +++ b/packages/allure-jest/src/environmentFactory.ts @@ -66,7 +66,7 @@ const createJestEnvironment = (Base: T): T => parent: { name: "ROOT_DESCRIBE_BLOCK" }, } as Circus.TestEntry)!; - this.runtime.applyRuntimeMessages([payload.message], { testUuid }); + this.runtime.applyRuntimeMessages(testUuid, [payload.message]); } private getTestUuid(test: Circus.TestEntry) { @@ -117,6 +117,9 @@ const createJestEnvironment = (Base: T): T => case "test_done": this.handleTestDone(event.test); break; + case "run_finish": + this.handleRunFinish(); + break; default: break; } @@ -150,7 +153,7 @@ const createJestEnvironment = (Base: T): T => ], }); - this.runtime.updateTest((result) => { + this.runtime.updateTest(testUuid, (result) => { if (threadLabel) { result.labels.push({ name: LabelName.THREAD, value: threadLabel }); } @@ -160,7 +163,7 @@ const createJestEnvironment = (Base: T): T => } result.labels.push(...getSuiteLabels(newTestSuitesPath)); - }, testUuid); + }); /** * If user have some tests with the same name, reporter will throw an error due the test with @@ -184,9 +187,9 @@ const createJestEnvironment = (Base: T): T => return; } - this.runtime.updateTest((result) => { + this.runtime.updateTest(testUuid, (result) => { result.stage = Stage.RUNNING; - }, testUuid); + }); } private handleTestPass(test: Circus.TestEntry) { @@ -196,10 +199,10 @@ const createJestEnvironment = (Base: T): T => return; } - this.runtime.updateTest((result) => { + this.runtime.updateTest(testUuid, (result) => { result.stage = Stage.FINISHED; result.status = Status.PASSED; - }, testUuid); + }); } private handleTestFail(test: Circus.TestEntry) { @@ -216,13 +219,13 @@ const createJestEnvironment = (Base: T): T => const details = getMessageAndTraceFromError(firstError); const status = getStatusFromError(firstError); - this.runtime.updateTest((result) => { + this.runtime.updateTest(testUuid, (result) => { result.stage = Stage.FINISHED; result.status = status; result.statusDetails = { ...details, }; - }, testUuid); + }); } private handleTestSkip(test: Circus.TestEntry) { @@ -232,11 +235,11 @@ const createJestEnvironment = (Base: T): T => return; } - this.runtime.updateTest((result) => { + this.runtime.updateTest(testUuid, (result) => { result.stage = Stage.PENDING; result.status = Status.SKIPPED; - }, testUuid); - this.runtime.stopTest({ uuid: testUuid }); + }); + this.runtime.stopTest(testUuid); this.runtime.writeTest(testUuid); // TODO: this.allureUuidsByTestIds.delete(getTestId(getTestPath(test))); @@ -249,7 +252,7 @@ const createJestEnvironment = (Base: T): T => return; } - this.runtime.stopTest({ uuid: testUuid }); + this.runtime.stopTest(testUuid); this.runtime.writeTest(testUuid); // TODO: this.allureUuidsByTestIds.delete(getTestId(getTestPath(test))); @@ -262,16 +265,21 @@ const createJestEnvironment = (Base: T): T => return; } - this.runtime.updateTest((result) => { + this.runtime.updateTest(testUuid, (result) => { result.stage = Stage.PENDING; result.status = Status.SKIPPED; - }, testUuid); + }); - this.runtime.stopTest({ uuid: testUuid }); + this.runtime.stopTest(testUuid); this.runtime.writeTest(testUuid); // TODO: this.allureUuidsByTestIds.delete(getTestId(getTestPath(test))); } + + private handleRunFinish() { + this.runtime.writeEnvironmentInfo(); + this.runtime.writeCategoriesDefinitions(); + } }; }; diff --git a/packages/allure-jest/test/spec/categories.test.ts b/packages/allure-jest/test/spec/categories.test.ts new file mode 100644 index 000000000..1d7a14bb9 --- /dev/null +++ b/packages/allure-jest/test/spec/categories.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { runJestInlineTest } from "../utils.js"; + +describe("categories", () => { + it("should support categories", async () => { + const { categories } = await runJestInlineTest( + ` + it("sample test", async () => { + }); + `, + ); + + expect(categories).toEqual(expect.arrayContaining([{ name: "first" }, { name: "second" }])); + }); +}); diff --git a/packages/allure-jest/test/spec/environmentInfo.test.ts b/packages/allure-jest/test/spec/environmentInfo.test.ts new file mode 100644 index 000000000..8da441a39 --- /dev/null +++ b/packages/allure-jest/test/spec/environmentInfo.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { runJestInlineTest } from "../utils.js"; + +describe("environment info", () => { + it("should add environmentInfo", async () => { + const { envInfo } = await runJestInlineTest( + ` + it("sample test", async () => { + }); + `, + ); + + expect(envInfo).toEqual({ "app version": "123.0.1", "some other key": "some other value" }); + }); +}); diff --git a/packages/allure-jest/test/utils.ts b/packages/allure-jest/test/utils.ts index 71217a5ea..444c2e02f 100644 --- a/packages/allure-jest/test/utils.ts +++ b/packages/allure-jest/test/utils.ts @@ -24,7 +24,16 @@ export const runJestInlineTest = async (testContent: string): Promise this.scopes.get(uuid); - getWrappedFixture = (uuid: string) => this.fixturesResults.get(uuid); + getWrappedFixtureResult = (uuid: string) => this.fixturesResults.get(uuid); - getFixture = (uuid: string) => this.getWrappedFixture(uuid)?.value; + getFixtureResult = (uuid: string) => this.getWrappedFixtureResult(uuid)?.value; - getTest = (uuid: string) => this.testResults.get(uuid); + getTestResult = (uuid: string) => this.testResults.get(uuid); - getStep = (uuid: string) => this.stepResults.get(uuid); + getStepResult = (uuid: string) => this.stepResults.get(uuid); getExecutionItem = (uuid: string): FixtureResult | TestResult | StepResult | undefined => - this.getFixture(uuid) ?? this.getTest(uuid) ?? this.getStep(uuid); + this.getFixtureResult(uuid) ?? this.getTestResult(uuid) ?? this.getStepResult(uuid); // test results setTestResult = (uuid: string, result: TestResult) => { @@ -61,7 +61,6 @@ export class LifecycleState { const scope: TestScope = { fixtures: [], tests: [], - subScopes: [], ...data, uuid, }; diff --git a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts index 9cb62eba8..422c58207 100644 --- a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts +++ b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts @@ -1,11 +1,16 @@ /* eslint max-lines: 0 */ import { extname } from "path"; -import type { Attachment, AttachmentOptions, FixtureResult, StepResult, TestResult } from "../../model.js"; -import { Stage } from "../../model.js"; +import { + type Attachment, + type AttachmentOptions, + type FixtureResult, + Stage, + type StepResult, + type TestResult, +} from "../../model.js"; import type { Category, EnvironmentInfo, - Messages, RuntimeAttachmentContentMessage, RuntimeAttachmentPathMessage, RuntimeMessage, @@ -16,288 +21,120 @@ import type { } from "../types.js"; import { LifecycleState } from "./LifecycleState.js"; import { Notifier } from "./Notifier.js"; -import { MutableAllureContextHolder, StaticContextProvider } from "./context/StaticAllureContextProvider.js"; -import type { AllureContextProvider } from "./context/types.js"; import { createFixtureResult, createStepResult, createTestResult } from "./factory.js"; import type { Config, FixtureType, FixtureWrapper, LinkConfig, TestScope, Writer } from "./types.js"; -import { deepClone, formatLinks, randomUuid } from "./utils.js"; -import { getTestResultHistoryId, getTestResultTestCaseId } from "./utils.js"; +import { deepClone, formatLinks, getTestResultHistoryId, getTestResultTestCaseId, randomUuid } from "./utils.js"; import { buildAttachmentFileName } from "./utils/attachments.js"; import { resolveWriter } from "./writer/loader.js"; -type StartScopeOpts = { - /** - * If set to `true`, a manual scope will be created. A manual scope doesn't affect - * the context. Therefore, tests and fixtures aren't linked to it - * automatically. - * - * Use `linkFixtures`, `updateScope`, or test and fixture start options to fill - * such scope with tests and fixtures. - */ - manual?: boolean; - - /** - * If set to the UUID of an existing scope, the new scope will be created as its - * sub-scope. - * - * Has an effect only if `manual` is `true`. - */ - parent?: string; -}; - -type StartFixtureOpts = { - /** - * The UUID of the scope that should be associated with the fixture. Defaults to the current - * scope of the context. - * - * If set to `null`, the fixture won't be attached to any scope (except the - * dedicated one in case `dedicatedScope` is `true`). - */ - scope?: string | null; - - /** - * If set to `true`, an extra scope will be created to hold the fixture result. - * The scope gets the same UUID as the fixture result and isn't pushed into - * the context. - * - * The scope denoted by the `scope` option will serve as the parent. - */ - dedicatedScope?: boolean; - - /** - * The UUIDs of tests affected by the fixture. Those tests will be associated - * with the fixture's scope. - * - * If the `scope` option is set to `null`, implicitly sets `dedicatedScope` to `true`. - */ - tests?: string[]; -}; - -type StartTestOpts = { - /** - * The UUID of a scope the test should be associated with. Defaults to the current one. - * - * If set to `null`, the test won't be associated with any scope (except the - * dedicated one in case the `dedicatedScope` option is `true`). - */ - scope?: string | null; - - /** - * If set to `true`, an extra scope will be created with the same UUID as the - * test result. The test will be attached to that scope. - * - * The scope denoted by the `scope` option will serve as the parent. - */ - dedicatedScope?: boolean; -}; - -type StopOpts = { - /** - * The test's or fixture's stop time. Defaults to `Date.now()`. - */ - stop?: number; - - /** - * The UUID of a test or fixture to stop. - */ - uuid?: string; -}; - -type LinkFixturesOpts = { - /** - * The UUIDs of fixtures to associate with the scope or tests. - */ - fixtures?: readonly string[]; - - /** - * The UUID of a scope to associate with the fixture or tests. - */ - scope?: string; - - /** - * The UUIDs of tests to associate with the fixture or scope. - */ - tests?: readonly string[]; -}; - -type ApplyMessagesOpts = { - fixtureUuid?: string; - testUuid?: string; - customHandler?: ( - message: Exclude, RuntimeMessage>, - fixture?: FixtureResult, - test?: TestResult, - step?: StepResult, - ) => void | Promise; -}; - -type MessageTargets = { - fixtureUuid: string | undefined; - fixture?: FixtureResult; - testUuid: string | undefined; - test?: TestResult; - rootUuid: string; - root: TestResult | FixtureResult; - step?: StepResult; -}; +interface StepStack { + clear(): void; + removeRoot(rootUuid: string): void; + currentStep(rootUuid: string): string | undefined; + addStep(rootUuid: string, stepUuid: string): void; + removeStep(stepUuid: string): void; +} + +class DefaultStepStack implements StepStack { + private stepsByRoot: Map = new Map(); + private rootsByStep: Map = new Map(); + + clear = (): void => { + this.stepsByRoot.clear(); + this.rootsByStep.clear(); + }; + + removeRoot = (rootUuid: string): void => { + const maybeValue = this.stepsByRoot.get(rootUuid); + this.stepsByRoot.delete(rootUuid); + if (maybeValue) { + maybeValue.forEach((stepUuid) => this.rootsByStep.delete(stepUuid)); + } + }; + + currentStep = (rootUuid: string): string | undefined => { + const maybeValue = this.stepsByRoot.get(rootUuid); + if (!maybeValue) { + return; + } + return maybeValue[maybeValue.length - 1]; + }; + + addStep = (rootUuid: string, stepUuid: string): void => { + const maybeValue = this.stepsByRoot.get(rootUuid); + if (!maybeValue) { + this.stepsByRoot.set(rootUuid, [stepUuid]); + } else { + maybeValue.push(stepUuid); + } + this.rootsByStep.set(stepUuid, rootUuid); + }; + + removeStep(stepUuid: string) { + const rootUuid = this.rootsByStep.get(stepUuid); + if (!rootUuid) { + return; + } + const maybeValue = this.stepsByRoot.get(rootUuid); + if (!maybeValue) { + return; + } + const newValue = maybeValue.filter((value) => value !== stepUuid); + this.stepsByRoot.set(rootUuid, newValue); + } +} export class ReporterRuntime { private readonly state = new LifecycleState(); private notifier: Notifier; - private links: LinkConfig; - private contextProvider: AllureContextProvider; + private stepStack: StepStack = new DefaultStepStack(); writer: Writer; categories?: Category[]; environmentInfo?: EnvironmentInfo; + linkConfig?: LinkConfig; - constructor({ - writer, - listeners = [], - links = {}, - environmentInfo, - categories, - contextProvider = StaticContextProvider.wrap(new MutableAllureContextHolder()), - }: Config) { + constructor({ writer, listeners = [], environmentInfo, categories, links }: Config) { this.writer = resolveWriter(writer); this.notifier = new Notifier({ listeners }); - this.links = links; this.categories = categories; this.environmentInfo = environmentInfo; - this.contextProvider = contextProvider; + this.linkConfig = links; } - hasScope = () => !!this.contextProvider.getScope(); - hasFixture = () => !!this.contextProvider.getFixture(); - hasTest = () => !!this.contextProvider.getTest(); - hasSteps = () => !!this.contextProvider.getStep(); - - getCurrentTest = () => { - const testUuid = this.contextProvider.getTest(); - return testUuid ? this.state.getTest(testUuid) : undefined; - }; - - getCurrentFixture = () => { - const fixtureUuid = this.contextProvider.getFixture(); - return fixtureUuid ? this.state.getFixture(fixtureUuid) : undefined; - }; - - getCurrentStep = (root?: string) => { - const stepUuid = this.contextProvider.getStep(root); - return stepUuid ? this.state.getStep(stepUuid) : undefined; - }; - - getCurrentExecutingItem = (root?: string): FixtureResult | TestResult | StepResult | undefined => { - const uuid = this.contextProvider.getExecutingItem(root); - return uuid ? this.state.getExecutionItem(uuid) : undefined; - }; - - getCurrentScope = () => { - const scopeUuid = this.contextProvider.getScope(); - return scopeUuid ? this.state.getScope(scopeUuid) : undefined; + startScope = (): string => { + const uuid = randomUuid(); + this.state.setScope(uuid); + return uuid; }; - /** - * Creates a new scope. The scope is pushed into the context unless the `manual` - * option is set to `true`. - * - * @param opts - * @returns - */ - startScope = (opts: StartScopeOpts = {}) => this.startScopeWithUuid(randomUuid(), opts); - - updateScope = (updateFunc: (scope: TestScope) => void, uuid?: string) => { - const resolvedUuid = uuid ?? this.contextProvider.getScope(); - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("No current scope to update!"); - return; - } - - const scope = this.state.getScope(resolvedUuid); + updateScope = (uuid: string, updateFunc: (scope: TestScope) => void): void => { + const scope = this.state.getScope(uuid); if (!scope) { // eslint-disable-next-line no-console - console.error(`No scope ${resolvedUuid} to update!`); + console.error(`count not update scope: no scope with uuid ${uuid} is found`); return; } updateFunc(scope); }; - /** - * Removes a scope from the context. Use `writeScope` to emit its fixtures on disk then. - * - * If you just want to write the current stop, you may omit the call to this method and - * call `writeScope` with no uuid. - * - * @param uuid The UUID of the scope. If not provided, the current scope will be stopped. - * - * @returns The UUID of the scope that has been stopped. - */ - stopScope = (uuid?: string) => { - const resolvedUuid = uuid ?? this.contextProvider.getScope(); - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("No current scope to stop!"); - return; - } - - this.contextProvider.removeScope(uuid); - return resolvedUuid; - }; - - /** - * Writes all fixtures of a scope on disk. - * - * @param uuid The UUID of the scope. If not provided, the current scope will - * be written and removed from the context. Don't call `stopScope` in that case. - */ - writeScope = (uuid?: string) => { - const resolvedUuid = uuid ?? this.stopScope(); - - if (!resolvedUuid) { - return; - } - - const scope = this.state.getScope(resolvedUuid); + writeScope = (uuid: string) => { + const scope = this.state.getScope(uuid); if (!scope) { // eslint-disable-next-line no-console - console.error(`No scope ${resolvedUuid} to write!`); + console.error(`count not write scope: no scope with uuid ${uuid} is found`); return; } - this.writeAllFixturesOfScope(scope); - this.removeScopeFromParent(scope); - this.state.deleteScope(resolvedUuid); + this.#writeFixturesOfScope(scope); + this.state.deleteScope(scope.uuid); }; - /** - * Creates a new fixture result and puts it in the context as the current one. - * - * Use the `scope` parameter to control the fixture's scope. Use `updateScope` - * or `linkFixtures` to associate fixtures with tests that can't be linked - * automatically. - * - * Use `stopFixture` once the fixture is completed. - * - * Use `writeScope` or `writeFixture` to emit fixtures on disk. - * - * @param type The type of the fixture. It's either `"before"` or `"after"`. - * @param fixtureResult The fixture result data. - * @param scope - * @param dedicatedScope - * @param tests - * @returns The UUID of the new fixture. - */ - startFixture = ( - type: FixtureType, - fixtureResult: Partial, - { scope, dedicatedScope, tests }: StartFixtureOpts = {}, - ) => { - dedicatedScope = dedicatedScope || (scope === null && !!tests); - const scopeObj = this.resolveScope(scope); - if (scopeObj === undefined) { + startFixture = (scopeUuid: string, type: FixtureType, fixtureResult: Partial): string | undefined => { + const scope = this.state.getScope(scopeUuid); + if (!scope) { // eslint-disable-next-line no-console - console.error("Can't resolve the scope for a new fixture"); + console.error(`count not start fixture: no scope with uuid ${scopeUuid} is found`); return; } @@ -308,322 +145,157 @@ export class ReporterRuntime { ...fixtureResult, }); - if (dedicatedScope || (tests && scopeObj === null)) { - this.setUpFixtureDedicatedScope(wrappedFixture, tests, scopeObj); - } else if (scopeObj !== null) { - this.linkFixtureToScope(wrappedFixture, scopeObj, tests); - } - - this.contextProvider.setFixture(uuid); + scope.fixtures.push(wrappedFixture); return uuid; }; - updateFixture = (updateFunc: (result: FixtureResult) => void, uuid?: string) => { - const resolvedUuid = uuid ?? this.contextProvider.getFixture(); - - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("No current fixture to update!"); - return; - } - - const fixture = this.state.getFixture(resolvedUuid); + updateFixture = (uuid: string, updateFunc: (result: FixtureResult) => void): void => { + const fixture = this.state.getFixtureResult(uuid); if (!fixture) { // eslint-disable-next-line no-console - console.error(`No fixture (${resolvedUuid}) to update!`); + console.error(`could not update fixture: no fixture with uuid ${uuid} is found`); return; } updateFunc(fixture); }; - /** - * Stops a fixture and removes it from the context. The fixture result will persist in - * the storage until it's written on disk with `writeScope` or `writeFixture`. - * - * @returns The UUID of the stopped fixture. - */ - stopFixture = ({ uuid, stop }: StopOpts = {}) => { - const resolvedUuid = uuid ?? this.contextProvider.getFixture(); - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("No current fixture to stop!"); - return; - } - - const fixture = this.state.getFixture(resolvedUuid); + stopFixture = (uuid: string, stop?: number): void => { + const fixture = this.state.getFixtureResult(uuid); if (!fixture) { // eslint-disable-next-line no-console - console.error(`No fixture (${resolvedUuid}) to stop!`); + console.error(`could not stop fixture: no fixture with uuid ${uuid} is found`); return; } - this.stopFixtureObj(fixture, uuid, stop); - return resolvedUuid; + fixture.stop = stop ?? Date.now(); + fixture.stage = Stage.FINISHED; }; - /** - * Use to associate fixtures, scopes, and tests with each other. - * - * At least two arguments must be provided. - */ - linkFixtures = ({ fixtures = [], scope, tests = [] }: LinkFixturesOpts) => { - const wrappedFixtures = fixtures - .map((f) => { - const obj = this.state.getWrappedFixture(f); - if (obj === undefined) { - // eslint-disable-next-line no-console - console.error(`No fixture (${f}) to link!`); - } - return obj; - }) - .filter((f) => f) as FixtureWrapper[]; + startTest = (result: Partial, scopeUuids: string[] = []): string => { + const uuid = randomUuid(); + const testResult: TestResult = { + ...createTestResult(uuid), + start: Date.now(), + ...deepClone(result), + }; - const scopeObj = scope ? this.state.getScope(scope) : null; - if (scopeObj === undefined) { - // eslint-disable-next-line no-console - console.error(`No scope (${scope!}) to link!`); - return; - } + this.notifier.beforeTestResultStart(testResult); - if (wrappedFixtures.length && scopeObj) { - this.linkFixturesToScope(wrappedFixtures, scopeObj, tests); - return; - } - - if (wrappedFixtures.length && tests.length) { - for (const fixture of wrappedFixtures) { - if (fixture.scope) { - this.linkTestsToScope(fixture.scope, tests); - } else { - this.setUpFixtureDedicatedScope(fixture, tests); - } + scopeUuids.forEach((scopeUuid) => { + const scope = this.state.getScope(scopeUuid); + if (!scope) { + // eslint-disable-next-line no-console + console.error(`count not link test to the scope: no scope with uuid ${uuid} is found`); + return; } - return; - } - - if (scopeObj && tests) { - this.linkTestsToScope(scopeObj, tests); - return; - } - - // eslint-disable-next-line no-console - console.error("Provide at least two arguments to link!"); - }; - - /** - * Emits a fixture on disk. Calls `stopFixture` prior to that in case the fixture - * hasn't been stopped yet. Use this method if you want to manage fixtures manually. - * Otherwise, use `writeScope`. - * - * If called without parameters, implicitly calls `stopFixture`. Make sure you don't call - * `stopFixture` by yourself in that case. - * - * The method has no effect if the fixture isn't associated with at least one test. - * - * @param uuid The UUID of the fixture. If not provided, the current fixture will - * be stopped and emitted. Don't call `stopFixture` in that case. - */ - writeFixture = (uuid?: string) => { - const resolvedUuid = uuid ?? this.stopFixture(); - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("Unable to stop the current fixture before write!"); - return; - } - - const wrappedFixture = this.state.getWrappedFixture(resolvedUuid); - if (!wrappedFixture) { - // eslint-disable-next-line no-console - console.error(`No fixture (${resolvedUuid}) to write!`); - return; - } - - const fixture = wrappedFixture.value; - if (fixture.stage !== Stage.FINISHED) { - this.stopFixtureObj(wrappedFixture.value, resolvedUuid); - } - - const { scope } = wrappedFixture; - if (scope) { - this.writeContainer(scope.tests, wrappedFixture); - this.removeFixtureFromScope(scope, wrappedFixture); - } - - this.state.deleteFixtureResult(resolvedUuid); - }; - - startTest = (result: Partial, { scope, dedicatedScope }: StartTestOpts = {}) => { - const stateObject = this.createTestResult(result); - const uuid = stateObject.uuid; - - this.notifier.beforeTestResultStart(stateObject); - - const resolvedScope = dedicatedScope - ? this.startScopeWithUuid(uuid, { - manual: scope !== undefined, - parent: scope ?? undefined, - }) - : scope ?? this.contextProvider.getScope(); - - if (resolvedScope) { - this.introduceTestIntoScopes(uuid, resolvedScope); - } - - this.state.setTestResult(uuid, stateObject); - this.contextProvider.setTest(uuid); - - this.notifier.afterTestResultStart(stateObject); + scope.tests.push(uuid); + }); + this.state.setTestResult(uuid, testResult); + this.notifier.afterTestResultStart(testResult); return uuid; }; - /** - * Updates test result by uuid - * @example - * ```ts - * runtime.update(uuid, (result) => { - * // change the result directly, you don't need to return anything - * result.name = "foo"; - * }); - * ``` - * @param updateFunc - a function that updates the test result; the result is passed as a single argument and should be mutated to apply the changes - * @param uuid - test result uuid - */ - updateTest = (updateFunc: (result: TestResult) => void, uuid?: string) => { - const resolvedUuid = uuid ?? this.contextProvider.getTest(); - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("No current test to update!"); - return; - } - const targetResult = this.state.getTest(resolvedUuid); + updateTest = (uuid: string, updateFunc: (result: TestResult) => void): void => { + const testResult = this.state.getTestResult(uuid); - if (!targetResult) { + if (!testResult) { // eslint-disable-next-line no-console - console.error(`No test (${resolvedUuid}) to update!`); + console.error(`could not update test result: no test with uuid ${uuid}) is found`); return; } - this.notifier.beforeTestResultUpdate(targetResult); - updateFunc(targetResult); - this.notifier.afterTestResultUpdate(targetResult); + this.notifier.beforeTestResultUpdate(testResult); + updateFunc(testResult); + this.notifier.afterTestResultUpdate(testResult); }; - stopTest = ({ uuid, stop }: StopOpts = {}) => { - const resolvedUuid = uuid ?? this.contextProvider.getTest(); - if (!resolvedUuid) { - // eslint-disable-next-line no-console - console.error("No current test to stop!"); - return; - } - - const targetResult = this.state.getTest(resolvedUuid); - if (!targetResult) { + stopTest = (uuid: string, stop?: number) => { + const testResult = this.state.getTestResult(uuid); + if (!testResult) { // eslint-disable-next-line no-console - console.error(`No test (${resolvedUuid}) to stop!`); + console.error(`could not stop test result: no test with uuid ${uuid}) is found`); return; } - this.notifier.beforeTestResultStop(targetResult); - targetResult.testCaseId ??= getTestResultTestCaseId(targetResult); - targetResult.historyId ??= getTestResultHistoryId(targetResult); - targetResult.stop = stop || Date.now(); + this.notifier.beforeTestResultStop(testResult); + testResult.testCaseId ??= getTestResultTestCaseId(testResult); + testResult.historyId ??= getTestResultHistoryId(testResult); + testResult.stop = stop ?? Date.now(); - this.notifier.afterTestResultStop(targetResult); + this.notifier.afterTestResultStop(testResult); }; - /** - * Writes a test result on disk and removes it from the storage and the context. - * @param uuid The UUID of the test. If not set, the current test result is written. - */ - writeTest = (uuid?: string) => { - const resolvedUuid = uuid ?? this.contextProvider.getTest(); - if (!resolvedUuid) { + writeTest = (uuid: string) => { + const testResult = this.state.testResults.get(uuid); + if (!testResult) { // eslint-disable-next-line no-console - console.error("No current test to write!"); + console.error(`could not write test result: no test with uuid ${uuid} is found`); return; } - const testResult = this.state.testResults.get(resolvedUuid); - if (!testResult) { - // eslint-disable-next-line no-console - console.error(`No test (${resolvedUuid}) to write!`); + if (testResult.labels.find((label) => label.name === "ALLURE_TESTPLAN_SKIP")) { + this.state.deleteTestResult(uuid); return; } this.notifier.beforeTestResultWrite(testResult); this.writer.writeResult(testResult); - this.contextProvider.removeTest(uuid); - this.state.deleteTestResult(resolvedUuid); - - const currentScope = this.contextProvider.getScope(); - if (currentScope === resolvedUuid) { - // Writes the scope introduced into the context by `startTest` with - // `dedicatedScope` set to `true`. - this.writeScope(); - } + this.state.deleteTestResult(uuid); this.notifier.afterTestResultWrite(testResult); }; - /** - * Starts a new step and pushes it into the context. - * - * @param result Data to be put into the step result object. - * @param uuid The UUID of a test or fixture to attach the step to. If not set, the UUID of the current fixture is used. - * If no fixture is running, the UUID of the current test is used. - * - * @returns The UUID of the step. - */ - startStep = (result: Partial, uuid?: string) => { - const parentUuid = this.contextProvider.getExecutingItem(uuid); - if (!parentUuid) { - // eslint-disable-next-line no-console - console.error("No current step, fixture, or test to start a new step!"); - return; - } + currentStep = (rootUuid: string): string | undefined => { + return this.stepStack.currentStep(rootUuid); + }; - const parent = this.state.getExecutionItem(parentUuid); + startStep = ( + rootUuid: string, + parentStepUuid: string | null | undefined, + result: Partial, + ): string | undefined => { + const parent = this.#findParent(rootUuid, parentStepUuid); if (!parent) { // eslint-disable-next-line no-console - console.error(`No execution item (${parentUuid}) to start a step!`); + console.error( + `could not start test step: no context for root ${rootUuid} and parentStepUuid ${JSON.stringify(parentStepUuid)} is found`, + ); return; } + const stepResult: StepResult = { + ...createStepResult(), + start: Date.now(), + ...result, + }; + parent.steps.push(stepResult); + const stepUuid = randomUuid(); + this.state.setStepResult(stepUuid, stepResult); - return this.addStepToItem(result, uuid, parent); - }; + this.stepStack.addStep(rootUuid, stepUuid); - updateStep = (updateFunc: (stepResult: StepResult) => void, uuid?: string) => { - const stepUuid = this.contextProvider.getStep(uuid); - if (!stepUuid) { - this.logMissingStepRoot(uuid, "update"); - return; - } + return stepUuid; + }; - const step = this.state.getStep(stepUuid)!; + updateStep = (uuid: string, updateFunc: (stepResult: StepResult) => void) => { + const step = this.state.getStepResult(uuid)!; if (!step) { // eslint-disable-next-line no-console - console.error(`No step ${stepUuid} to update!`); + console.error(`could not update test step: no step with uuid ${uuid} is found`); return; } updateFunc(step); }; - stopStep = ({ uuid, stop }: StopOpts = {}) => { - const stepUuid = this.contextProvider.getStep(uuid); - if (!stepUuid) { - this.logMissingStepRoot(uuid, "stop"); - return; - } - - const step = this.state.getStep(stepUuid); + stopStep = (uuid: string, stop?: number) => { + const step = this.state.getStepResult(uuid); if (!step) { // eslint-disable-next-line no-console - console.error(`No step ${stepUuid} to stop!`); + console.error(`could not stop test step: no step with uuid ${uuid} is found`); return; } @@ -632,46 +304,57 @@ export class ReporterRuntime { step.stop = stop ?? Date.now(); step.stage = Stage.FINISHED; - this.state.deleteStepResult(stepUuid); - this.contextProvider.removeStep(uuid); + this.stepStack.removeStep(uuid); this.notifier.afterStepStop(step); }; - writeAttachment = (attachmentName: string, attachmentContent: Buffer, options: AttachmentOptions, uuid?: string) => { - const target = this.getCurrentExecutingItem(uuid); - if (!target) { - if (uuid) { - // eslint-disable-next-line no-console - console.error(`No test or fixture ${uuid} to attach!`); - } else { - // eslint-disable-next-line no-console - console.error("No current test or fixture to attach!"); - } + writeAttachment = ( + rootUuid: string, + parentStepUuid: string | null | undefined, + attachmentName: string, + attachmentContentOrPath: Buffer | string, + options: AttachmentOptions & { wrapInStep?: boolean; timestamp?: number }, + ) => { + const parent = this.#findParent(rootUuid, parentStepUuid); + if (!parent) { + // eslint-disable-next-line no-console + console.error( + `could not write test attachment: no context for root ${rootUuid} and parentStepUuid ${JSON.stringify(parentStepUuid)} is found`, + ); return; } - this.writeAttachmentForItem(attachmentName, attachmentContent, options, target); - }; + const isPath = typeof attachmentContentOrPath === "string"; + const fileExtension = options.fileExtension ?? (isPath ? extname(attachmentContentOrPath) : undefined); + const attachmentFileName = buildAttachmentFileName({ + contentType: options.contentType, + fileExtension, + }); - writeAttachmentFromPath = ( - attachmentName: string, - attachmentPath: string, - options: AttachmentOptions, - uuid?: string, - ) => { - const target = this.getCurrentExecutingItem(uuid); - if (!target) { - if (uuid) { - // eslint-disable-next-line no-console - console.error(`No test or fixture ${uuid} to attach!`); - } else { - // eslint-disable-next-line no-console - console.error("No current test or fixture to attach!"); - } - return; + if (isPath) { + this.writer.writeAttachmentFromPath(attachmentFileName, attachmentContentOrPath); + } else { + this.writer.writeAttachment(attachmentFileName, attachmentContentOrPath); + } + + const attachment: Attachment = { + name: attachmentName, + source: attachmentFileName, + type: options.contentType, + }; + + if (options.wrapInStep) { + const { timestamp = Date.now() } = options; + parent.steps.push({ + name: attachmentName, + attachments: [attachment], + start: timestamp, + stop: timestamp, + } as StepResult); + } else { + parent.attachments.push(attachment); } - this.writeAttachmentForItem(attachmentName, attachmentPath, options, target); }; writeEnvironmentInfo = () => { @@ -702,314 +385,149 @@ export class ReporterRuntime { this.writer.writeCategoriesDefinitions(serializedCategories); }; - applyRuntimeMessages = ( - messages: Messages[] = [], - { testUuid, fixtureUuid, customHandler }: ApplyMessagesOpts = {}, - ) => { - const resolvedTestUuid = testUuid ?? this.contextProvider.getTest(); - const resolvedFixtureUuid = fixtureUuid ?? this.contextProvider.getFixture(); - const resolvedRootUuid = resolvedFixtureUuid ?? resolvedTestUuid ?? this.contextProvider.getStepRoot(); + applyRuntimeMessages = (rootUuid: string, messages: RuntimeMessage[]) => { + messages.forEach((message) => { + switch (message.type) { + case "metadata": + this.#handleMetadataMessage(rootUuid, message.data); + return; + case "step_metadata": + this.#handleStepMetadataMessage(rootUuid, message.data); + return; + case "step_start": + this.#handleStartStepMessage(rootUuid, message.data); + return; + case "step_stop": + this.#handleStopStepMessage(rootUuid, message.data); + return; + case "attachment_content": + this.#handleAttachmentContentMessage(rootUuid, message.data); + return; + case "attachment_path": + this.#handleAttachmentPathMessage(rootUuid, message.data); + return; + default: + // eslint-disable-next-line no-console + console.error(`could not apply runtime messages: unknown message ${JSON.stringify(message)}`); + return; + } + }); + }; - if (!resolvedRootUuid) { - // eslint-disable-next-line no-console - console.error("No current fixture or test to apply runtime messages to!"); + #handleMetadataMessage = (rootUuid: string, message: RuntimeMetadataMessage["data"]) => { + // only display name could be set to fixture. + const fixtureResult = this.state.getFixtureResult(rootUuid); + if (fixtureResult) { + this.updateFixture(rootUuid, (result) => { + if (message.displayName) { + result.name = message.displayName; + } + }); return; } - const fixture = resolvedFixtureUuid ? this.state.getFixture(resolvedFixtureUuid) : undefined; - const test = resolvedTestUuid ? this.state.getTest(resolvedTestUuid) : undefined; - const root = fixture ?? test; + const { links, labels, parameters, displayName, ...rest } = message; + this.updateTest(rootUuid, (result) => { + if (links) { + result.links = [...result.links, ...(this.linkConfig ? formatLinks(this.linkConfig, links) : links)]; + } + if (labels) { + result.labels = [...result.labels, ...labels]; + } + if (parameters) { + result.parameters = [...result.parameters, ...parameters]; + } + if (displayName) { + result.name = displayName; + } + Object.assign(result, rest); + }); + }; - if (!root) { + #handleStepMetadataMessage = (rootUuid: string, message: RuntimeStepMetadataMessage["data"]) => { + const stepUuid = this.currentStep(rootUuid); + if (!stepUuid) { // eslint-disable-next-line no-console - console.error(`No fixture or test (${resolvedRootUuid}) to apply runtime messages to!`); + console.error("could not handle step metadata message: no step is running"); return; } - - const targets: MessageTargets = { - fixtureUuid: resolvedFixtureUuid ?? undefined, - fixture, - testUuid: resolvedTestUuid ?? undefined, - test, - rootUuid: resolvedRootUuid, - root, - }; - - for (const message of messages) { - const step = this.getCurrentStep(resolvedRootUuid); - - targets.step = step; - - const unhandledMessage = this.handleBuiltInMessage(message, targets); - - if (unhandledMessage && customHandler) { - customHandler(unhandledMessage, fixture, test, step); + const { name, parameters } = message; + this.updateStep(stepUuid, (stepResult) => { + if (name) { + stepResult.name = name; } - } - }; - - protected createTestResult(result: Partial): TestResult { - const uuid = randomUuid(); - return { - ...createTestResult(uuid), - start: Date.now(), - ...deepClone(result), - }; - } - - private handleBuiltInMessage = (message: Messages, targets: MessageTargets) => { - switch (message.type) { - case "metadata": - this.handleMetadataMessage(message as RuntimeMetadataMessage, targets); - return; - case "step_start": - this.handleStepStartMessage(message as RuntimeStartStepMessage, targets); - return; - case "step_metadata": - this.handleStepMetadataMessage(message as RuntimeStepMetadataMessage, targets); - return; - case "step_stop": - this.handleStepStopMessage(message as RuntimeStopStepMessage, targets); - return; - case "attachment_content": - this.handleAttachmentContentMessage(message as RuntimeAttachmentContentMessage, targets); - return; - case "attachment_path": - this.handleAttachmentPathMessage(message as RuntimeAttachmentPathMessage, targets); - return; - default: - return message as Exclude, RuntimeMessage>; - } - }; - - private handleMetadataMessage = (message: RuntimeMetadataMessage, { test, root, step }: MessageTargets) => { - const { links = [], attachments = [], displayName, parameters = [], labels = [], ...rest } = message.data; - const formattedLinks = formatLinks(this.links, links); - - if (displayName) { - root.name = displayName; - } - - if (test) { - test.links = test.links.concat(formattedLinks); - test.labels = test.labels.concat(labels); - test.parameters = test.parameters.concat(parameters); - Object.assign(test, rest); - } - - const attachmentTarget = step || root; - attachmentTarget.attachments = attachmentTarget.attachments.concat(attachments); + if (parameters) { + stepResult.parameters = [...stepResult.parameters, ...parameters]; + } + }); }; - private handleStepStartMessage = (message: RuntimeStartStepMessage, { rootUuid, root, step }: MessageTargets) => - this.addStepToItem({ ...message.data }, rootUuid, step ?? root); - - private handleStepMetadataMessage = (message: RuntimeStepMetadataMessage, { rootUuid, step }: MessageTargets) => { - if (!step) { - // eslint-disable-next-line no-console - console.error(`No current step of ${rootUuid} to apply the metadata`); - return; - } - const { name, parameters } = message.data; - if (name) { - step.name = name; - } - if (parameters?.length) { - step.parameters = step.parameters.concat(parameters); - } + #handleStartStepMessage = (rootUuid: string, message: RuntimeStartStepMessage["data"]) => { + this.startStep(rootUuid, undefined, { ...message }); }; - private handleStepStopMessage = (message: RuntimeStopStepMessage, { rootUuid, step }: MessageTargets) => { - if (!step) { + #handleStopStepMessage = (rootUuid: string, message: RuntimeStopStepMessage["data"]) => { + const stepUuid = this.currentStep(rootUuid); + if (!stepUuid) { // eslint-disable-next-line no-console - console.error(`No current step of ${rootUuid} to stop`); + console.error("could not handle step stop message: no step is running"); return; } - - const { status, stage, stop, ...rest } = message.data; - - // we should not override the status and stage if they are already set - if (step.status === undefined) { - step.status = status; - } - - if (step.stage === undefined) { - step.stage = stage; - } - - Object.assign(step, rest); - - this.stopStep({ uuid: rootUuid, stop }); - }; - - private handleAttachmentContentMessage = ( - message: RuntimeAttachmentContentMessage, - { root, step }: MessageTargets, - ) => { - const item: FixtureResult | TestResult | StepResult = step ?? root; - const { name, content, encoding, contentType, fileExtension, wrapInStep } = message.data; - this.writeAttachmentForItem( - name, - Buffer.from(content, encoding), - { - contentType, - fileExtension, - }, - item, - wrapInStep, - ); - }; - - private handleAttachmentPathMessage = (message: RuntimeAttachmentPathMessage, { root, step }: MessageTargets) => { - const item: FixtureResult | TestResult | StepResult = step ?? root; - const { name, path, contentType, fileExtension, wrapInStep } = message.data; - this.writeAttachmentForItem(name, path, { contentType, fileExtension }, item, wrapInStep); - }; - - private writeAttachmentForItem = ( - attachmentName: string, - attachmentContentOrPath: Buffer | string, - options: Pick, - item: StepResult | TestResult | FixtureResult, - wrapInStepAttachment: boolean = false, - ) => { - const isPath = typeof attachmentContentOrPath === "string"; - const fileExtension = options.fileExtension ?? (isPath ? extname(attachmentContentOrPath) : undefined); - const attachmentFileName = buildAttachmentFileName({ - contentType: options.contentType, - fileExtension, - }); - - if (isPath) { - this.writer.writeAttachmentFromPath(attachmentFileName, attachmentContentOrPath); - } else { - this.writer.writeAttachment(attachmentFileName, attachmentContentOrPath); - } - - const attachment: Attachment = { - name: attachmentName, - source: attachmentFileName, - type: options.contentType, - }; - - if (wrapInStepAttachment) { - item.steps.push({ name: attachmentName, attachments: [attachment] } as StepResult); - } else { - item.attachments.push(attachment); - } - }; - - private startScopeWithUuid = (uuid: string, { manual, parent }: StartScopeOpts = {}) => { - const newScope = this.state.setScope(uuid); - - if (!manual) { - parent = this.contextProvider.getScope() ?? undefined; - this.contextProvider.addScope(uuid); - } - if (parent) { - const parentScope = this.state.getScope(parent); - if (parentScope) { - this.linkScopes(parentScope, newScope); + this.updateStep(stepUuid, (result) => { + if (message.status && !result.status) { + result.status = message.status; } - } - return uuid; - }; - - private resolveScope = (scopeUuid: string | undefined | null) => { - if (scopeUuid === null) { - return null; - } - scopeUuid = scopeUuid ?? this.contextProvider.getScope(); - return scopeUuid ? this.state.getScope(scopeUuid) : null; - }; - - private removeScopeFromParent = (scope: TestScope) => { - const { subScopes } = scope.parent ?? {}; - if (subScopes) { - const scopeIndex = subScopes.indexOf(scope); - if (scopeIndex !== -1) { - subScopes.splice(scopeIndex, 1); + if (message.statusDetails) { + result.statusDetails = { ...result.statusDetails, ...message.statusDetails }; } - } - }; - - private removeFixtureFromScope = ({ fixtures }: TestScope, wrappedFixture: FixtureWrapper) => { - const fixtureIndex = fixtures.indexOf(wrappedFixture); - if (fixtureIndex !== -1) { - fixtures.splice(fixtureIndex, 1); - } - }; - - private setUpFixtureDedicatedScope = ( - wrappedFixture: FixtureWrapper, - tests: readonly string[] | undefined, - parentScope?: TestScope | null, - ) => { - const scope = this.state.setScope(wrappedFixture.uuid, { - fixtures: [wrappedFixture], - tests: [...(tests ?? [])], }); - wrappedFixture.scope = scope; - if (parentScope) { - this.linkScopes(parentScope, scope); - } + this.stopStep(stepUuid, message.stop); }; - private linkScopes = (parent: TestScope, child: TestScope) => { - child.parent = parent; - parent.subScopes.push(child); + #handleAttachmentContentMessage = (rootUuid: string, message: RuntimeAttachmentContentMessage["data"]) => { + this.writeAttachment(rootUuid, undefined, message.name, Buffer.from(message.content, message.encoding), { + encoding: message.encoding, + contentType: message.contentType, + fileExtension: message.fileExtension, + wrapInStep: message.wrapInStep, + timestamp: message.timestamp, + }); }; - private linkFixturesToScope = ( - wrappedFixtures: readonly FixtureWrapper[], - scope: TestScope, - extraTests: readonly string[] | undefined, - ) => { - for (const fixture of wrappedFixtures) { - this.linkFixtureToScope(fixture, scope, extraTests); - } + #handleAttachmentPathMessage = (rootUuid: string, message: RuntimeAttachmentPathMessage["data"]) => { + this.writeAttachment(rootUuid, undefined, message.name, message.path, { + contentType: message.contentType, + fileExtension: message.fileExtension, + wrapInStep: message.wrapInStep, + timestamp: message.timestamp, + }); }; - private linkFixtureToScope = ( - wrappedFixture: FixtureWrapper, - scope: TestScope, - extraTests: readonly string[] | undefined, - ) => { - if (wrappedFixture.scope) { - const fixtureIndex = wrappedFixture.scope.fixtures.indexOf(wrappedFixture); - if (fixtureIndex !== -1) { - wrappedFixture.scope.fixtures.splice(fixtureIndex, 1); - } - } - - wrappedFixture.scope = scope; - scope.fixtures.push(wrappedFixture); - if (extraTests) { - this.linkTestsToScope(scope, extraTests); + #findParent = ( + rootUuid: string, + parentStepUuid: string | null | undefined, + ): FixtureResult | TestResult | StepResult | undefined => { + const root = this.state.getExecutionItem(rootUuid); + if (!root) { + return; } - }; - - private stopFixtureObj = (fixture: FixtureResult, uuid?: string, stop?: number) => { - fixture.stop = stop ?? Date.now(); - fixture.stage = Stage.FINISHED; - - this.contextProvider.removeFixture(uuid); - }; - private writeAllFixturesOfScope = (root: TestScope) => { - const stack = [root]; - for (let scope = stack.pop(); scope; scope = stack.pop()) { - this.writeFixturesOfScope(scope); - this.state.deleteScope(scope.uuid); + if (parentStepUuid === null) { + return root; + } else if (parentStepUuid === undefined) { + const stepUuid = this.currentStep(rootUuid); + return stepUuid ? this.state.getStepResult(stepUuid) : root; + } else { + return this.state.getStepResult(parentStepUuid); } }; - private writeFixturesOfScope = ({ fixtures, tests }: TestScope) => { + #writeFixturesOfScope = ({ fixtures, tests }: TestScope) => { const writtenFixtures = new Set(); if (tests.length) { for (const wrappedFixture of fixtures) { if (!writtenFixtures.has(wrappedFixture.uuid)) { - this.writeContainer(tests, wrappedFixture); + this.#writeContainer(tests, wrappedFixture); this.state.deleteFixtureResult(wrappedFixture.uuid); writtenFixtures.add(wrappedFixture.uuid); } @@ -1017,62 +535,16 @@ export class ReporterRuntime { } }; - private writeContainer = (tests: string[], wrappedFixture: FixtureWrapper) => { + #writeContainer = (tests: string[], wrappedFixture: FixtureWrapper) => { const fixture = wrappedFixture.value; const befores = wrappedFixture.type === "before" ? [wrappedFixture.value] : []; const afters = wrappedFixture.type === "after" ? [wrappedFixture.value] : []; this.writer.writeGroup({ - uuid: randomUuid(), + uuid: wrappedFixture.uuid, name: fixture.name, children: [...new Set(tests)], befores, afters, }); }; - - private addStepToItem = ( - data: Partial, - rootUuid: string | undefined, - parent: StepResult | TestResult | FixtureResult, - ) => { - const stepResult: StepResult = { - ...createStepResult(), - start: Date.now(), - ...data, - }; - parent.steps.push(stepResult); - const stepUuid = randomUuid(); - this.state.setStepResult(stepUuid, stepResult); - - this.contextProvider.addStep(stepUuid, rootUuid); - - return stepUuid; - }; - - private logMissingStepRoot = (uuid: string | undefined, op: string) => { - if (uuid) { - // eslint-disable-next-line no-console - console.error(`No test or fixture of (${uuid}) to ${op} the step!`); - } else { - // eslint-disable-next-line no-console - console.error(`No current step to ${op}!`); - } - }; - - private introduceTestIntoScopes = (testUuid: string, scopeUuid: string) => { - const scope = this.state.getScope(scopeUuid); - if (!scope) { - // eslint-disable-next-line no-console - console.error(`No scope ${scopeUuid} to introduce the test into`); - return; - } - - this.linkTestsToScope(scope, [testUuid]); - }; - - private linkTestsToScope = (scope: TestScope, testUuids: readonly string[]) => { - for (let curScope: TestScope | undefined = scope; curScope; curScope = curScope.parent) { - curScope.tests.splice(curScope.tests.length, 0, ...testUuids); - } - }; } diff --git a/packages/allure-js-commons/src/sdk/reporter/context/AllureContextProviderBase.ts b/packages/allure-js-commons/src/sdk/reporter/context/AllureContextProviderBase.ts deleted file mode 100644 index 359f0e950..000000000 --- a/packages/allure-js-commons/src/sdk/reporter/context/AllureContextProviderBase.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint brace-style: 0 */ -import type { AllureContext, AllureContextHolder, AllureContextProvider } from "./types.js"; - -/** - * Provides the set of methods to access and update the context. - * Successor classes are responsible for persisting and accessing the context. - */ -export abstract class AllureContextProviderBase< - TContext extends AllureContext, - THolder extends AllureContextHolder, -> implements AllureContextProvider -{ - /** - * Gets the holder that contains the current value of the context. - */ - protected abstract load: () => THolder; - - /** - * Persist the changes applied to the context since it was last time persisted. - */ - protected abstract store: (holder: THolder) => void; - - getScope = () => this.getCurrentContext().getScope(); - - getFixture = () => this.getCurrentContext().getFixture(); - - getTest = () => this.getCurrentContext().getTest(); - - getStepRoot = () => this.getFixture() ?? this.getTest(); - - getStep = (root?: string) => { - const resolvedRoot = root ?? this.getStepRoot(); - if (resolvedRoot) { - return this.getCurrentContext().getStep(resolvedRoot); - } - return null; - }; - - getExecutingItem = (root?: string) => { - const resolvedRoot = root ?? this.getStepRoot(); - if (resolvedRoot) { - return this.getStep(resolvedRoot) ?? resolvedRoot; - } - return null; - }; - - addScope = (uuid: string) => this.update((b) => b.addScope(uuid)); - - removeScope = (uuid?: string) => this.update((b) => (uuid ? b.removeScopeByUuid(uuid) : b.removeScope())); - - setFixture = (uuid: string) => this.update((b) => b.setFixture(uuid)); - - removeFixture = (uuid?: string) => { - if (!uuid || this.getFixture() === uuid) { - this.update((b) => b.removeFixture()); - } - }; - - setTest = (uuid: string) => this.update((b) => b.setTest(uuid)); - - removeTest = (uuid?: string) => { - if (!uuid || this.getTest() === uuid) { - this.update((b) => b.removeTest()); - } - }; - - addStep = (uuid: string, root?: string) => { - const resolvedRoot = root ?? this.getStepRoot(); - if (resolvedRoot) { - this.update((b) => b.addStep(resolvedRoot, uuid)); - } - }; - - removeStep = (root?: string) => { - const resolvedRoot = root ?? this.getStepRoot(); - if (resolvedRoot) { - this.update((b) => b.removeStep(resolvedRoot)); - } - }; - - private getCurrentContext = () => this.load().get(); - - private update = (fn: (holder: THolder) => void) => { - const holder = this.load(); - fn(holder); - this.store(holder); - }; -} diff --git a/packages/allure-js-commons/src/sdk/reporter/context/StaticAllureContextProvider.ts b/packages/allure-js-commons/src/sdk/reporter/context/StaticAllureContextProvider.ts deleted file mode 100644 index 9bde6a50b..000000000 --- a/packages/allure-js-commons/src/sdk/reporter/context/StaticAllureContextProvider.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { AllureContextProviderBase } from "./AllureContextProviderBase.js"; -import type { AllureContext, AllureContextHolder } from "./types.js"; - -/** - * Allure context that stores its data in mutable class fields. - * Unsafe from the cuncurrency standpoint. - */ -export class MutableAllureContext implements AllureContext { - readonly scopeStack: string[] = []; - currentFixture: string | null = null; - currentTest: string | null = null; - readonly stepStacks: Map = new Map(); - - getScope = () => MutableAllureContext.last(this.scopeStack); - getFixture = () => this.currentFixture; - getTest = () => this.currentTest; - getStep = (scope: string) => MutableAllureContext.last(this.stepStacks.get(scope)); - - private static last = (arr: T[] | undefined) => arr?.[arr.length - 1] ?? null; -} - -/** - * Implements transitioning between context values by mutating the context - * object. - * Unsafe from the cuncurrency standpoint. - */ -export class MutableAllureContextHolder implements AllureContextHolder { - private readonly context: MutableAllureContext = new MutableAllureContext(); - - get = () => this.context; - - addScope = (uuid: string) => { - this.context.scopeStack.push(uuid); - }; - - removeScope = () => { - this.context.scopeStack.pop(); - }; - - removeScopeByUuid = (uuid: string) => MutableAllureContextHolder.removeAllOccurrences(this.context.scopeStack, uuid); - - setFixture = (uuid: string) => { - this.context.currentFixture = uuid; - }; - - removeFixture = () => { - this.context.currentFixture = null; - }; - - setTest = (uuid: string) => { - this.context.currentTest = uuid; - }; - - removeTest = () => { - this.context.currentTest = null; - }; - - addStep = (scope: string, uuid: string) => { - const steps = this.context.stepStacks.get(scope); - if (steps) { - steps.push(uuid); - } else { - this.context.stepStacks.set(scope, [uuid]); - } - }; - - removeStep = (scope: string) => { - const steps = this.context.stepStacks.get(scope); - if (steps) { - steps.pop(); - if (!steps.length) { - this.context.stepStacks.delete(scope); - } - } - }; - - removeStepByUuid = (scope: string, uuid: string) => { - const steps = this.context.stepStacks.get(scope); - if (steps) { - MutableAllureContextHolder.removeAllOccurrences(steps, uuid); - if (!steps.length) { - this.context.stepStacks.delete(scope); - } - } - }; - - private static removeAllOccurrences(arr: T[], val: T) { - for (let i = arr.indexOf(val); i !== -1; i = arr.indexOf(val, i)) { - arr.splice(i, 1); - } - } -} - -/** - * Stores the context in a class field. That's a simple but not async-safe way of - * manipulating the context. - */ -export class StaticContextProvider< - TContext extends AllureContext, - THolder extends AllureContextHolder, -> extends AllureContextProviderBase { - constructor(private readonly holderSingleton: THolder) { - super(); - } - - protected override load = () => this.holderSingleton; - - /* The changes are already persisted in the holder singleton. */ - protected store = (holder: THolder) => { - if (!Object.is(holder, this.holderSingleton)) { - throw new Error("The static context holder can'be replaced with another one."); - } - }; - - /** - * Wraps a context holder singleton in the static context provider. - * @param holderSingleton The singleton to wrap. - */ - // eslint-disable-next-line @typescript-eslint/no-shadow - static wrap = >( - holderSingleton: THolder, - ) => new StaticContextProvider(holderSingleton); -} diff --git a/packages/allure-js-commons/src/sdk/reporter/context/types.ts b/packages/allure-js-commons/src/sdk/reporter/context/types.ts deleted file mode 100644 index 9e1fdcd4f..000000000 --- a/packages/allure-js-commons/src/sdk/reporter/context/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Represents a snapshot of the Allure state at some particular moment during the run. - */ -export type AllureContext = { - getScope: () => string | null; - getFixture: () => string | null; - getTest: () => string | null; - getStep: (root: string) => string | null; -}; - -/** - * Implements the transitions from one snapshot to another. - */ -export type AllureContextHolder = { - get: () => TContext; - - addScope: (uuid: string) => void; - removeScope: () => void; - removeScopeByUuid: (uuid: string) => void; - - setFixture: (uuid: string) => void; - removeFixture: () => void; - - setTest: (uuid: string) => void; - removeTest: () => void; - - addStep: (root: string, uuid: string) => void; - removeStep: (root: string) => void; -}; - -/** - * Provides the set of methods to access and update the context. - */ -export type AllureContextProvider = { - getScope: () => string | null; - getFixture: () => string | null; - getTest: () => string | null; - getStepRoot: () => string | null; - getStep: (root?: string) => string | null; - getExecutingItem: (root?: string) => string | null; - addScope: (uuid: string) => void; - removeScope: (uuid?: string) => void; - setFixture: (uuid: string) => void; - removeFixture: (uuid?: string) => void; - setTest: (uuid: string) => void; - removeTest: (uuid?: string) => void; - addStep: (uuid: string, root?: string) => void; - removeStep: (root?: string, uuid?: string) => void; -}; diff --git a/packages/allure-js-commons/src/sdk/reporter/index.ts b/packages/allure-js-commons/src/sdk/reporter/index.ts index 3f7f283ad..3c73680d6 100644 --- a/packages/allure-js-commons/src/sdk/reporter/index.ts +++ b/packages/allure-js-commons/src/sdk/reporter/index.ts @@ -1,9 +1,5 @@ export type * from "./types.js"; -export { - ALLURE_METADATA_CONTENT_TYPE, - ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE, - ALLURE_SKIPPED_BY_TEST_PLAN_LABEL, -} from "./types.js"; +export { ALLURE_METADATA_CONTENT_TYPE, ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE } from "./types.js"; export * from "./utils.js"; export * from "./testplan.js"; export * from "./factory.js"; diff --git a/packages/allure-js-commons/src/sdk/reporter/testplan.ts b/packages/allure-js-commons/src/sdk/reporter/testplan.ts index 120944f98..dd1b70f9e 100644 --- a/packages/allure-js-commons/src/sdk/reporter/testplan.ts +++ b/packages/allure-js-commons/src/sdk/reporter/testplan.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; import type { TestPlanV1 } from "../types.js"; +import { allureIdRegexp } from "../utils.js"; export const parseTestPlan = (): TestPlanV1 | undefined => { const testPlanPath = process.env.ALLURE_TESTPLAN_PATH; @@ -22,3 +23,19 @@ export const parseTestPlan = (): TestPlanV1 | undefined => { return undefined; } }; + +export const includedInTestPlan = ( + testPlan: TestPlanV1, + subject: { id?: string; fullName?: string; tags?: string[] }, +): boolean => { + const { id, fullName, tags = [] } = subject; + const effectiveId = + id ?? tags.map((tag) => tag?.match(allureIdRegexp)?.groups?.id).find((maybeId) => maybeId !== undefined); + + return testPlan.tests.some((test) => { + const idMatched = effectiveId && test.id ? String(test.id) === effectiveId : false; + const selectorMatched = fullName && test.selector === fullName; + + return idMatched || selectorMatched; + }); +}; diff --git a/packages/allure-js-commons/src/sdk/reporter/types.ts b/packages/allure-js-commons/src/sdk/reporter/types.ts index c14242c56..ab88a3ec3 100644 --- a/packages/allure-js-commons/src/sdk/reporter/types.ts +++ b/packages/allure-js-commons/src/sdk/reporter/types.ts @@ -1,45 +1,9 @@ -import type { - FixtureResult, - Label, - Link, - LinkType, - Parameter, - StepResult, - TestResult, - TestResultContainer, -} from "../../model.js"; +import type { FixtureResult, LinkType, StepResult, TestResult, TestResultContainer } from "../../model.js"; import type { Category, EnvironmentInfo } from "../types.js"; -import type { AllureContextProvider } from "./context/types.js"; export const ALLURE_METADATA_CONTENT_TYPE = "application/vnd.allure.metadata+json"; -export const ALLURE_SKIPPED_BY_TEST_PLAN_LABEL = "allure-skipped-by-test-plan"; export const ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE = "application/vnd.allure.message+json"; -export interface AttachmentMetadata { - name: string; - type: string; - content: string; - encoding: BufferEncoding; -} - -export interface StepMetadata extends Omit { - steps: StepMetadata[]; - attachments: AttachmentMetadata[]; -} - -export interface MetadataMessage { - attachments?: AttachmentMetadata[]; - displayName?: string; - testCaseId?: string; - historyId?: string; - labels?: Label[]; - links?: Link[]; - parameter?: Parameter[]; - description?: string; - descriptionHtml?: string; - steps?: StepMetadata[]; -} - export interface LifecycleListener { beforeTestResultStart?: (result: TestResult) => void; @@ -83,7 +47,6 @@ export interface Config { readonly listeners?: LifecycleListener[]; readonly environmentInfo?: EnvironmentInfo; readonly categories?: Category[]; - readonly contextProvider?: AllureContextProvider; } export interface Writer { @@ -100,15 +63,9 @@ export interface Writer { writeCategoriesDefinitions(categories: Category[]): void; } -export type WellKnownWriters = { - [key: string]: (new (...args: readonly unknown[]) => Writer) | undefined; -}; - export type TestScope = { uuid: string; tests: string[]; - parent?: TestScope; - subScopes: TestScope[]; fixtures: FixtureWrapper[]; }; @@ -117,6 +74,5 @@ export type FixtureType = "before" | "after"; export type FixtureWrapper = { uuid: string; value: FixtureResult; - scope?: TestScope; type: FixtureType; }; diff --git a/packages/allure-js-commons/src/sdk/runtime/MessageTestRuntime.ts b/packages/allure-js-commons/src/sdk/runtime/MessageTestRuntime.ts index 3b84cee8f..cd4af88ec 100644 --- a/packages/allure-js-commons/src/sdk/runtime/MessageTestRuntime.ts +++ b/packages/allure-js-commons/src/sdk/runtime/MessageTestRuntime.ts @@ -7,7 +7,7 @@ import type { ParameterMode, ParameterOptions, } from "../../model.js"; -import { Stage, Status } from "../../model.js"; +import { Status } from "../../model.js"; import type { RuntimeMessage } from "../types.js"; import { getStatusFromError } from "../utils.js"; import type { TestRuntime } from "./types.js"; @@ -120,6 +120,7 @@ export abstract class MessageTestRuntime implements TestRuntime { contentType: options.contentType, fileExtension: options.fileExtension, wrapInStep: true, + timestamp: Date.now(), }, }); } @@ -133,6 +134,7 @@ export abstract class MessageTestRuntime implements TestRuntime { contentType: options.contentType, fileExtension: options.fileExtension, wrapInStep: true, + timestamp: Date.now(), }, }); } @@ -153,7 +155,6 @@ export abstract class MessageTestRuntime implements TestRuntime { type: "step_stop", data: { status: Status.PASSED, - stage: Stage.FINISHED, stop: Date.now(), }, }); @@ -166,7 +167,6 @@ export abstract class MessageTestRuntime implements TestRuntime { type: "step_stop", data: { status: getStatusFromError(err as Error), - stage: Stage.FINISHED, stop: Date.now(), statusDetails: { message, diff --git a/packages/allure-js-commons/src/sdk/types.ts b/packages/allure-js-commons/src/sdk/types.ts index fffe2e93b..c1a865b8a 100644 --- a/packages/allure-js-commons/src/sdk/types.ts +++ b/packages/allure-js-commons/src/sdk/types.ts @@ -1,27 +1,14 @@ -import type { - Attachment, - Label, - Link, - Parameter, - Stage, - Status, - StatusDetails, - TestResult, - TestResultContainer, -} from "../model.js"; +import type { Label, Link, Parameter, Status, StatusDetails, TestResult, TestResultContainer } from "../model.js"; type RuntimeMessageBase = { type: T; }; -type MessageTypes = T extends RuntimeMessageBase ? K : never; - export type RuntimeMetadataMessage = RuntimeMessageBase<"metadata"> & { data: { labels?: Label[]; links?: Link[]; parameters?: Parameter[]; - attachments?: Attachment[]; description?: string; descriptionHtml?: string; testCaseId?: string; @@ -48,7 +35,6 @@ export type RuntimeStopStepMessage = RuntimeMessageBase<"step_stop"> & { data: { stop: number; status: Status; - stage: Stage; statusDetails?: StatusDetails; }; }; @@ -61,6 +47,7 @@ export type RuntimeAttachmentContentMessage = RuntimeMessageBase<"attachment_con contentType: string; fileExtension?: string; wrapInStep?: boolean; + timestamp?: number; }; }; @@ -71,6 +58,7 @@ export type RuntimeAttachmentPathMessage = RuntimeMessageBase<"attachment_path"> contentType: string; fileExtension?: string; wrapInStep?: boolean; + timestamp?: number; }; }; @@ -82,15 +70,9 @@ export type RuntimeMessage = | RuntimeAttachmentContentMessage | RuntimeAttachmentPathMessage; -// Could be used by adapters to define additional message types -export type ExtensionMessage = T extends MessageTypes ? never : RuntimeMessageBase; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export type Messages = T extends RuntimeMessage | ExtensionMessage ? T : never; - export interface TestPlanV1Test { - id: string | number; - selector: string; + id?: string | number; + selector?: string; } export interface TestPlanV1 { diff --git a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts index 17cbedab8..7a88eb9bf 100644 --- a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { Link } from "../../../src/model.js"; +import { type Link, Stage, Status } from "../../../src/model.js"; import { ReporterRuntime } from "../../../src/sdk/reporter/ReporterRuntime.js"; import { mockWriter } from "../../utils/writer.js"; @@ -22,66 +22,199 @@ const fixtures = { }; describe("ReporterRuntime", () => { + it("should start/stop steps", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + + const rootUuid = runtime.startTest({}); + + const t1 = Date.now(); + const stepUuid = runtime.startStep(rootUuid, undefined, { name: "some name" }); + const t2 = Date.now(); + runtime.stopStep(stepUuid!); + const t3 = Date.now(); + + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [step] = testResult.steps; + + expect(step).toEqual( + expect.objectContaining({ + name: "some name", + status: undefined, + stage: Stage.FINISHED, + }), + ); + + expect(step.start).toBeGreaterThanOrEqual(t1); + expect(step.start).toBeLessThanOrEqual(t2); + expect(step.stop).toBeGreaterThanOrEqual(t2); + expect(step.stop).toBeLessThanOrEqual(t3); + }); + + it("should set start/stop time for steps", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + + const rootUuid = runtime.startTest({}); + + const stepUuid = runtime.startStep(rootUuid, undefined, { name: "some name", start: 123 }); + runtime.stopStep(stepUuid!, 321); + + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [step] = testResult.steps; + + expect(step).toEqual( + expect.objectContaining({ + name: "some name", + start: 123, + stop: 321, + }), + ); + }); + + it("should support concurrent tests", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + const test1 = runtime.startTest({ name: "test1" }); + const test2 = runtime.startTest({ name: "test2" }); + runtime.stopTest(test1); + runtime.stopTest(test2); + runtime.writeTest(test1); + runtime.writeTest(test2); + + const [[tr1], [tr2]] = writer.writeResult.mock.calls; + + expect([tr1, tr2]).toEqual([ + expect.objectContaining({ name: "test1" }), + expect.objectContaining({ name: "test2" }), + ]); + }); + + it("should support steps in concurrent tests", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + const test1 = runtime.startTest({ name: "test1" }); + const test2 = runtime.startTest({ name: "test2" }); + const t2_1 = runtime.startStep(test2, undefined, { name: "test2 > 1" }); + const t1_1 = runtime.startStep(test1, undefined, { name: "test1 > 1" }); + const t2_2 = runtime.startStep(test2, undefined, { name: "test2 > 2" }); + const t2_3 = runtime.startStep(test2, undefined, { name: "test2 > 3" }); + const t1_2 = runtime.startStep(test1, undefined, { name: "test1 > 2" }); + runtime.stopStep(t2_3!); + runtime.stopStep(t1_2!); + const t1_3 = runtime.startStep(test1, undefined, { name: "test1 > 3" }); + runtime.stopStep(t2_2!); + runtime.stopStep(t1_3!); + runtime.stopStep(t1_1!); + runtime.stopStep(t2_1!); + + runtime.stopTest(test1); + runtime.stopTest(test2); + runtime.writeTest(test1); + runtime.writeTest(test2); + + const [[tr1], [tr2]] = writer.writeResult.mock.calls; + + expect(tr1.steps).toEqual([ + expect.objectContaining({ + name: "test1 > 1", + steps: expect.arrayContaining([ + expect.objectContaining({ name: "test1 > 2" }), + expect.objectContaining({ name: "test1 > 3" }), + ]), + }), + ]); + expect(tr2.steps).toEqual([ + expect.objectContaining({ + name: "test2 > 1", + steps: expect.arrayContaining([ + expect.objectContaining({ + name: "test2 > 2", + steps: expect.arrayContaining([expect.objectContaining({ name: "test2 > 3" })]), + }), + ]), + }), + ]); + }); + describe("writeAttachmentFromPath", () => { it("should use extension from fileExtension option if specified", () => { const writer = mockWriter(); const runtime = new ReporterRuntime({ writer }); - runtime.startTest({}); + const rootUuid = runtime.startTest({}); - runtime.writeAttachmentFromPath("some attachment", "some/path/to/file", { + runtime.writeAttachment(rootUuid, undefined, "some attachment", "some/path/to/file", { fileExtension: ".mst", contentType: "*/*", }); - const attachment = runtime.getCurrentTest()!.attachments[0]; + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [attachment] = testResult.attachments; expect(attachment.name).to.be.eq("some attachment"); expect(attachment.source).to.match(/.+\.mst/); - const writeAttachmentFromPathCall = writer.writeAttachmentFromPath.mock.calls[0]; + const [destFileName, from] = writer.writeAttachmentFromPath.mock.calls[0]; - expect(writeAttachmentFromPathCall[0]).to.be.eq(attachment.source); - expect(writeAttachmentFromPathCall[1]).to.be.eq("some/path/to/file"); + expect(destFileName).to.be.eq(attachment.source); + expect(from).to.be.eq("some/path/to/file"); }); it("should use extension from original file if fileExtension option is not specified", () => { const writer = mockWriter(); const runtime = new ReporterRuntime({ writer }); - runtime.startTest({}); + const rootUuid = runtime.startTest({}); - runtime.writeAttachmentFromPath("some attachment", "some/path/to/file.abc", { + runtime.writeAttachment(rootUuid, undefined, "some attachment", "some/path/to/file.abc", { contentType: "*/*", }); - const attachment = runtime.getCurrentTest()!.attachments[0]; + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [attachment] = testResult.attachments; expect(attachment.name).to.be.eq("some attachment"); expect(attachment.source).to.match(/.+\.abc/); - const writeAttachmentFromPathCall = writer.writeAttachmentFromPath.mock.calls[0]; + const [destFileName, from] = writer.writeAttachmentFromPath.mock.calls[0]; - expect(writeAttachmentFromPathCall[0]).to.be.eq(attachment.source); - expect(writeAttachmentFromPathCall[1]).to.be.eq("some/path/to/file.abc"); + expect(destFileName).to.be.eq(attachment.source); + expect(from).to.be.eq("some/path/to/file.abc"); }); it("should detect extension by content type if no option or path specified", () => { const writer = mockWriter(); const runtime = new ReporterRuntime({ writer }); - runtime.startTest({}); + const rootUuid = runtime.startTest({}); - runtime.writeAttachment("some other attachment", Buffer.from("attachment content"), { + runtime.writeAttachment(rootUuid, undefined, "some other attachment", Buffer.from("attachment content"), { contentType: "text/csv", }); - const attachment = runtime.getCurrentTest()!.attachments[0]; + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [attachment] = testResult.attachments; expect(attachment.name).to.be.eq("some other attachment"); expect(attachment.source).to.match(/.+\.csv/); - const writeAttachmentFromPathCall = writer.writeAttachment.mock.calls[0]; + const [destFileName, buffer] = writer.writeAttachment.mock.calls[0]; - expect(writeAttachmentFromPathCall[0]).to.be.eq(attachment.source); - expect(writeAttachmentFromPathCall[1].toString("utf-8")).to.be.eq("attachment content"); + expect(destFileName).to.be.eq(attachment.source); + expect(buffer.toString("utf-8")).to.be.eq("attachment content"); }); }); @@ -90,8 +223,8 @@ describe("ReporterRuntime", () => { const writer = mockWriter(); const runtime = new ReporterRuntime({ writer }); - runtime.startTest({}); - runtime.applyRuntimeMessages([ + const rootUuid = runtime.startTest({}); + runtime.applyRuntimeMessages(rootUuid, [ { type: "metadata", data: { @@ -99,7 +232,8 @@ describe("ReporterRuntime", () => { }, }, ]); - runtime.writeTest(); + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); // eslint-disable-next-line @typescript-eslint/unbound-method expect(writer.writeResult).toHaveBeenCalledWith( @@ -125,8 +259,9 @@ describe("ReporterRuntime", () => { }, }); - runtime.startTest({}); - runtime.applyRuntimeMessages([ + const rootUuid = runtime.startTest({}); + + runtime.applyRuntimeMessages(rootUuid, [ { type: "metadata", data: { @@ -134,7 +269,8 @@ describe("ReporterRuntime", () => { }, }, ]); - runtime.writeTest(); + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); // eslint-disable-next-line @typescript-eslint/unbound-method expect(writer.writeResult).toHaveBeenCalledWith( @@ -155,6 +291,76 @@ describe("ReporterRuntime", () => { }), ); }); + + it("should add step parameters", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + + const rootUuid = runtime.startTest({}); + + const stepUuid = runtime.startStep(rootUuid, undefined, { name: "some name" }); + runtime.applyRuntimeMessages(rootUuid, [ + { type: "step_metadata", data: { parameters: [{ name: "p1", value: "v1" }] } }, + ]); + runtime.stopStep(stepUuid!); + + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [step] = testResult.steps; + + expect(step).toEqual( + expect.objectContaining({ + name: "some name", + status: undefined, + stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "p1", value: "v1" })], + }), + ); + }); + + it("should not override step status on step_stop event", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + + const rootUuid = runtime.startTest({}); + + const stepUuid = runtime.startStep(rootUuid, undefined, { name: "some name" }); + runtime.updateStep(stepUuid!, (result) => (result.status = Status.BROKEN)); + runtime.applyRuntimeMessages(rootUuid, [ + { type: "step_stop", data: { status: Status.PASSED, stop: Date.now() } }, + ]); + + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + const [step] = testResult.steps; + + expect(step).toEqual( + expect.objectContaining({ + name: "some name", + status: Status.BROKEN, + }), + ); + }); + + it("should ignore results with ALLURE_TESTPLAN_SKIP label", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + + const rootUuid = runtime.startTest({}); + + runtime.updateTest(rootUuid, (result) => { + result.labels.push({ name: "ALLURE_TESTPLAN_SKIP", value: "any" }); + }); + + runtime.stopTest(rootUuid); + runtime.writeTest(rootUuid); + + expect(writer.writeResult.mock.calls.length).toBe(0); + }); }); describe("load well-known writers", () => { diff --git a/packages/allure-js-commons/test/sdk/reporter/testplan.spec.ts b/packages/allure-js-commons/test/sdk/reporter/testplan.spec.ts index 7b4908a6b..1be3e5097 100644 --- a/packages/allure-js-commons/test/sdk/reporter/testplan.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/testplan.spec.ts @@ -3,7 +3,8 @@ import { mkdtempSync, writeFileSync } from "fs"; import os from "os"; import path from "path"; import { afterEach, describe, expect, it } from "vitest"; -import { parseTestPlan } from "../../../src/sdk/reporter/testplan.js"; +import { includedInTestPlan, parseTestPlan } from "../../../src/sdk/reporter/testplan.js"; +import type { TestPlanV1 } from "../../../src/sdk/types.js"; const originalEnv = process.env; const tmpDir = mkdtempSync(path.join(os.tmpdir(), "test-")); @@ -62,7 +63,7 @@ describe("parseTestPlan", () => { expect(res).toBeUndefined(); }); - it("should return undefiend if file don't exist", () => { + it("should return undefined if file don't exist", () => { process.env = { ...originalEnv, ALLURE_TESTPLAN_PATH: "some-strange-path.json", @@ -73,3 +74,40 @@ describe("parseTestPlan", () => { expect(res).toBeUndefined(); }); }); + +describe("includedInTestPlan", () => { + it("should match @allure.id tag", () => { + const exampleTestPlan: TestPlanV1 = { + version: "1.0", + tests: [ + { + id: 123, + selector: "some strange text", + }, + ], + }; + + const r1 = includedInTestPlan(exampleTestPlan, { tags: ["@allure.id=123"] }); + expect(r1).toBe(true); + + const r2 = includedInTestPlan(exampleTestPlan, { tags: ["@allure.id=122"] }); + expect(r2).toBe(false); + }); + it("should match by id", () => { + const exampleTestPlan: TestPlanV1 = { + version: "1.0", + tests: [ + { + id: 123, + selector: "some strange text", + }, + ], + }; + + const r1 = includedInTestPlan(exampleTestPlan, { id: "123", tags: ["@allure.id=133"] }); + expect(r1).toBe(true); + + const r2 = includedInTestPlan(exampleTestPlan, { id: "442", tags: ["@allure.id=123"] }); + expect(r2).toBe(false); + }); +}); diff --git a/packages/allure-js-commons/test/sdk/reporter/writer/FileSystemWriter.spec.ts b/packages/allure-js-commons/test/sdk/reporter/writer/FileSystemWriter.spec.ts index a2ba102f7..eaa055c88 100644 --- a/packages/allure-js-commons/test/sdk/reporter/writer/FileSystemWriter.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/writer/FileSystemWriter.spec.ts @@ -12,31 +12,28 @@ describe("FileSystemWriter", () => { it("should save attachment from path", () => { const tmp = mkdtempSync(path.join(os.tmpdir(), "foo-")); const allureResults = path.join(tmp, "allure-results"); - const config: Config = { writer: new FileSystemWriter({ resultsDir: allureResults, }), }; - const runtime = new ReporterRuntime(config); - const from = path.join(tmp, "test-attachment.txt"); const data = "test content"; writeFileSync(from, data, "utf8"); - runtime.startTest({ name: "test" }); - runtime.writeAttachmentFromPath("Attachment", from, { contentType: ContentType.TEXT }); - runtime.stopTest(); - runtime.writeTest(); + const testUuid = runtime.startTest({ name: "test" }); + + runtime.writeAttachment(testUuid, undefined, "Attachment", from, { contentType: ContentType.TEXT }); + runtime.stopTest(testUuid); + runtime.writeTest(testUuid); const resultFiles = readdirSync(allureResults); expect(resultFiles).toHaveLength(2); const attachmentResultPath = resultFiles.find((file) => file.includes("attachment"))!; - const actualContent = readFileSync(path.join(allureResults, attachmentResultPath)); expect(actualContent.toString("utf8")).toBe(data); @@ -50,14 +47,15 @@ describe("FileSystemWriter", () => { }), }; const runtime = new ReporterRuntime(config); + let testUuid = runtime.startTest({}); - runtime.startTest({}); - runtime.stopTest(); - runtime.writeTest(); + runtime.stopTest(testUuid); + runtime.writeTest(testUuid); rmSync(tmpReportPath, { recursive: true }); - runtime.startTest({}); - runtime.stopTest(); - runtime.writeTest(); + + testUuid = runtime.startTest({}); + runtime.stopTest(testUuid); + runtime.writeTest(testUuid); expect(existsSync(tmpReportPath)).toBe(true); }); diff --git a/packages/allure-mocha/src/AllureMochaReporter.ts b/packages/allure-mocha/src/AllureMochaReporter.ts index aab7c707a..56c3e65ab 100644 --- a/packages/allure-mocha/src/AllureMochaReporter.ts +++ b/packages/allure-mocha/src/AllureMochaReporter.ts @@ -1,6 +1,7 @@ import * as Mocha from "mocha"; -import type { Label } from "allure-js-commons"; +import type { AttachmentOptions, ContentType, Label } from "allure-js-commons"; import { Stage, Status } from "allure-js-commons"; +import type { Category, RuntimeMessage } from "allure-js-commons/sdk"; import { getStatusFromError } from "allure-js-commons/sdk"; import type { Config } from "allure-js-commons/sdk/reporter"; import { @@ -43,22 +44,27 @@ const { export class AllureMochaReporter extends Mocha.reporters.Base { private readonly runtime: ReporterRuntime; private readonly testplan?: TestPlanIndices; + private scopesStack: string[] = []; + private currentTest?: string; + private currentHook?: string; + private readonly isInWorker: boolean; - constructor(runner: Mocha.Runner, opts: Mocha.MochaOptions) { + constructor(runner: Mocha.Runner, opts: Mocha.MochaOptions, isInWorker: boolean = false) { super(runner, opts); const { resultsDir = "allure-results", writer, ...restOptions }: Config = opts.reporterOptions || {}; + this.isInWorker = isInWorker; this.runtime = new ReporterRuntime({ writer: writer || new FileSystemWriter({ resultsDir }), ...restOptions, }); this.testplan = createTestPlanIndices(); - const testRuntime = new MochaTestRuntime(this.runtime); + const testRuntime = new MochaTestRuntime(this.applyRuntimeMessages); setGlobalTestRuntime(testRuntime); - setLegacyApiRuntime(this.runtime); + setLegacyApiRuntime(this); if (opts.parallel) { opts.require = [...(opts.require ?? []), resolveParallelModeSetupFile()]; @@ -67,6 +73,49 @@ export class AllureMochaReporter extends Mocha.reporters.Base { } } + applyRuntimeMessages = (...message: RuntimeMessage[]) => { + const root = this.currentHook ?? this.currentTest; + if (root) { + this.runtime.applyRuntimeMessages(root, message); + } + }; + + /** + * @deprecated for removal. Use reporter config option instead. + */ + writeCategoriesDefinitions = (categories: Category[]) => { + this.runtime.categories = categories; + if (this.isInWorker) { + // done is not called in a worker; emit the file immediately + this.runtime.writeCategoriesDefinitions(); + } + }; + + /** + * @deprecated for removal. Use reporter config option instead. + */ + writeEnvironmentInfo = (environmentInfo: Record) => { + this.runtime.environmentInfo = environmentInfo; + if (this.isInWorker) { + // done is not called in a worker; emit the file immediately + this.runtime.writeEnvironmentInfo(); + } + }; + + /** + * @deprecated for removal. Use reporter config option instead. + */ + testAttachment = (name: string, content: Buffer | string, options: ContentType | string | AttachmentOptions) => { + const root = this.currentHook ?? this.currentTest; + if (!root) { + return; + } + const opts = typeof options === "string" ? { contentType: options } : options; + const encoding = opts.encoding ?? "utf8"; + const buffer = typeof content === "string" ? Buffer.from(content, encoding) : content; + this.runtime.writeAttachment(root, null, name, buffer, { ...opts, wrapInStep: false }); + }; + override done(failures: number, fn?: ((failures: number) => void) | undefined): void { this.runtime.writeEnvironmentInfo(); this.runtime.writeCategoriesDefinitions(); @@ -90,11 +139,15 @@ export class AllureMochaReporter extends Mocha.reporters.Base { if (!suite.parent && this.testplan) { applyTestPlan(this.testplan.idIndex, this.testplan.fullNameIndex, suite); } - this.runtime.startScope(); + const scopeUuid = this.runtime.startScope(); + this.scopesStack.push(scopeUuid); }; private onSuiteEnd = () => { - this.runtime.writeScope(); + const scopeUuid = this.scopesStack.pop(); + if (scopeUuid) { + this.runtime.writeScope(scopeUuid); + } }; private onTest = (test: Mocha.Test) => { @@ -109,7 +162,9 @@ export class AllureMochaReporter extends Mocha.reporters.Base { labels.push(packageLabelFromPath); } - this.runtime.startTest( + const scopeUuid = this.runtime.startScope(); + this.scopesStack.push(scopeUuid); + this.currentTest = this.runtime.startTest( { name: getAllureDisplayName(test), stage: Stage.RUNNING, @@ -117,18 +172,24 @@ export class AllureMochaReporter extends Mocha.reporters.Base { labels, testCaseId: getTestCaseId(test), }, - { dedicatedScope: true }, + this.scopesStack, ); }; private onPassed = () => { - this.runtime.updateTest((r) => { + if (!this.currentTest) { + return; + } + this.runtime.updateTest(this.currentTest, (r) => { r.status = Status.PASSED; }); }; private onFailed = (_: Mocha.Test, error: Error) => { - this.runtime.updateTest((r) => { + if (!this.currentTest) { + return; + } + this.runtime.updateTest(this.currentTest, (r) => { r.status = getStatusFromError(error); r.statusDetails = { message: error.message, @@ -139,10 +200,10 @@ export class AllureMochaReporter extends Mocha.reporters.Base { private onPending = (test: Mocha.Test) => { if (isIncludedInTestRun(test)) { - if (!this.runtime.hasTest()) { + if (!this.currentTest) { this.onTest(test); } - this.runtime.updateTest((r) => { + this.runtime.updateTest(this.currentTest!, (r) => { r.status = Status.SKIPPED; r.statusDetails = { message: "Test skipped", @@ -152,43 +213,58 @@ export class AllureMochaReporter extends Mocha.reporters.Base { }; private onTestEnd = (test: Mocha.Test) => { + if (!this.currentTest) { + return; + } if (isIncludedInTestRun(test)) { const defaultSuites = getSuitesOfMochaTest(test); - this.runtime.updateTest((t) => { + this.runtime.updateTest(this.currentTest, (t) => { ensureSuiteLabels(t, defaultSuites); t.stage = Stage.FINISHED; }); - this.runtime.stopTest(); - this.runtime.writeTest(); + this.runtime.stopTest(this.currentTest); + this.runtime.writeTest(this.currentTest); + this.currentTest = undefined; + // finish dedicated scope for test + const scopeUuid = this.scopesStack.pop(); + if (scopeUuid) { + this.runtime.writeScope(scopeUuid); + } } }; private onHookStart = (hook: Mocha.Hook) => { + const scopeUuid = this.scopesStack.length > 0 ? this.scopesStack[this.scopesStack.length - 1] : undefined; + if (!scopeUuid) { + return; + } const name = hook.originalTitle ?? ""; // eslint-disable-next-line @typescript-eslint/quotes if (name.startsWith('"before')) { - this.runtime.startFixture("before", { name }); + this.currentHook = this.runtime.startFixture(scopeUuid, "before", { name }); // eslint-disable-next-line @typescript-eslint/quotes } else if (name.startsWith('"after')) { - this.runtime.startFixture("after", { name }); + this.currentHook = this.runtime.startFixture(scopeUuid, "after", { name }); } }; private onHookEnd = (hook: Mocha.Hook) => { - if (this.runtime.hasFixture()) { - this.runtime.updateFixture((r) => { - const error: Error | undefined = hook.error(); - if (error) { - r.status = getStatusFromError(error); - r.statusDetails = { - message: error.message, - trace: error.stack, - }; - } else { - r.status = Status.PASSED; - } - }); - this.runtime.stopFixture(); + if (!this.currentHook) { + return; } + this.runtime.updateFixture(this.currentHook, (r) => { + const error: Error | undefined = hook.error(); + if (error) { + r.status = getStatusFromError(error); + r.statusDetails = { + message: error.message, + trace: error.stack, + }; + } else { + r.status = Status.PASSED; + } + }); + this.runtime.stopFixture(this.currentHook); + this.currentHook = undefined; }; } diff --git a/packages/allure-mocha/src/MochaTestRuntime.ts b/packages/allure-mocha/src/MochaTestRuntime.ts index e57ccc55e..8acb98cf0 100644 --- a/packages/allure-mocha/src/MochaTestRuntime.ts +++ b/packages/allure-mocha/src/MochaTestRuntime.ts @@ -1,14 +1,13 @@ import type { RuntimeMessage } from "allure-js-commons/sdk"; -import type { ReporterRuntime } from "allure-js-commons/sdk/reporter"; import { MessageTestRuntime } from "allure-js-commons/sdk/runtime"; export class MochaTestRuntime extends MessageTestRuntime { - constructor(private readonly reporterRuntime: ReporterRuntime) { + constructor(private readonly messageProcessor: (...messages: RuntimeMessage[]) => void) { super(); } async sendMessage(message: RuntimeMessage) { - this.reporterRuntime.applyRuntimeMessages([message]); - await Promise.resolve(); + this.messageProcessor(message); + return Promise.resolve(); } } diff --git a/packages/allure-mocha/src/legacy.ts b/packages/allure-mocha/src/legacy.ts index ae9b91a70..e1df4461a 100644 --- a/packages/allure-mocha/src/legacy.ts +++ b/packages/allure-mocha/src/legacy.ts @@ -1,6 +1,6 @@ import * as commons from "allure-js-commons"; import type { ContentType, ParameterOptions } from "allure-js-commons"; -import { Stage, Status } from "allure-js-commons"; +import { Status } from "allure-js-commons"; import type { Category } from "allure-js-commons/sdk"; import { getStatusFromError, isPromise } from "allure-js-commons/sdk"; import { serialize } from "allure-js-commons/sdk/reporter"; @@ -17,6 +17,7 @@ interface AttachmentOptions { fileExtension?: string; } +// noinspection JSDeprecatedSymbols /** * @deprecated please use api exported by "allure-js-commons" instead. */ @@ -98,13 +99,13 @@ class LegacyAllureApi { * @deprecated please use the `environmentInfo` config option instead. */ writeEnvironmentInfo = (info: Record) => { - getLegacyApiRuntime()?.writer.writeEnvironmentInfo(info); + getLegacyApiRuntime()?.writeEnvironmentInfo(info); }; /** * @deprecated please use the `categories` config option instead. */ writeCategoriesDefinitions = (categories: Category[]) => { - getLegacyApiRuntime()?.writer.writeCategoriesDefinitions(categories); + getLegacyApiRuntime()?.writeCategoriesDefinitions(categories); }; /** * @deprecated please use import { attachment } from "allure-js-commons" instead. @@ -118,26 +119,30 @@ class LegacyAllureApi { */ testAttachment = (name: string, content: Buffer | string, options: ContentType | string | AttachmentOptions) => { const runtime = getLegacyApiRuntime(); - const currentTest = runtime?.getCurrentTest(); - if (currentTest) { - const opts: AttachmentOptions = typeof options === "string" ? { contentType: options } : { ...options }; - runtime?.writeAttachment( - name, - Buffer.from(content), - { - ...opts, - }, - currentTest.uuid, - ); - } + runtime.testAttachment(name, content, options); }; /** * @deprecated please use import { step } from "allure-js-commons" instead. */ logStep = (name: string, status?: Status) => { - this.step(name, () => { - getLegacyApiRuntime()?.updateStep((s) => (s.status = status)); - }); + const runtime = getLegacyApiRuntime(); + const timestamp = Date.now(); + runtime?.applyRuntimeMessages( + { + type: "step_start", + data: { + name, + start: timestamp, + }, + }, + { + type: "step_stop", + data: { + status: status ?? Status.PASSED, + stop: timestamp, + }, + }, + ); }; // It's sync-first. That's why we can't simply reuse commons.step. @@ -146,15 +151,13 @@ class LegacyAllureApi { */ step = (name: string, body: (step: StepInterface) => T): T => { const runtime = getLegacyApiRuntime(); - runtime?.applyRuntimeMessages([ - { - type: "step_start", - data: { - name, - start: Date.now(), - }, + runtime?.applyRuntimeMessages({ + type: "step_start", + data: { + name, + start: Date.now(), }, - ]); + }); try { const result = body({ name: this.renameStep, @@ -181,54 +184,44 @@ class LegacyAllureApi { }; private renameStep = (name: string) => { - getLegacyApiRuntime()?.applyRuntimeMessages([ - { - type: "step_metadata", - data: { name }, - }, - ]); + getLegacyApiRuntime()?.applyRuntimeMessages({ + type: "step_metadata", + data: { name }, + }); }; private addStepParameter = (name: string, value: string) => { - getLegacyApiRuntime()?.applyRuntimeMessages([ - { - type: "step_metadata", - data: { - parameters: [{ name, value }], - }, + getLegacyApiRuntime()?.applyRuntimeMessages({ + type: "step_metadata", + data: { + parameters: [{ name, value }], }, - ]); + }); }; private stopStepSuccess = () => { - getLegacyApiRuntime()?.applyRuntimeMessages([ - { - type: "step_stop", - data: { - status: Status.PASSED, - stage: Stage.FINISHED, - stop: Date.now(), - }, + getLegacyApiRuntime()?.applyRuntimeMessages({ + type: "step_stop", + data: { + status: Status.PASSED, + stop: Date.now(), }, - ]); + }); }; private stopStepWithError = (error: unknown) => { const { message, stack } = error as Error; - getLegacyApiRuntime()?.applyRuntimeMessages([ - { - type: "step_stop", - data: { - status: getStatusFromError(error as Error), - stage: Stage.FINISHED, - stop: Date.now(), - statusDetails: { - message, - trace: stack, - }, + getLegacyApiRuntime()?.applyRuntimeMessages({ + type: "step_stop", + data: { + status: getStatusFromError(error as Error), + stop: Date.now(), + statusDetails: { + message, + trace: stack, }, }, - ]); + }); }; } diff --git a/packages/allure-mocha/src/legacyUtils.ts b/packages/allure-mocha/src/legacyUtils.ts index d3d0ad13a..60fe49f12 100644 --- a/packages/allure-mocha/src/legacyUtils.ts +++ b/packages/allure-mocha/src/legacyUtils.ts @@ -1,8 +1,8 @@ -import type { ReporterRuntime } from "allure-js-commons/sdk/reporter"; +import type { AllureMochaReporter } from "./AllureMochaReporter.js"; const ALLURE_TEST_RUNTIME_KEY = "__allure_mocha_legacy_runtime__"; -export const getLegacyApiRuntime = () => (globalThis as any)[ALLURE_TEST_RUNTIME_KEY] as ReporterRuntime; +export const getLegacyApiRuntime = () => (globalThis as any)[ALLURE_TEST_RUNTIME_KEY] as AllureMochaReporter; -export const setLegacyApiRuntime = (runtime: ReporterRuntime) => +export const setLegacyApiRuntime = (runtime: AllureMochaReporter) => ((globalThis as any)[ALLURE_TEST_RUNTIME_KEY] = runtime); diff --git a/packages/allure-mocha/src/setupAllureMochaParallel.ts b/packages/allure-mocha/src/setupAllureMochaParallel.ts index f7c651f1e..32b8f7257 100644 --- a/packages/allure-mocha/src/setupAllureMochaParallel.ts +++ b/packages/allure-mocha/src/setupAllureMochaParallel.ts @@ -8,6 +8,6 @@ const originalCreateListeners: (runner: Mocha.Runner) => Mocha.reporters.Base = ParallelBuffered.prototype.createListeners = function (runner: Mocha.Runner) { const result = originalCreateListeners.call(this, runner); - new AllureMochaReporter(runner, this.options as Mocha.MochaOptions); + new AllureMochaReporter(runner, this.options as Mocha.MochaOptions, true); return result; }; diff --git a/packages/allure-mocha/src/utils.ts b/packages/allure-mocha/src/utils.ts index 9082b5812..95a7c8b26 100644 --- a/packages/allure-mocha/src/utils.ts +++ b/packages/allure-mocha/src/utils.ts @@ -44,8 +44,8 @@ const createTestPlanSelectorIndex = (testplan: TestPlanV1) => createTestPlanInde const createTestPlanIdIndex = (testplan: TestPlanV1) => createTestPlanIndex((e) => e.id?.toString(), testplan); -const createTestPlanIndex = (keySelector: (entry: TestPlanV1Test) => T, testplan: TestPlanV1) => - new Set(testplan.tests.map((e) => keySelector(e)).filter((v) => v) as readonly T[]); +const createTestPlanIndex = (keySelector: (entry: TestPlanV1Test) => T | undefined, testplan: TestPlanV1): Set => + new Set(testplan.tests.map((e) => keySelector(e)).filter((v) => v)) as Set; export type TestPlanIndices = { fullNameIndex: ReadonlySet; diff --git a/packages/allure-mocha/test/spec/framework/categories.test.ts b/packages/allure-mocha/test/spec/framework/categories.test.ts new file mode 100644 index 000000000..82e5d3f5d --- /dev/null +++ b/packages/allure-mocha/test/spec/framework/categories.test.ts @@ -0,0 +1,20 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import type { AllureResults } from "allure-js-commons/sdk"; +import { runMochaInlineTest } from "../../utils.js"; + +describe("categories", () => { + let results: AllureResults; + beforeAll(async () => { + results = await runMochaInlineTest( + { + categories: [{ name: "category 1", status: "failed" }], + }, + ["plain-mocha", "testInSuite"], + ); + }); + + it("should store categories", () => { + const { categories } = results; + expect(categories).toEqual([{ name: "category 1", status: "failed" }]); + }); +}); diff --git a/packages/allure-mocha/test/spec/framework/envInfo.test.ts b/packages/allure-mocha/test/spec/framework/envInfo.test.ts new file mode 100644 index 000000000..c6662c8ff --- /dev/null +++ b/packages/allure-mocha/test/spec/framework/envInfo.test.ts @@ -0,0 +1,26 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import type { AllureResults } from "allure-js-commons/sdk"; +import { runMochaInlineTest } from "../../utils.js"; + +describe("environment info", () => { + let results: AllureResults; + beforeAll(async () => { + results = await runMochaInlineTest( + { + environmentInfo: { + a: "b", + c: "d", + }, + }, + ["plain-mocha", "testInSuite"], + ); + }); + + it("should store environment info", () => { + const { envInfo } = results; + expect(envInfo).toEqual({ + a: "b", + c: "d", + }); + }); +}); diff --git a/packages/allure-playwright/src/index.ts b/packages/allure-playwright/src/index.ts index a7fa1403e..c90bb71e1 100644 --- a/packages/allure-playwright/src/index.ts +++ b/packages/allure-playwright/src/index.ts @@ -73,6 +73,7 @@ export class AllureReporter implements ReporterV2 { private processedDiffs: string[] = []; private readonly startedTestCasesTitlesCache: string[] = []; private readonly allureResultsUuids: Map = new Map(); + private readonly attachmentSteps: Map = new Map(); constructor(config: AllurePlaywrightReporterConfig) { this.options = { suiteTitle: true, detail: true, ...config }; @@ -100,7 +101,8 @@ export class AllureReporter implements ReporterV2 { const cliArgs: string[] = []; testsWithSelectors.forEach((test) => { - if (!/#/.test(test.selector)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + if (!/#/.test(test.selector!)) { v2ReporterTests.push(test); return; } @@ -111,7 +113,8 @@ export class AllureReporter implements ReporterV2 { if (v2ReporterTests.length) { // we need to cut off column because playwright works only with line number const v2SelectorsArgs = v2ReporterTests - .map((test) => test.selector.replace(/:\d+$/, "")) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + .map((test) => test.selector!.replace(/:\d+$/, "")) .map((selector) => escapeRegExp(selector)); cliArgs.push(...v2SelectorsArgs); @@ -120,7 +123,8 @@ export class AllureReporter implements ReporterV2 { if (v1ReporterTests.length) { const v1SelectorsArgs = v1ReporterTests // we can filter tests only by absolute path, so we need to cut off test name - .map((test) => test.selector.split("#")[0]) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + .map((test) => test.selector!.split("#")[0]) .map((selector) => escapeRegExp(selector)); cliArgs.push(...v1SelectorsArgs); @@ -192,24 +196,23 @@ export class AllureReporter implements ReporterV2 { } onStepBegin(test: TestCase, _result: PlaywrightTestResult, step: TestStep): void { - if (!this.options.detail && step.category !== "test.step") { - return; - } + const testUuid = this.allureResultsUuids.get(test.id)!; - // ignore attach steps since attachments are already in the report if (step.category === "attach") { + const currentStep = this.allureRuntime?.currentStep(testUuid); + this.attachmentSteps.set(testUuid, [...(this.attachmentSteps.get(testUuid) ?? []), currentStep]); return; } - const testUuid = this.allureResultsUuids.get(test.id)!; + // TODO fix the details disable, e.g. only ignore pw:api steps + if (!this.options.detail && step.category !== "test.step") { + return; + } - this.allureRuntime!.startStep( - { - name: step.title.substring(0, stepAttachPrefixLength), - start: step.startTime.getTime(), - }, - testUuid, - ); + this.allureRuntime!.startStep(testUuid, undefined, { + name: step.title.substring(0, stepAttachPrefixLength), + start: step.startTime.getTime(), + }); } onStepEnd(test: TestCase, _result: PlaywrightTestResult, step: TestStep): void { @@ -224,15 +227,20 @@ export class AllureReporter implements ReporterV2 { const testUuid = this.allureResultsUuids.get(test.id)!; - this.allureRuntime!.updateStep((stepResult) => { + const currentStep = this.allureRuntime!.currentStep(testUuid); + if (!currentStep) { + return; + } + + this.allureRuntime!.updateStep(currentStep, (stepResult) => { stepResult.status = step.error ? Status.FAILED : Status.PASSED; stepResult.stage = Stage.FINISHED; if (step.error) { stepResult.statusDetails = { ...getMessageAndTraceFromError(step.error) }; } - }, testUuid); - this.allureRuntime!.stopStep({ uuid: testUuid }); + }); + this.allureRuntime!.stopStep(currentStep, step.startTime.getTime() + step.duration); } async onTestEnd(test: TestCase, result: PlaywrightTestResult) { @@ -245,7 +253,7 @@ export class AllureReporter implements ReporterV2 { // only apply default suites if not set by user const [, projectSuiteTitle, fileSuiteTitle, ...suiteTitles] = test.parent.titlePath(); - this.allureRuntime!.updateTest((testResult) => { + this.allureRuntime!.updateTest(testUuid, (testResult) => { testResult.labels.push({ name: LabelName.HOST, value: this.hostname }); testResult.labels.push({ name: LabelName.THREAD, value: thread }); @@ -267,37 +275,42 @@ export class AllureReporter implements ReporterV2 { testResult.status = statusToAllureStats(result.status, test.expectedStatus); testResult.stage = Stage.FINISHED; - }, testUuid); + }); - for (const attachment of result.attachments) { - await this.processAttachment(test.id, attachment); + const attachmentSteps = this.attachmentSteps.get(testUuid) ?? []; + for (let i = 0; i < result.attachments.length; i++) { + const attachment = result.attachments[i]; + const attachmentStep = attachmentSteps.length > i ? attachmentSteps[i] : undefined; + await this.processAttachment(testUuid, attachmentStep, attachment); } if (result.stdout.length > 0) { this.allureRuntime!.writeAttachment( + testUuid, + undefined, "stdout", Buffer.from(stripAnsi(result.stdout.join("")), "utf-8"), { contentType: ContentType.TEXT, }, - testUuid, ); } if (result.stderr.length > 0) { this.allureRuntime!.writeAttachment( + testUuid, + undefined, "stderr", Buffer.from(stripAnsi(result.stderr.join("")), "utf-8"), { contentType: ContentType.TEXT, }, - testUuid, ); } // FIXME: temp logic for labels override, we need it here to keep the reporter compatible with v2 API // in next iterations we need to implement the logic for every javascript integration - this.allureRuntime!.updateTest((testResult) => { + this.allureRuntime!.updateTest(testUuid, (testResult) => { const mappedLabels = testResult.labels.reduce>((acc, label) => { if (!acc[label.name]) { acc[label.name] = []; @@ -322,9 +335,9 @@ export class AllureReporter implements ReporterV2 { }); testResult.labels = newLabels; - }, testUuid); + }); - this.allureRuntime!.stopTest({ uuid: testUuid }); + this.allureRuntime!.stopTest(testUuid, result.startTime.getTime() + result.duration); this.allureRuntime!.writeTest(testUuid); } @@ -365,7 +378,8 @@ export class AllureReporter implements ReporterV2 { } private async processAttachment( - testId: string, + testUuid: string, + attachmentStepUuid: string | undefined, attachment: { name: string; contentType: string; @@ -373,8 +387,6 @@ export class AllureReporter implements ReporterV2 { body?: Buffer; }, ) { - const testUuid = this.allureResultsUuids.get(testId)!; - if (!attachment.body && !attachment.path) { return; } @@ -388,31 +400,29 @@ export class AllureReporter implements ReporterV2 { if (allureRuntimeMessage) { const message = JSON.parse(attachment.body!.toString()) as RuntimeMessage; - // TODO: make possible to pass single message and list of them - this.allureRuntime!.applyRuntimeMessages([message], { testUuid }); + // TODO fix step metadata messages + this.allureRuntime!.applyRuntimeMessages(testUuid, [message]); return; } + const parentUuid = this.allureRuntime!.startStep(testUuid, attachmentStepUuid, { name: attachment.name }); + // only stop if step is created. Step may not be created only if test with specified uuid doesn't exists. + // usually, missing test by uuid means we should completely skip result processing; + // the later operations are safe and will only produce console warnings + if (parentUuid) { + this.allureRuntime!.stopStep(parentUuid, undefined); + } if (attachment.body) { - this.allureRuntime!.writeAttachment( - attachment.name, - attachment.body, - { - contentType: attachment.contentType, - }, - testUuid, - ); + this.allureRuntime!.writeAttachment(testUuid, parentUuid, attachment.name, attachment.body, { + contentType: attachment.contentType, + }); } else if (!existsSync(attachment.path!)) { return; } else { - this.allureRuntime!.writeAttachmentFromPath( - attachment.name, - attachment.path!, - { - contentType: attachment.contentType, - }, - testUuid, - ); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + this.allureRuntime!.writeAttachment(testUuid, parentUuid, attachment.name, attachment.path!, { + contentType: attachment.contentType, + }); } if (!attachment.name.match(diffEndRegexp)) { @@ -431,6 +441,8 @@ export class AllureReporter implements ReporterV2 { const diffName = attachment.name.replace(diffEndRegexp, ""); this.allureRuntime!.writeAttachment( + testUuid, + undefined, diffName, Buffer.from( JSON.stringify({ @@ -445,7 +457,6 @@ export class AllureReporter implements ReporterV2 { contentType: ContentType.IMAGEDIFF, fileExtension: ".imagediff", }, - testUuid, ); this.processedDiffs.push(pathWithoutEnd); diff --git a/packages/allure-playwright/src/model.ts b/packages/allure-playwright/src/model.ts index c616a4400..2aeacedb0 100644 --- a/packages/allure-playwright/src/model.ts +++ b/packages/allure-playwright/src/model.ts @@ -2,7 +2,6 @@ import type { Config } from "allure-js-commons/sdk/reporter"; export interface AllurePlaywrightReporterConfig extends Omit { detail?: boolean; - outputFolder?: string; suiteTitle?: boolean; testMode?: boolean; } diff --git a/packages/allure-playwright/src/runtime.ts b/packages/allure-playwright/src/runtime.ts index 71dd99e74..d888e315f 100644 --- a/packages/allure-playwright/src/runtime.ts +++ b/packages/allure-playwright/src/runtime.ts @@ -1,4 +1,5 @@ import test from "@playwright/test"; +import type { AttachmentOptions } from "allure-js-commons"; import type { RuntimeMessage } from "allure-js-commons/sdk"; import { ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE } from "allure-js-commons/sdk/reporter"; import { MessageTestRuntime } from "allure-js-commons/sdk/runtime"; @@ -7,9 +8,21 @@ export class AllurePlaywrightTestRuntime extends MessageTestRuntime { constructor() { super(); } + + async step(name: string, body: () => T | PromiseLike) { + return await test.step(name, () => Promise.resolve(body())); + } + + async attachment(name: string, content: Buffer | string, options: AttachmentOptions) { + await test.info().attach(name, { body: content, contentType: options.contentType }); + } + + async attachmentFromPath(name: string, path: string, options: AttachmentOptions) { + await test.info().attach(name, { path, contentType: options.contentType }); + } + async sendMessage(message: RuntimeMessage) { - // @ts-ignore - await test.info().attach("allure-metadata.json", { + await test.info().attach(`Allure Metadata (${message.type})`, { contentType: ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE, body: Buffer.from(JSON.stringify(message), "utf8"), }); diff --git a/packages/allure-playwright/src/testplan.ts b/packages/allure-playwright/src/testplan.ts index 2bae78be4..39d4b3b9d 100644 --- a/packages/allure-playwright/src/testplan.ts +++ b/packages/allure-playwright/src/testplan.ts @@ -6,8 +6,10 @@ export const testPlanFilter = () => { return undefined; } - return testPlan.tests.map((testInfo) => { - const pattern = testInfo.selector.replace("#", " "); - return new RegExp(`\\s${escapeRegExp(pattern)}$`); - }); + return testPlan.tests + .flatMap((testInfo) => (testInfo.selector ? [testInfo.selector] : [])) + .map((selector) => { + const pattern = selector.replace("#", " "); + return new RegExp(`\\s${escapeRegExp(pattern)}$`); + }); }; diff --git a/packages/allure-playwright/test/spec/attachments.spec.ts b/packages/allure-playwright/test/spec/attachments.spec.ts index 51ea745a1..1ed0262c5 100644 --- a/packages/allure-playwright/test/spec/attachments.spec.ts +++ b/packages/allure-playwright/test/spec/attachments.spec.ts @@ -4,7 +4,7 @@ import { expect, it } from "vitest"; import { runPlaywrightInlineTest } from "../utils.js"; it("doesn't not throw on missing attachment", async () => { - const { tests, attachments } = await runPlaywrightInlineTest({ + const { tests } = await runPlaywrightInlineTest({ "sample.test.js": ` import test from '@playwright/test'; @@ -24,13 +24,17 @@ it("doesn't not throw on missing attachment", async () => { `, }); - expect(tests[0].attachments).toEqual([ + expect(tests[0].steps).toContainEqual( expect.objectContaining({ name: "buffer-attachment", - type: "text/plain", + attachments: expect.arrayContaining([ + expect.objectContaining({ + name: "buffer-attachment", + type: "text/plain", + }), + ]), }), - ]); - expect(attachments[tests[0].attachments[0].source]).toEqual(Buffer.from("foo").toString("base64")); + ); }); it("adds snapshots correctly and provide a screenshot diff", async () => { diff --git a/packages/allure-playwright/test/spec/runtime/legacy/attachments.spec.ts b/packages/allure-playwright/test/spec/runtime/legacy/attachments.spec.ts index 665c996df..c4d9852a2 100644 --- a/packages/allure-playwright/test/spec/runtime/legacy/attachments.spec.ts +++ b/packages/allure-playwright/test/spec/runtime/legacy/attachments.spec.ts @@ -205,8 +205,8 @@ it("doesn't not report detail steps for attachments", async () => { }), ); - const [attachment1] = tests[0].steps[2].steps[0].steps[0].attachments; - const [attachment2] = tests[0].steps[3].steps[1].steps[0].attachments; + const [attachment1] = tests[0].steps[1].steps[0].steps[0].attachments; + const [attachment2] = tests[0].steps[2].steps[1].steps[0].attachments; expect(attachments).toHaveProperty(attachment1.source); expect(attachments).toHaveProperty(attachment2.source); diff --git a/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts b/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts index 006cf6a16..d066e94fa 100644 --- a/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts +++ b/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts @@ -38,7 +38,7 @@ it("handles single lambda step with attachment", async () => { expect(tests).toHaveLength(1); expect(tests[0].steps).toHaveLength(3); - const [step] = tests[0].steps[2].steps; + const [step] = tests[0].steps[1].steps; expect(step.name).toBe("foo.txt"); const [attachment] = step.attachments; @@ -66,19 +66,19 @@ it("handles nested lambda steps", async () => { expect(tests).toHaveLength(1); expect(tests[0].steps).toHaveLength(3); - expect(tests[0].steps[2]).toMatchObject({ + expect(tests[0].steps[1]).toMatchObject({ name: "step 1", status: Status.PASSED, stage: Stage.FINISHED, }); - expect(tests[0].steps[2].steps).toHaveLength(1); - expect(tests[0].steps[2].steps[0]).toMatchObject({ + expect(tests[0].steps[1].steps).toHaveLength(1); + expect(tests[0].steps[1].steps[0]).toMatchObject({ name: "step 2", status: Status.PASSED, stage: Stage.FINISHED, }); - expect(tests[0].steps[2].steps[0].steps).toHaveLength(1); - expect(tests[0].steps[2].steps[0].steps[0]).toMatchObject({ + expect(tests[0].steps[1].steps[0].steps).toHaveLength(1); + expect(tests[0].steps[1].steps[0].steps[0]).toMatchObject({ name: "step 3", status: Status.PASSED, stage: Stage.FINISHED, diff --git a/packages/allure-playwright/test/spec/runtime/modern/attachments.spec.ts b/packages/allure-playwright/test/spec/runtime/modern/attachments.spec.ts index 43ba357f2..002b343f3 100644 --- a/packages/allure-playwright/test/spec/runtime/modern/attachments.spec.ts +++ b/packages/allure-playwright/test/spec/runtime/modern/attachments.spec.ts @@ -205,8 +205,8 @@ it("doesn't not report detail steps for attachments", async () => { }), ); - const [attachment1] = tests[0].steps[2].steps[0].steps[0].attachments; - const [attachment2] = tests[0].steps[3].steps[1].steps[0].attachments; + const [attachment1] = tests[0].steps[1].steps[0].steps[0].attachments; + const [attachment2] = tests[0].steps[2].steps[1].steps[0].attachments; expect(attachments).toHaveProperty(attachment1.source); expect(attachments).toHaveProperty(attachment2.source); diff --git a/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts b/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts index bb0973013..016517640 100644 --- a/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts +++ b/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts @@ -40,7 +40,7 @@ it("handles single lambda step with attachment", async () => { expect(tests).toHaveLength(1); expect(tests[0].steps).toHaveLength(3); - const [step] = tests[0].steps[2].steps; + const [step] = tests[0].steps[1].steps; expect(step.name).toBe("foo.txt"); const [attachment] = step.attachments; @@ -69,19 +69,19 @@ it("handles nested lambda steps", async () => { expect(tests).toHaveLength(1); expect(tests[0].steps).toHaveLength(3); - expect(tests[0].steps[2]).toMatchObject({ + expect(tests[0].steps[1]).toMatchObject({ name: "step 1", status: Status.PASSED, stage: Stage.FINISHED, }); - expect(tests[0].steps[2].steps).toHaveLength(1); - expect(tests[0].steps[2].steps[0]).toMatchObject({ + expect(tests[0].steps[1].steps).toHaveLength(1); + expect(tests[0].steps[1].steps[0]).toMatchObject({ name: "step 2", status: Status.PASSED, stage: Stage.FINISHED, }); - expect(tests[0].steps[2].steps[0].steps).toHaveLength(1); - expect(tests[0].steps[2].steps[0].steps[0]).toMatchObject({ + expect(tests[0].steps[1].steps[0].steps).toHaveLength(1); + expect(tests[0].steps[1].steps[0].steps[0]).toMatchObject({ name: "step 3", status: Status.PASSED, stage: Stage.FINISHED, diff --git a/packages/allure-playwright/vitest.config.ts b/packages/allure-playwright/vitest.config.ts index 749d5ea52..8beee3441 100644 --- a/packages/allure-playwright/vitest.config.ts +++ b/packages/allure-playwright/vitest.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ fileParallelism: false, testTimeout: 25000, setupFiles: ["./vitest-setup.ts"], - reporters: ["default", ["allure-vitest/reporter", { resultsDir: "./out/allure-results" }]], + reporters: ["verbose", ["allure-vitest/reporter", { resultsDir: "./out/allure-results" }]], typecheck: { enabled: true, tsconfig: "./tsconfig.test.json", diff --git a/packages/allure-vitest/src/reporter.ts b/packages/allure-vitest/src/reporter.ts index 635047503..e823cee6f 100644 --- a/packages/allure-vitest/src/reporter.ts +++ b/packages/allure-vitest/src/reporter.ts @@ -88,10 +88,10 @@ export default class AllureVitestReporter implements Reporter { const testFullname = getTestFullName(task, cwd()); const testUuid = this.allureReporterRuntime!.startTest({ name: testDisplayName, - start: task.result!.startTime, + start: task.result?.startTime ?? Date.now(), }); - this.allureReporterRuntime!.updateTest((result) => { + this.allureReporterRuntime!.updateTest(testUuid, (result) => { result.fullName = testFullname; result.labels.push({ name: LabelName.FRAMEWORK, @@ -114,7 +114,7 @@ export default class AllureVitestReporter implements Reporter { }); } - this.allureReporterRuntime!.applyRuntimeMessages(allureRuntimeMessages, { testUuid }); + this.allureReporterRuntime!.applyRuntimeMessages(testUuid, allureRuntimeMessages); switch (task.result?.state) { case "fail": { @@ -140,11 +140,9 @@ export default class AllureVitestReporter implements Reporter { break; } } - }, testUuid); - this.allureReporterRuntime!.stopTest({ - uuid: testUuid, - stop: (task.result?.startTime || 0) + (task.result?.duration || 0), }); + const stop = task.result?.startTime ? task.result.startTime + (task.result.duration ?? 0) : undefined; + this.allureReporterRuntime!.stopTest(testUuid, stop); this.allureReporterRuntime!.writeTest(testUuid); } } diff --git a/packages/allure-vitest/test/utils.ts b/packages/allure-vitest/test/utils.ts index 119dd7409..ce9aab790 100644 --- a/packages/allure-vitest/test/utils.ts +++ b/packages/allure-vitest/test/utils.ts @@ -32,7 +32,7 @@ export const runVitestInlineTest = async ( test: { setupFiles: ["allure-vitest/setup"], reporters: [ - "default", + "verbose", new AllureReporter({ testMode: true, links: { diff --git a/packages/allure-vitest/vitest.config.ts b/packages/allure-vitest/vitest.config.ts index 0632c8211..9e7a44f64 100644 --- a/packages/allure-vitest/vitest.config.ts +++ b/packages/allure-vitest/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ dir: "./test/spec", testTimeout: 20000, setupFiles: ["allure-vitest/setup"], - reporters: ["default", ["allure-vitest/reporter", { resultsDir: "./out/allure-results" }]], + reporters: ["basic", ["allure-vitest/reporter", { resultsDir: "./out/allure-results" }]], typecheck: { enabled: true, tsconfig: "./tsconfig.test.json", diff --git a/packages/newman-reporter-allure/src/index.ts b/packages/newman-reporter-allure/src/index.ts index ef5e1446c..cf3ae25a3 100644 --- a/packages/newman-reporter-allure/src/index.ts +++ b/packages/newman-reporter-allure/src/index.ts @@ -13,6 +13,8 @@ class AllureReporter { runningItems: RunningItem[] = []; currentCollection: CollectionDefinition; pmItemsByAllureUuid: Map = new Map(); + currentTest?: string; + currentScope?: string; constructor( emitter: EventEmitter, @@ -36,65 +38,6 @@ class AllureReporter { this.registerEvents(emitter); } - pathToItem(item: Item): string[] { - if (!item || !(typeof item.parent === "function") || !(typeof item.forEachParent === "function")) { - return []; - } - - const chain: string[] = []; - - if (this.currentCollection.name && this.allureConfig.collectionAsParentSuite) { - chain.push(this.currentCollection.name); - } - - item.forEachParent((parent) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - chain.unshift(parent.name || parent.id); - }); - - return chain; - } - - getFullName(item: Item): string { - const chain = this.pathToItem(item); - - return `${chain.join("/")}#${item.name}`; - } - - attachString(name: string, value: string | string[]) { - const stringToAttach = Array.isArray(value) ? value.join("\n") : value; - - if (!stringToAttach) { - return; - } - - const content = Buffer.from(stringToAttach, "utf-8"); - - this.allureRuntime.writeAttachment(name, content, { - contentType: ContentType.TEXT, - }); - } - - headerListToJsonBuffer(headers: HeaderList) { - const ret: { [k: string]: any } = {}; - - headers.all().forEach((h) => { - ret[h.key] = h.value; - }); - - return Buffer.from(JSON.stringify(ret, null, 4), "utf-8"); - } - - escape(val: string) { - return ( - val - .replace("\n", "") - .replace("\r", "") - // eslint-disable-next-line @typescript-eslint/quotes - .replace('"', '"') - ); - } - registerEvents(emitter: EventEmitter) { emitter.on("start", this.onStart.bind(this)); emitter.on("beforeItem", this.onBeforeItem.bind(this)); @@ -108,7 +51,7 @@ class AllureReporter { } onStart() { - this.allureRuntime.startScope(); + this.currentScope = this.allureRuntime.startScope(); } onPrerequest( @@ -117,13 +60,11 @@ class AllureReporter { executions: Event[]; }, ) { - const currentAllureTest = this.allureRuntime.getCurrentTest(); - - if (!currentAllureTest) { + if (!this.currentTest) { return; } - const currentPmItem = this.pmItemsByAllureUuid.get(currentAllureTest.uuid); + const currentPmItem = this.pmItemsByAllureUuid.get(this.currentTest); if (!currentPmItem) { return; @@ -141,12 +82,13 @@ class AllureReporter { failedAssertions: [], consoleLogs: [], }; + const itemGroup = args.item.parent(); const item = args.item; - const fullName = this.getFullName(item); - const testPath = this.pathToItem(item); + const fullName = this.#getFullName(item); + const testPath = this.#pathToItem(item); const { labels } = extractMeta(args.item.events); - const currentTestUuid = this.allureRuntime.startTest({ + this.currentTest = this.allureRuntime.startTest({ name: args.item.name, fullName, stage: Stage.RUNNING, @@ -158,7 +100,7 @@ class AllureReporter { ], }); - this.allureRuntime.updateTest((test) => { + this.allureRuntime.updateTest(this.currentTest, (test) => { const [parentSuite, suite, ...subSuites] = testPath; if (parentSuite) { @@ -173,7 +115,7 @@ class AllureReporter { test.labels.push({ name: LabelName.SUB_SUITE, value: subSuites.join(" > ") }); } }); - this.pmItemsByAllureUuid.set(currentTestUuid, pmItem); + this.pmItemsByAllureUuid.set(this.currentTest, pmItem); if (itemGroup && this.currentCollection !== itemGroup) { this.currentCollection = itemGroup; @@ -186,13 +128,11 @@ class AllureReporter { item: Item; }, ) { - const currentAllureTest = this.allureRuntime.getCurrentTest(); - - if (!currentAllureTest) { + if (!this.currentTest) { return; } - const currentPmItem = this.pmItemsByAllureUuid.get(currentAllureTest.uuid); + const currentPmItem = this.pmItemsByAllureUuid.get(this.currentTest); if (!currentPmItem) { return; @@ -207,40 +147,58 @@ class AllureReporter { const requestError = currentPmItem.requestError; if (currentPmItem.prerequest) { - this.attachString("PreRequest", currentPmItem.prerequest); + this.#attachString("PreRequest", currentPmItem.prerequest); } if (currentPmItem.testScript) { - this.attachString("TestScript", currentPmItem.testScript); + this.#attachString("TestScript", currentPmItem.testScript); } if (currentPmItem.consoleLogs.length) { - this.attachString("ConsoleLogs", currentPmItem.consoleLogs); + this.#attachString("ConsoleLogs", currentPmItem.consoleLogs); } if (requestData?.headers && requestData?.headers?.count() > 0) { - this.allureRuntime.writeAttachment("Request Headers", this.headerListToJsonBuffer(requestData.headers), { - contentType: ContentType.JSON, - }); + this.allureRuntime.writeAttachment( + this.currentTest, + undefined, + "Request Headers", + this.#headerListToJsonBuffer(requestData.headers), + { + contentType: ContentType.JSON, + }, + ); } if (requestData?.body?.mode === "raw" && requestData.body.raw) { - this.attachString("Request Body", requestData.body.raw); + this.#attachString("Request Body", requestData.body.raw); } if (response?.headers && response?.headers?.count() > 0) { - this.allureRuntime.writeAttachment("Response Headers", this.headerListToJsonBuffer(response.headers), { - contentType: ContentType.JSON, - }); + this.allureRuntime.writeAttachment( + this.currentTest, + undefined, + "Response Headers", + this.#headerListToJsonBuffer(response.headers), + { + contentType: ContentType.JSON, + }, + ); } if (response?.body) { - this.allureRuntime.writeAttachment("Response Body", Buffer.from(response.body, "utf-8"), { - contentType: ContentType.TEXT, - }); + this.allureRuntime.writeAttachment( + this.currentTest, + undefined, + "Response Body", + Buffer.from(response.body, "utf-8"), + { + contentType: ContentType.TEXT, + }, + ); } - this.allureRuntime.updateTest((test) => { + this.allureRuntime.updateTest(this.currentTest, (test) => { if (requestDataURL) { test.parameters.push({ name: "Request", @@ -265,24 +223,20 @@ class AllureReporter { }); if (response && failedAssertions?.length) { - if (currentAllureTest.status === Status.FAILED || currentAllureTest.status === Status.BROKEN) { - return; - } + const details = this.#escape(`Response code: ${response.code}, status: ${response.status}`); - const details = this.escape(`Response code: ${response.code}, status: ${response.status}`); - - this.allureRuntime.updateTest((test) => { + this.allureRuntime.updateTest(this.currentTest, (test) => { test.status = Status.FAILED; test.stage = Stage.FINISHED; test.statusDetails = { - message: this.escape(failedAssertions.join(", ")), + message: this.#escape(failedAssertions.join(", ")), trace: details, }; }); } else if (requestError) { - const errorMsg = this.escape(requestError); + const errorMsg = this.#escape(requestError); - this.allureRuntime.updateTest((test) => { + this.allureRuntime.updateTest(this.currentTest, (test) => { test.status = Status.BROKEN; test.stage = Stage.FINISHED; test.statusDetails = { @@ -290,24 +244,22 @@ class AllureReporter { }; }); } else { - this.allureRuntime.updateTest((test) => { + this.allureRuntime.updateTest(this.currentTest, (test) => { test.status = Status.PASSED; test.stage = Stage.FINISHED; }); } - this.allureRuntime.stopTest(); - this.allureRuntime.writeTest(); + this.allureRuntime.stopTest(this.currentTest); + this.allureRuntime.writeTest(this.currentTest); } onTest(err: any, args: { executions: Event[] }) { - const currentAllureTest = this.allureRuntime.getCurrentTest(); - - if (!currentAllureTest) { + if (!this.currentTest) { return; } - const currentPmItem = this.pmItemsByAllureUuid.get(currentAllureTest.uuid); + const currentPmItem = this.pmItemsByAllureUuid.get(this.currentTest); if (!currentPmItem) { return; @@ -331,7 +283,7 @@ class AllureReporter { const errName: string = testArgs.error.name; const errMsg: string = testArgs.error.message; - this.allureRuntime.startStep({ + const stepUuid = this.allureRuntime.startStep(this.currentTest, undefined, { name: errName, status: Status.FAILED, stage: Stage.FINISHED, @@ -340,19 +292,22 @@ class AllureReporter { }, }); + if (!stepUuid) { + // no such test running, ignore reporting + return; + } + currentPmItem.failedAssertions.push(errName); - this.allureRuntime.stopStep(); + this.allureRuntime.stopStep(stepUuid); } onConsole(err: any, args: ConsoleEvent) { - const currentAllureTest = this.allureRuntime.getCurrentTest(); - - if (!currentAllureTest) { + if (!this.currentTest) { return; } - const currentPmItem = this.pmItemsByAllureUuid.get(currentAllureTest.uuid); + const currentPmItem = this.pmItemsByAllureUuid.get(this.currentTest); if (!currentPmItem || err) { return; @@ -370,13 +325,11 @@ class AllureReporter { response: Response; }, ) { - const currentAllureTest = this.allureRuntime.getCurrentTest(); - - if (!currentAllureTest) { + if (!this.currentTest) { return; } - const currentPmItem = this.pmItemsByAllureUuid.get(currentAllureTest.uuid); + const currentPmItem = this.pmItemsByAllureUuid.get(this.currentTest); if (!currentPmItem) { return; @@ -413,22 +366,25 @@ class AllureReporter { } onAssertion(err: any, args: NewmanRunExecutionAssertion) { - const currentAllureTest = this.allureRuntime.getCurrentTest(); - - if (!currentAllureTest) { + if (!this.currentTest) { return; } - const currentPmItem = this.pmItemsByAllureUuid.get(currentAllureTest.uuid); + const currentPmItem = this.pmItemsByAllureUuid.get(this.currentTest); if (!currentPmItem) { return; } - this.allureRuntime.startStep({ + const stepUuid = this.allureRuntime.startStep(this.currentTest, undefined, { name: args.assertion, }); - this.allureRuntime.updateStep((step) => { + if (!stepUuid) { + // no such test running, ignore reporting + return; + } + + this.allureRuntime.updateStep(stepUuid, (step) => { if (err && currentPmItem) { currentPmItem.passed = false; currentPmItem.failedAssertions.push(args.assertion); @@ -445,11 +401,73 @@ class AllureReporter { step.stage = Stage.FINISHED; }); - this.allureRuntime.stopStep(); + this.allureRuntime.stopStep(stepUuid); } onDone() { - this.allureRuntime.writeScope(); + if (this.currentScope) { + this.allureRuntime.writeScope(this.currentScope); + this.currentScope = undefined; + } + } + + #pathToItem(item: Item): string[] { + if (!item || !(typeof item.parent === "function") || !(typeof item.forEachParent === "function")) { + return []; + } + + const chain: string[] = []; + + if (this.currentCollection.name && this.allureConfig.collectionAsParentSuite) { + chain.push(this.currentCollection.name); + } + + item.forEachParent((parent) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + chain.unshift(parent.name || parent.id); + }); + + return chain; + } + + #getFullName(item: Item): string { + const chain = this.#pathToItem(item); + + return `${chain.join("/")}#${item.name}`; + } + + #attachString(name: string, value: string | string[]) { + const stringToAttach = Array.isArray(value) ? value.join("\n") : value; + + if (!stringToAttach) { + return; + } + + const content = Buffer.from(stringToAttach, "utf-8"); + + this.allureRuntime.writeAttachment(this.currentTest!, undefined, name, content, { + contentType: ContentType.TEXT, + }); + } + + #headerListToJsonBuffer(headers: HeaderList) { + const ret: { [k: string]: any } = {}; + + headers.all().forEach((h) => { + ret[h.key] = h.value; + }); + + return Buffer.from(JSON.stringify(ret, null, 4), "utf-8"); + } + + #escape(val: string) { + return ( + val + .replace("\n", "") + .replace("\r", "") + // eslint-disable-next-line @typescript-eslint/quotes + .replace('"', '"') + ); } } diff --git a/yarn.lock b/yarn.lock index c38f9762e..fff7ba362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1891,9 +1891,9 @@ __metadata: languageName: node linkType: hard -"@cucumber/cucumber@npm:^10.4.0": - version: 10.4.0 - resolution: "@cucumber/cucumber@npm:10.4.0" +"@cucumber/cucumber@npm:^10.8.0": + version: 10.8.0 + resolution: "@cucumber/cucumber@npm:10.8.0" dependencies: "@cucumber/ci-environment": "npm:10.0.1" "@cucumber/cucumber-expressions": "npm:17.1.0" @@ -1938,7 +1938,7 @@ __metadata: yup: "npm:1.2.0" bin: cucumber-js: bin/cucumber.js - checksum: 10/9527f82450d1b3af51d1aa5df4aed3db24c41c9b6587de833f607c2ad9a3832b010d43875e3e6ead3f5801f737d0db62b5cd264a04acb6e963a7083af16634cb + checksum: 10/537bd8b89128a00dddee690e2056c3513ec537cc5f45de8f49b231cf7b3219c45e6536a0c1201efef01772a473673fffbe64fe104ee5451ef93bbf1b60b5930f languageName: node linkType: hard @@ -2021,7 +2021,7 @@ __metadata: languageName: node linkType: hard -"@cucumber/messages@npm:24.1.0, @cucumber/messages@npm:>=19.1.4 <=24, @cucumber/messages@npm:^24.1.0": +"@cucumber/messages@npm:24.1.0, @cucumber/messages@npm:>=19.1.4 <=24": version: 24.1.0 resolution: "@cucumber/messages@npm:24.1.0" dependencies: @@ -2045,6 +2045,18 @@ __metadata: languageName: node linkType: hard +"@cucumber/messages@npm:^25.0.1": + version: 25.0.1 + resolution: "@cucumber/messages@npm:25.0.1" + dependencies: + "@types/uuid": "npm:9.0.8" + class-transformer: "npm:0.5.1" + reflect-metadata: "npm:0.2.2" + uuid: "npm:9.0.1" + checksum: 10/ed3d4554cf540f432622b05e419d310852070437dbb0f0fe030156cefa523015bc5895e4e1660356b0d75c217d4a9e8f62d436ccbba18587b53360af7c42d102 + languageName: node + linkType: hard + "@cucumber/tag-expressions@npm:6.1.0": version: 6.1.0 resolution: "@cucumber/tag-expressions@npm:6.1.0" @@ -4103,12 +4115,12 @@ __metadata: "@babel/plugin-transform-modules-commonjs": "npm:^7.24.6" "@babel/preset-env": "npm:^7.24.6" "@babel/preset-typescript": "npm:^7.24.6" - "@cucumber/cucumber": "npm:^10.4.0" + "@cucumber/cucumber": "npm:^10.8.0" "@cucumber/gherkin": "npm:^28.0.0" "@cucumber/gherkin-streams": "npm:^5.0.1" "@cucumber/gherkin-utils": "npm:^9.0.0" "@cucumber/message-streams": "npm:^4.0.1" - "@cucumber/messages": "npm:^24.1.0" + "@cucumber/messages": "npm:^25.0.1" "@types/babel__core": "npm:^7" "@types/babel__preset-env": "npm:^7" "@types/chai": "npm:^4.3.6" @@ -4140,6 +4152,8 @@ __metadata: ts-node: "npm:^10.9.1" typescript: "npm:^5.2.2" vitest: "npm:^1.6.0" + peerDependencies: + "@cucumber/cucumber": ^10.8.0 languageName: unknown linkType: soft @@ -12646,6 +12660,13 @@ __metadata: languageName: node linkType: hard +"reflect-metadata@npm:0.2.2": + version: 0.2.2 + resolution: "reflect-metadata@npm:0.2.2" + checksum: 10/1c93f9ac790fea1c852fde80c91b2760420069f4862f28e6fae0c00c6937a56508716b0ed2419ab02869dd488d123c4ab92d062ae84e8739ea7417fae10c4745 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.1 resolution: "regenerate-unicode-properties@npm:10.1.1"