├── .github └── workflows │ ├── pr-check.yml │ └── test.yml ├── .gitignore ├── README.md ├── action.yml ├── cucumber-report.json ├── cucumber └── js │ ├── cucumber.js │ ├── features │ ├── background.feature │ └── test.feature │ └── step-definitions │ └── stepdefs.js ├── dist ├── index.js ├── index.js.map └── sourcemap-register.js ├── doc └── demo.png ├── index.js ├── package-lock.json ├── package.json ├── reportReader-json.js ├── reportReader-ndjson.js └── test.js /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR check 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | check-build-files-in-commit: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: npm ci 12 | - run: npm run build 13 | - name: Confirm new build included in commit 14 | # language=bash 15 | run: | 16 | # Confirm that the dist was built, and changes included in the commit 17 | if [ $(git status --porcelain dist/index.js | wc -l) -eq "1" ]; then 18 | echo "::error title=Build not committed::Running 'npm run build' changes 'dist/index.js' - build locally and add changes to commit." 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | workflow_dispatch: 9 | 10 | jobs: 11 | direct-from-repository-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Confirm execution without install/build 16 | uses: ./ 17 | with: 18 | name: "Successful cucumber report direct from repository" 19 | access-token: ${{ secrets.GITHUB_TOKEN }} 20 | path: "**/cucumber-report.json" # This committed report should be a success 21 | 22 | ubuntu-action-tests: 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | extension: ["json", "ndjson"] 27 | steps: 28 | - uses: actions/checkout@v3 29 | - run: npm ci 30 | - run: npm run cucumber || true 31 | - run: npm run cucumber-ok || true 32 | - run: npm run cucumber-undefined || true 33 | - run: npm run cucumber-non-failed || true 34 | - run: npm run cucumber-empty || true 35 | - uses: ./ 36 | name: "fail because of error" 37 | with: 38 | name: "${{ matrix.extension }}: failed cucumber report" 39 | access-token: ${{ secrets.GITHUB_TOKEN }} 40 | path: "**/cucumber-report.${{ matrix.extension }}" 41 | - uses: ./ 42 | name: "fail because of error (without number of error)" 43 | with: 44 | name: "${{ matrix.extension }}: failed (no number of error)" 45 | access-token: ${{ secrets.GITHUB_TOKEN }} 46 | path: "**/cucumber-report.${{ matrix.extension }}" 47 | show-number-of-error-on-check-title: false 48 | - uses: ./ 49 | name: "fail because of error with all annotation" 50 | with: 51 | name: "${{ matrix.extension }}: failed with all annotation" 52 | access-token: ${{ secrets.GITHUB_TOKEN }} 53 | path: "**/cucumber-report.${{ matrix.extension }}" 54 | show-number-of-error-on-check-title: false 55 | annotation-status-on-undefined: notice 56 | annotation-status-on-pending: notice 57 | - uses: ./ 58 | name: "fail because of undefined test" 59 | with: 60 | name: "${{ matrix.extension }}: failed because of undefined test" 61 | access-token: ${{ secrets.GITHUB_TOKEN }} 62 | path: "**/cucumber-report-non-failed.${{ matrix.extension }}" 63 | check-status-on-undefined: "failure" 64 | annotation-status-on-undefined: "failure" 65 | - uses: ./ 66 | name: "fail because of pending test" 67 | with: 68 | name: "${{ matrix.extension }}: failed because of pending test" 69 | access-token: ${{ secrets.GITHUB_TOKEN }} 70 | path: "**/cucumber-report-non-failed.${{ matrix.extension }}" 71 | check-status-on-pending: "failure" 72 | annotation-status-on-pending: "failure" 73 | - uses: ./ 74 | name: "neutral because of error" 75 | with: 76 | name: "${{ matrix.extension }}: neutral cucumber report" 77 | access-token: ${{ secrets.GITHUB_TOKEN }} 78 | path: "**/cucumber-report.${{ matrix.extension }}" 79 | check-status-on-error: 'neutral' 80 | annotation-status-on-error: 'warning' 81 | - uses: ./ 82 | name: "sucess because all is ok" 83 | with: 84 | name: "${{ matrix.extension }}: success cucumber report" 85 | access-token: ${{ secrets.GITHUB_TOKEN }} 86 | path: "**/cucumber-report-ok.${{ matrix.extension }}" 87 | check-status-on-error: 'neutral' 88 | annotation-status-on-error: 'warning' 89 | - uses: ./ 90 | id: success-with-empty-cucumber-report 91 | name: "success with empty cucumber report" 92 | with: 93 | name: "${{ matrix.extension }}: success empty report" 94 | access-token: ${{ secrets.GITHUB_TOKEN }} 95 | path: "**/cucumber-report-empty.${{ matrix.extension }}" 96 | - uses: ./ 97 | name: "job with summary" 98 | with: 99 | name: "${{ matrix.extension }}: job with summary" 100 | access-token: ${{ secrets.GITHUB_TOKEN }} 101 | path: "**/cucumber-report.${{ matrix.extension }}" 102 | show-global-summary-report: 'true' 103 | - id: check-output 104 | name: "check output for cucumber-report-empty" 105 | if: steps.success-with-empty-cucumber-report.outputs.cucumber-report-empty_failed_scenarios != 0 106 | run: exit 1 107 | - uses: ./ 108 | id: job-failed 109 | continue-on-error: true 110 | name: "fail job due to failed test" 111 | with: 112 | name: "${{ matrix.extension }}: fail job due to failed test" 113 | access-token: ${{ secrets.GITHUB_TOKEN }} 114 | path: "**/cucumber-report-non-failed.${{ matrix.extension }}" 115 | check-status-on-pending: "failure" 116 | annotation-status-on-pending: "failure" 117 | number-of-test-error-to-fail-job: 1 118 | - name: 'check that job-failed outcome is failed' 119 | if: steps.job-failed.outcome == 'failure' 120 | run: exit 0 121 | - name: 'check that job-failed outcome is failed' 122 | if: steps.job-failed.outcome != 'failure' 123 | run: exit 1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules/ 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # IDE 23 | *.iml 24 | .idea/ 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Report Annotations Action 2 | 3 | This GitHub Action publishes action annotations from Cucumber reports. 4 | 5 | ## Example 6 | 7 | ```yaml 8 | - uses: deblockt/cucumber-report-annotations-action@v1.7 9 | with: 10 | access-token: ${{ secrets.GITHUB_TOKEN }} 11 | path: "**/cucumber-report.json" 12 | ``` 13 | 14 | ![demo](doc/demo.png) 15 | 16 | ## Supported Formats 17 | 18 | This GitHub Action supports two formats: 19 | 20 | - **JSON**: The deprecated Cucumber report format. Prefer using the message format. If you use this format, the file extension should be `.json`. 21 | - **Message**: The new Cucumber report format using NDJSON (newline-delimited JSON). If you use this format, the file extension should be `.ndjson`. 22 | 23 | ## Parameters 24 | 25 | All parameters in this action are optional, allowing flexibility in configuration based on your needs. 26 | 27 | | **Input Name** | **Description** | **Default** | **Options** | 28 | |-----------------------------------------|-----------------------------------------------------------------------------------------|---------------------------|---------------------------------| 29 | | **access-token** | GitHub token. | `${{ github.token }}` | | 30 | | **path** | Glob pattern to locate Cucumber JSON files. | `**/cucumber-report.json` | | 31 | | **name** | The name of the check. | `Cucumber report` | | 32 | | **check-status-on-error** | Check status for Cucumber errors. | `failure` | `success`, `neutral`, `failure` | 33 | | **check-status-on-undefined** | Check status for undefined steps. | `success` | `success`, `neutral`, `failure` | 34 | | **check-status-on-pending** | Check status for pending steps. | `success` | `success`, `neutral`, `failure` | 35 | | **annotation-status-on-error** | Annotation status for errors. | `failure` | `notice`, `warning`, `failure` | 36 | | **annotation-status-on-undefined** | Annotation status for undefined steps. No annotation if not set. | | `notice`, `warning`, `failure` | 37 | | **annotation-status-on-pending** | Annotation status for pending steps. No annotation if not set. | | `notice`, `warning`, `failure` | 38 | | **show-number-of-error-on-check-title** | Show the number of errors in the check title (visible in PR checks). | `true` | `true`, `false` | 39 | | **show-global-summary-report** | Display a full summary report for each feature file. | `false` | `true`, `false` | 40 | | **number-of-test-error-to-fail-job** | Number of test errors required to fail the build.
`-1` prevents the build from failing. | `-1` | | 41 | 42 | ## Outputs 43 | 44 | The following variables are available as output (with the JSON file name used as a base, spaces replaced by underscores, and without the `.json` extension): 45 | 46 | - `${output}_failed_scenarios`: Number of failed scenarios. 47 | - `${output}_undefined_scenarios`: Number of undefined scenarios. 48 | - `${output}_pending_scenarios`: Number of pending scenarios. 49 | - `${output}_passed_scenarios`: Number of passed scenarios. 50 | - `${output}_failed_steps`: Number of failed steps. 51 | - `${output}_undefined_steps`: Number of undefined steps. 52 | - `${output}_pending_steps`: Number of pending steps. 53 | - `${output}_passed_steps`: Number of passed steps. 54 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Cucumber Report to Annotations' 2 | description: 'Create an annotation of the test run summary and also list first n failed tests as seporate annotations' 3 | branding: 4 | icon: check-square 5 | color: green 6 | inputs: 7 | access-token: 8 | description: "github token" 9 | required: true 10 | default: ${{ github.token }} 11 | path: 12 | description: "glob to cucumber json files" 13 | required: true 14 | default: "**/cucumber-report.json" 15 | check-status-on-error: 16 | description: "the check status to use on cucumber error. Can be 'success', 'neutral' or 'failure'" 17 | required: true 18 | default: 'failure' 19 | check-status-on-undefined: 20 | description: "the check status to use on cucumber undefined steps. Can be 'success', 'neutral' or 'failure'" 21 | required: true 22 | default: 'success' 23 | check-status-on-pending: 24 | description: "the check status to use on cucumber pending steps. Can be 'success', 'neutral' or 'failure'" 25 | required: true 26 | default: 'success' 27 | annotation-status-on-error: 28 | description: "the annotation status on error. Can be 'notice', 'warning', 'failure'" 29 | require: true 30 | default: 'failure' 31 | annotation-status-on-undefined: 32 | description: "the annotation status on undefined steps. Can be 'notice', 'warning', 'failure'. if this property is not set, no annotation will be generated for undefined steps" 33 | require: false 34 | annotation-status-on-pending: 35 | description: "the annotation status on pending steps. Can be 'notice', 'warning', 'failure'. if this property is not set, no annotation will be generated for pending steps" 36 | require: false 37 | show-number-of-error-on-check-title: 38 | description: "if it is set to true, the number of errors will be indicated on the check title (visible on the pr check)" 39 | require: true 40 | default: 'true' 41 | show-global-summary-report: 42 | description: "if it is set to true, a full summary report will be display for each feature file." 43 | require: true 44 | default: 'false' 45 | number-of-test-error-to-fail-job: 46 | description: "indicate the number of test in error to fail the build. If the value is -1 this action will never fail the build." 47 | require: true 48 | default: -1 49 | name: 50 | description: "the name of the check" 51 | require: true 52 | default: 'Cucumber report' 53 | 54 | runs: 55 | using: 'node20' 56 | main: 'dist/index.js' 57 | -------------------------------------------------------------------------------- /cucumber-report.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keyword": "Feature", 4 | "name": "Test file", 5 | "line": 1, 6 | "id": "test-file", 7 | "tags": [], 8 | "uri": "cucumber/features/test.feature", 9 | "elements": [ 10 | { 11 | "id": "test-file;scenario-ok", 12 | "keyword": "Scenario", 13 | "line": 4, 14 | "name": "Scenario OK", 15 | "tags": [ 16 | { 17 | "name": "@SuccessTest", 18 | "line": 3 19 | } 20 | ], 21 | "type": "scenario", 22 | "steps": [ 23 | { 24 | "arguments": [], 25 | "keyword": "When ", 26 | "line": 5, 27 | "name": "this step will success", 28 | "match": { 29 | "location": "cucumber/step-definitions/stepdefs.js:8" 30 | }, 31 | "result": { 32 | "status": "passed", 33 | "duration": 1000000 34 | } 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | ] -------------------------------------------------------------------------------- /cucumber/js/cucumber.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: `--format-options '{"snippetInterface": "synchronous"}'` 3 | } -------------------------------------------------------------------------------- /cucumber/js/features/background.feature: -------------------------------------------------------------------------------- 1 | Feature: Test with background 2 | 3 | Background: 4 | Given this step will fail 5 | 6 | Scenario: This scenario will fail because of background 7 | When this step will success 8 | -------------------------------------------------------------------------------- /cucumber/js/features/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test file 2 | 3 | @NonFailedTest 4 | @SuccessTest 5 | Scenario: Scenario OK 6 | When this step will success 7 | 8 | 9 | Scenario: Scenario KO 10 | When this step will fail 11 | 12 | @SuccessTest 13 | @NonFailedTest 14 | @UndefinedStepTest 15 | Scenario: Scenario with undefined step 16 | Given this step will success 17 | When this step is undefined 18 | 19 | @NonFailedTest 20 | Scenario: Scenario with pending step 21 | When this step is pending 22 | 23 | Scenario Outline: Scenario with Outline 24 | Given this step will success using example 25 | Examples: 26 | | variable | 27 | | tets | 28 | | test2 | -------------------------------------------------------------------------------- /cucumber/js/step-definitions/stepdefs.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { Given, When, Then } = require('@cucumber/cucumber'); 3 | 4 | When('this step will fail', function () { 5 | assert.fail('this step allways fail') 6 | }); 7 | 8 | When('this step will success', function () { 9 | }); 10 | 11 | When('this step is pending', function() { 12 | return 'pending'; 13 | }) 14 | 15 | When('this step will success using example {string}', function() { 16 | }) -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={650:e=>{var r=Object.prototype.toString;var n=typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},274:(e,r,n)=>{var t=n(339);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(190);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},680:(e,r,n)=>{var t=n(339);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.H=MappingList},758:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(339);var i=n(345);var a=n(274).I;var u=n(449);var s=n(758).U;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){d.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(d,o.compareByOriginalPositions);this.__originalMappings=d};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(449);var o=n(339);var i=n(274).I;var a=n(680).H;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var h=0,d=g.length;h0){if(!o.compareByGeneratedPositionsInflated(c,g[h-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.h=SourceMapGenerator},351:(e,r,n)=>{var t;var o=n(591).h;var i=n(339);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},997:(e,r,n)=>{n(591).h;r.SourceMapConsumer=n(952).SourceMapConsumer;n(351)},284:(e,r,n)=>{e=n.nmd(e);var t=n(997).SourceMapConsumer;var o=n(17);var i;try{i=n(147);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(650);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var h=[];var d=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=h.slice(0);var _=d.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){h.length=0}h.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){d.length=0}d.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){h.length=0;d.length=0;h=S.slice(0);d=_.slice(0);v=handlerExec(d);m=handlerExec(h)}},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};(()=>{__webpack_require__(284).install()})();module.exports=n})(); -------------------------------------------------------------------------------- /doc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deblockt/cucumber-report-annotations-action/9494d7d413bab67984452211bd3f54b08a7875c0/doc/demo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const github = require('@actions/github'); 3 | const glob = require("@actions/glob"); 4 | const fs = require("fs"); 5 | const reportReaderJson = require('./reportReader-json'); 6 | const reportReaderNdJson = require('./reportReader-ndjson'); 7 | const path = require('path'); 8 | 9 | function memoize(fn) { 10 | const cache = {}; 11 | 12 | return (...args) => { 13 | let argsString = JSON.stringify(args); 14 | return argsString in cache 15 | ? cache[argsString] 16 | : (cache[argsString] = fn(...args)); 17 | } 18 | } 19 | 20 | async function findBestFileMatch(file) { 21 | let searchFile = file; 22 | if (searchFile.startsWith('classpath:')) { 23 | searchFile = searchFile.substring(10); 24 | } 25 | const globber = await glob.create('**/' + searchFile, { 26 | followSymbolicLinks: false, 27 | }); 28 | const files = await globber.glob() 29 | if (files.length > 0) { 30 | const featureFile = files[0]; 31 | const repoName = github.context.repo.repo; 32 | const indexOfRepoName = featureFile.indexOf(repoName); 33 | const filePathWithoutWorkspace = featureFile.substring(indexOfRepoName + repoName.length * 2 + 2); 34 | return filePathWithoutWorkspace; 35 | } 36 | 37 | return undefined; 38 | } 39 | 40 | const memoizedFindBestFileMatch = memoize(findBestFileMatch) 41 | 42 | async function buildStepAnnotation(cucumberError, status, errorType) { 43 | return { 44 | path: (await memoizedFindBestFileMatch(cucumberError.file)) || cucumberError.file, 45 | start_line: cucumberError.line || 0, 46 | end_line: cucumberError.line || 0, 47 | start_column: 0, 48 | end_column: 0, 49 | annotation_level: status, 50 | title: cucumberError.title + ' ' + errorType + '.', 51 | message: 'Scenario: ' + cucumberError.title + '\nStep: ' + cucumberError.step + '\nError: \n' + cucumberError.error 52 | } 53 | } 54 | 55 | async function buildReportDetailAnnotation(fileReport) { 56 | const message = fileReport.scenarios 57 | .map(scenario => `${emojiByStatus(scenario.status)} Scenario: ${scenario.name}`) 58 | .join('\n'); 59 | 60 | return { 61 | path: (await memoizedFindBestFileMatch(fileReport.file)) || fileReport.file, 62 | start_line: 0, 63 | end_line: 0, 64 | start_column: 0, 65 | end_column: 0, 66 | annotation_level: 'notice', 67 | title: `Feature: ${fileReport.name} Report`, 68 | message 69 | }; 70 | } 71 | 72 | async function buildErrorAnnotations(cucumberError, statusOnError) { 73 | return await buildStepAnnotation(cucumberError, statusOnError, 'Failed'); 74 | } 75 | 76 | async function buildUndefinedAnnotation(cucumberError, statusOnSkipped) { 77 | return await buildStepAnnotation(cucumberError, statusOnSkipped, 'Undefined'); 78 | } 79 | 80 | async function buildPendingAnnotation(cucumberError, statusOnPending) { 81 | return await buildStepAnnotation(cucumberError, statusOnPending, 'Pending'); 82 | } 83 | 84 | function emojiByStatus(status) { 85 | switch (status) { 86 | case 'success': 87 | return '✅'; 88 | case 'failed': 89 | return '❌' 90 | case 'pending': 91 | return '⌛'; 92 | default: 93 | return '-'; 94 | } 95 | } 96 | 97 | function setOutput(core, outputName, summaryScenario, summarySteps) { 98 | for (const type in summaryScenario) { 99 | core.debug(`publish output ${outputName}_${type}_scenarios=${summaryScenario[type]}`); 100 | core.setOutput(`${outputName}_${type}_scenarios`, summaryScenario[type]); 101 | } 102 | for (const type in summarySteps) { 103 | core.debug(`publish output ${outputName}_${type}_steps=${summaryScenario[type]}`); 104 | core.setOutput(`${outputName}_${type}_steps`, summaryScenario[type]); 105 | } 106 | } 107 | 108 | (async() => { 109 | const inputPath = core.getInput('path'); 110 | const checkName = core.getInput('name'); 111 | const accessToken = core.getInput('access-token'); 112 | const checkStatusOnError = core.getInput('check-status-on-error'); 113 | const checkStatusOnUndefined = core.getInput('check-status-on-undefined'); 114 | const checkStatusOnPending = core.getInput('check-status-on-pending'); 115 | const annotationStatusOnError = core.getInput('annotation-status-on-error'); 116 | const annotationStatusOnUndefined = core.getInput('annotation-status-on-undefined'); 117 | const annotationStatusOnPending = core.getInput('annotation-status-on-pending'); 118 | const showNumberOfErrorOnCheckTitle = core.getInput('show-number-of-error-on-check-title'); 119 | const numberOfTestErrorToFailJob = core.getInput('number-of-test-error-to-fail-job'); 120 | const showGlobalSummaryReport = core.getInput('show-global-summary-report') 121 | const globber = await glob.create(inputPath, { 122 | followSymbolicLinks: false, 123 | }); 124 | 125 | core.info("start to read cucumber logs using path " + inputPath); 126 | 127 | for await (const cucumberReportFile of globber.globGenerator()) { 128 | core.info("found cucumber report " + cucumberReportFile); 129 | 130 | const reportOutputName = path.basename(cucumberReportFile).replace(' ', '_').replace('.json', ''); 131 | const reportResultString = await fs.promises.readFile(cucumberReportFile); 132 | const reportResult = (cucumberReportFile.endsWith('.json') ? reportReaderJson : reportReaderNdJson).reader(reportResultString); 133 | const globalInformation = reportResult.globalInformation; 134 | const summaryScenario = { 135 | 'failed': globalInformation.failedScenarioNumber, 136 | 'undefined': globalInformation.undefinedScenarioNumber, 137 | 'pending': globalInformation.pendingScenarioNumber, 138 | 'passed': globalInformation.succeedScenarioNumber 139 | }; 140 | const summarySteps = { 141 | 'failed': globalInformation.failedStepsNumber, 142 | 'undefined': globalInformation.undefinedStepsNumber, 143 | 'skipped': globalInformation.skippedStepsNumber, 144 | 'pending': globalInformation.pendingStepNumber, 145 | 'passed': globalInformation.succeedStepsNumber 146 | }; 147 | setOutput(core, reportOutputName, summaryScenario, summarySteps); 148 | 149 | const summary = 150 | buildSummary(globalInformation.scenarioNumber, 'Scenarios', summaryScenario) 151 | + '\n' 152 | + buildSummary(globalInformation.stepsNumber, 'Steps', summarySteps); 153 | 154 | const errors = reportResult.failedSteps; 155 | var errorAnnotations = await Promise.all(errors.map(e => buildErrorAnnotations(e, annotationStatusOnError))); 156 | 157 | if (annotationStatusOnUndefined) { 158 | const undefined = reportResult.undefinedSteps; 159 | var undefinedAnnotations = await Promise.all(undefined.map(e => buildUndefinedAnnotation(e, annotationStatusOnUndefined))); 160 | errorAnnotations.push(...undefinedAnnotations); 161 | } 162 | if (annotationStatusOnPending) { 163 | const pending = reportResult.pendingSteps; 164 | var pendingAnnotations = await Promise.all(pending.map(e => buildPendingAnnotation(e, annotationStatusOnPending))); 165 | errorAnnotations.push(...pendingAnnotations); 166 | } 167 | 168 | // TODO make an update request if there are more than 50 annotations 169 | const errorAnnotationsToCreate = errorAnnotations.slice(0, 49); 170 | const pullRequest = github.context.payload.pull_request; 171 | const head_sha = (pullRequest && pullRequest.head.sha) || github.context.sha; 172 | const annotations = [ 173 | ...errorAnnotationsToCreate 174 | ]; 175 | 176 | var additionnalTitleInfo = ''; 177 | if (showNumberOfErrorOnCheckTitle == 'true' && globalInformation.failedScenarioNumber > 0) { 178 | additionnalTitleInfo = ` (${globalInformation.failedScenarioNumber} error${globalInformation.failedScenarioNumber > 1 ? 's': ''})`; 179 | } 180 | var checkStatus = ''; 181 | if (globalInformation.failedScenarioNumber > 0 && checkStatusOnError !== 'success') { 182 | checkStatus = checkStatusOnError; 183 | } else if (globalInformation.undefinedStepsNumber > 0 && checkStatusOnUndefined !== 'success') { 184 | checkStatus = checkStatusOnUndefined; 185 | } else if (globalInformation.pendingStepNumber > 0) { 186 | checkStatus = checkStatusOnPending; 187 | } else { 188 | checkStatus = 'success'; 189 | } 190 | const createCheckRequest = { 191 | ...github.context.repo, 192 | name: checkName, 193 | head_sha, 194 | status: 'completed', 195 | conclusion: checkStatus, 196 | output: { 197 | title: checkName + additionnalTitleInfo, 198 | summary, 199 | annotations 200 | }, 201 | }; 202 | 203 | core.info('Creating summary:\n' + summary); 204 | await core.summary 205 | .addHeading(checkName + additionnalTitleInfo, 4) 206 | .addRaw("\n" + summary) 207 | .write() 208 | 209 | core.info('Sending cucumber annotations'); 210 | const octokit = github.getOctokit(accessToken); 211 | const checksReponse = await octokit.rest.checks.create(createCheckRequest); 212 | 213 | if (numberOfTestErrorToFailJob != -1 && errorAnnotations.length >= numberOfTestErrorToFailJob) { 214 | core.setFailed(`${errorAnnotations.length} test(s) in error`); 215 | } 216 | 217 | if (showGlobalSummaryReport === 'true') { 218 | core.info('Building all scenario summary') 219 | const allScenarioByFile = reportResult.listAllScenarioByFile; 220 | const allAnnoattions = await Promise.all( 221 | allScenarioByFile 222 | .map(buildReportDetailAnnotation) 223 | .reduce((a, b) => a.concat(b), []) 224 | ); 225 | core.info('Send core scenario summary') 226 | await octokit.rest.checks.update({ 227 | ...github.context.repo, 228 | check_run_id: checksReponse.data.id, 229 | output: { 230 | title: checkName + additionnalTitleInfo, 231 | summary, 232 | annotations: allAnnoattions 233 | } 234 | }); 235 | } 236 | } 237 | })(); 238 | 239 | function buildSummary(itemNumber, itemType, itemCounts) { 240 | const header = itemNumber + ' ' + itemType; 241 | const counts = Object.keys(itemCounts) 242 | .filter(key => itemCounts[key] > 0) 243 | .map(key => itemCounts[key] + ' ' + key) 244 | .join(', '); 245 | return ` ${header} (${counts})`; 246 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cucumber-report-annotations-action", 3 | "version": "1.0.0", 4 | "description": "This action should be used to publish action annotations from cucumber json report.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "ncc build --source-map index.js", 8 | "test": "mocha **/*.spec.js", 9 | "cucumber": "cucumber-js cucumber --format json:cucumber-report.json --format message:cucumber-report.ndjson --require '**/step-definitions/**/*.js'", 10 | "cucumber-ok": "cucumber-js cucumber --format json:cucumber-report-ok.json --format message:cucumber-report-ok.ndjson --require '**/step-definitions/**/*.js' --tags @SuccessTest", 11 | "cucumber-undefined": "cucumber-js cucumber --format json:cucumber-report-undefined.json --format message:cucumber-report-undefined.ndjson --require '**/step-definitions/**/*.js' --tags @UndefinedStepTest", 12 | "cucumber-non-failed": "cucumber-js cucumber --format json:cucumber-report-non-failed.json --format message:cucumber-report-non-failed.ndjson --require '**/step-definitions/**/*.js' --tags @NonFailedTest", 13 | "cucumber-empty": "cucumber-js cucumber --format json:cucumber-report-empty.json --format message:cucumber-report-empty.ndjson --require '**/step-definitions/**/*.js' --tags @NonExistingTag" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/deblockt/cucumber-report-annotations-action.git" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/deblockt/cucumber-report-annotations-action/issues" 24 | }, 25 | "homepage": "https://github.com/deblockt/cucumber-report-annotations-action#readme", 26 | "dependencies": { 27 | "@actions/core": "^1.10.0", 28 | "@actions/github": "^6.0.0", 29 | "@actions/glob": "^0.3.0" 30 | }, 31 | "devDependencies": { 32 | "@vercel/ncc": "^0.34.0", 33 | "@cucumber/cucumber": "^10.3.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /reportReader-json.js: -------------------------------------------------------------------------------- 1 | const EMPTY_GLOBAL_INFO = { 2 | scenarioNumber: 0, 3 | failedScenarioNumber: 0, 4 | pendingScenarioNumber: 0, 5 | undefinedScenarioNumber: 0, 6 | stepsNumber: 0, 7 | succeedScenarioNumber: 0, 8 | failedStepsNumber: 0, 9 | skippedStepsNumber: 0, 10 | undefinedStepsNumber: 0, 11 | succeedStepsNumber: 0, 12 | pendingStepNumber: 0 13 | } 14 | 15 | module.exports.reader = (reportString) => { 16 | const report = JSON.parse(reportString) 17 | return { 18 | get listAllScenarioByFile() { 19 | return report 20 | .map(fileReport => fileAllScenario(fileReport)) 21 | }, 22 | get globalInformation() { 23 | return report 24 | .map(fileReport => globalFileInformation(fileReport)) 25 | .reduce((a, b) => sum(a, b), EMPTY_GLOBAL_INFO); 26 | }, 27 | get failedSteps() { 28 | return report 29 | .map(fileReport => fileFailureStepData(fileReport)) 30 | .reduce((a, b) => a.concat(b), []); 31 | }, 32 | get undefinedSteps() { 33 | return report 34 | .map(fileReport => fileUndefinedStepsData(fileReport)) 35 | .reduce((a, b) => a.concat(b), []); 36 | }, 37 | get pendingSteps() { 38 | return report 39 | .map(fileReport => filePendingStepsData(fileReport)) 40 | .reduce((a, b) => a.concat(b), []); 41 | } 42 | } 43 | } 44 | 45 | function fileFailureStepData(fileReport) { 46 | return fileReport.elements 47 | .filter(scenario => hasFailed(scenario)) 48 | .map(failedScenario => buildFailData(fileReport, failedScenario)); 49 | } 50 | 51 | function fileUndefinedStepsData(fileReport) { 52 | return fileReport.elements 53 | .filter(scenario => hasUndefined(scenario)) 54 | .map(undefinedScenario => buildUndefinedData(fileReport, undefinedScenario)); 55 | } 56 | 57 | function filePendingStepsData(fileReport) { 58 | return fileReport.elements 59 | .filter(scenario => hasPending(scenario)) 60 | .map(pendingScenario => buildPendingData(fileReport, pendingScenario)); 61 | } 62 | 63 | function fileAllScenario(fileReport) { 64 | return { 65 | file: fileReport.uri, 66 | name: fileReport.name, 67 | scenarios: fileReport.elements 68 | .map(scenario => ({ 69 | name: scenario.name, 70 | status: getScenarioStatus(scenario) 71 | })) 72 | } 73 | } 74 | 75 | function sum(info1, info2) { 76 | return { 77 | scenarioNumber: info1.scenarioNumber + info2.scenarioNumber, 78 | failedScenarioNumber: info1.failedScenarioNumber + info2.failedScenarioNumber, 79 | pendingScenarioNumber: info1.pendingScenarioNumber + info2.pendingScenarioNumber, 80 | undefinedScenarioNumber: info1.undefinedScenarioNumber + info2.undefinedScenarioNumber, 81 | stepsNumber: info1.stepsNumber + info2.stepsNumber, 82 | succeedScenarioNumber: info1.succeedScenarioNumber + info2.succeedScenarioNumber, 83 | failedStepsNumber: info1.failedStepsNumber + info2.failedStepsNumber, 84 | skippedStepsNumber: info1.skippedStepsNumber + info2.skippedStepsNumber, 85 | undefinedStepsNumber: info1.undefinedStepsNumber + info2.undefinedStepsNumber, 86 | succeedStepsNumber: info1.succeedStepsNumber + info2.succeedStepsNumber, 87 | pendingStepNumber: info1.pendingStepNumber + info2.pendingStepNumber 88 | } 89 | } 90 | 91 | function globalFileInformation(reportFile) { 92 | const scenario = reportFile.elements 93 | .filter(element => element.type === 'scenario'); 94 | 95 | const failedScenarioNumber = scenario 96 | .filter(scenario => hasFailed(scenario)) 97 | .length; 98 | const undefinedScenarioNumber = scenario 99 | .filter(scenario => hasUndefined(scenario)) 100 | .length; 101 | const pendingScenarioNumber = scenario 102 | .filter(scenario => hasPending(scenario)) 103 | .length; 104 | const stepsNumber = reportFile.elements 105 | .map(scenario => scenario.steps.length) 106 | .reduce((a, b) => a + b, 0); 107 | const failedStepsNumber = reportFile.elements 108 | .map(scenario => getFailedSteps(scenario).length) 109 | .reduce((a, b) => a + b, 0); 110 | const skippedStepsNumber = reportFile.elements 111 | .map(scenario => getSkippedSteps(scenario).length) 112 | .reduce((a, b) => a + b, 0); 113 | const undefinedStepsNumber = reportFile.elements 114 | .map(scenario => getUndefinedSteps(scenario).length) 115 | .reduce((a, b) => a + b, 0); 116 | const pendingStepNumber = reportFile.elements 117 | .map(scenario => getPendingSteps(scenario).length) 118 | .reduce((a, b) => a + b, 0); 119 | 120 | const result = { 121 | scenarioNumber: scenario.length, 122 | failedScenarioNumber: failedScenarioNumber, 123 | undefinedScenarioNumber: undefinedScenarioNumber, 124 | pendingScenarioNumber: pendingScenarioNumber, 125 | succeedScenarioNumber: scenario.length - failedScenarioNumber - undefinedScenarioNumber - pendingScenarioNumber, 126 | stepsNumber: stepsNumber, 127 | failedStepsNumber: failedStepsNumber, 128 | skippedStepsNumber: skippedStepsNumber, 129 | undefinedStepsNumber: undefinedStepsNumber, 130 | pendingStepNumber: pendingStepNumber, 131 | succeedStepsNumber: stepsNumber - failedStepsNumber - skippedStepsNumber - undefinedStepsNumber - pendingStepNumber 132 | }; 133 | 134 | return result; 135 | } 136 | 137 | function getFailedSteps(scenario) { 138 | return getStepByStatus(scenario, 'failed'); 139 | } 140 | function getSkippedSteps(scenario) { 141 | return getStepByStatus(scenario, 'skipped'); 142 | } 143 | function getUndefinedSteps(scenario) { 144 | return getStepByStatus(scenario, 'undefined'); 145 | } 146 | function getPendingSteps(scenario) { 147 | return getStepByStatus(scenario, 'pending'); 148 | } 149 | function getScenarioStatus(scenario) { 150 | const steps = [...scenario.before || [], ...scenario.after || [], ...scenario.steps || []]; 151 | for (const step of steps) { 152 | if (step.result.status === 'failed') { 153 | return 'failed'; 154 | } else if (step.result.status === 'skipped') { 155 | return 'skipped'; 156 | } else if (step.result.status === 'undefined') { 157 | return 'undefined'; 158 | } else if (step.result.status === 'pending') { 159 | return 'pending'; 160 | } 161 | } 162 | return 'success'; 163 | } 164 | 165 | function getStepByStatus(scenario, status) { 166 | const before = scenario.before || []; 167 | const after = scenario.after || []; 168 | const steps = scenario.steps || []; 169 | 170 | return before.concat(after, steps) 171 | .filter(step => step.result.status === status); 172 | } 173 | 174 | function hasFailed(scenario) { 175 | return getFailedSteps(scenario).length > 0; 176 | } 177 | 178 | function hasUndefined(scenario) { 179 | return getUndefinedSteps(scenario).length > 0; 180 | } 181 | 182 | function hasPending(scenario) { 183 | return getPendingSteps(scenario).length > 0; 184 | } 185 | 186 | function buildFailData(fileReport, scenario) { 187 | return buildStepData(fileReport, scenario, getFailedSteps); 188 | } 189 | 190 | function buildUndefinedData(fileReport, scenario) { 191 | return buildStepData(fileReport, scenario, getUndefinedSteps); 192 | } 193 | 194 | function buildPendingData(fileReport, scenario) { 195 | return buildStepData(fileReport, scenario, getPendingSteps); 196 | } 197 | 198 | function buildStepData(fileReport, scenario, getStepsFunction) { 199 | const skippedStep = getStepsFunction(scenario)[0]; 200 | return { 201 | file: fileReport.uri, 202 | line: skippedStep.line, 203 | title: scenario.name, 204 | step: skippedStep.name, 205 | error: skippedStep.result.error_message 206 | } 207 | } -------------------------------------------------------------------------------- /reportReader-ndjson.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | 3 | const EMPTY_GLOBAL_INFO = { 4 | scenarioNumber: 0, 5 | failedScenarioNumber: 0, 6 | pendingScenarioNumber: 0, 7 | undefinedScenarioNumber: 0, 8 | stepsNumber: 0, 9 | succeedScenarioNumber: 0, 10 | failedStepsNumber: 0, 11 | skippedStepsNumber: 0, 12 | undefinedStepsNumber: 0, 13 | succeedStepsNumber: 0, 14 | pendingStepNumber: 0 15 | } 16 | 17 | module.exports.reader = (reportString) => { 18 | const features = [] 19 | const scenario = {} 20 | const steps = {} 21 | const pickles = {} 22 | const picklesSteps = {} 23 | const testCases = {} 24 | const testSteps = {} 25 | const globalInfo = { ...EMPTY_GLOBAL_INFO } 26 | reportString 27 | .toString().split('\n') 28 | .filter(it => it !== '') 29 | .forEach(line => { 30 | const element = JSON.parse(line) 31 | if ("gherkinDocument" in element) { 32 | const feature = element.gherkinDocument.feature 33 | const scenarios = [] 34 | feature.children 35 | .filter(it => "scenario" in it) 36 | .forEach(it => { 37 | const sc = { 38 | name: it.scenario.name, 39 | id: it.scenario.id, 40 | location: it.scenario.location, 41 | uri: element.gherkinDocument.uri, 42 | pickles: {} 43 | } 44 | scenario[sc.id] = sc 45 | scenarios.push(sc) 46 | }) 47 | feature.children 48 | .filter(it => "background" in it || "scenario" in it) 49 | .forEach(it => { 50 | const scenarioSteps = it.background?.steps ?? it.scenario?.steps 51 | scenarioSteps.forEach(step => { 52 | steps[step.id] = { 53 | location: step.location 54 | } 55 | }) 56 | }) 57 | 58 | features.push({ 59 | name: feature.name, 60 | location: feature.location, 61 | uri: element.gherkinDocument.uri, 62 | scenarios: scenarios 63 | }) 64 | } else if ("pickle" in element) { 65 | const pk = { 66 | name: element.pickle.name, 67 | scenario: scenario[element.pickle.astNodeIds[0]] 68 | } 69 | pk.steps = element.pickle.steps.map(it => { 70 | return ({ 71 | id: it.id, 72 | name: it.text, 73 | pickle: pk, 74 | location: steps[it.astNodeIds[0]].location 75 | }) 76 | }) 77 | pk.steps.forEach(it => picklesSteps[it.id] = it) 78 | scenario[element.pickle.astNodeIds[0]].pickles[element.pickle.id] = pk 79 | pickles[element.pickle.id] = pk 80 | } else if ("testCase" in element) { 81 | globalInfo.scenarioNumber++; 82 | const caseTestSteps = element.testCase.testSteps.map(it => ({ 83 | id: it.id, 84 | pickleStep: picklesSteps[it.pickleStepId] 85 | })) 86 | caseTestSteps.forEach(it => testSteps[it.id] = it) 87 | const testCase = { 88 | id: element.testCase.id, 89 | pickleId: element.testCase.pickleId, 90 | steps: caseTestSteps 91 | } 92 | pickles[element.testCase.pickleId].testCase = testCase 93 | testCases[testCase.id] = testCase 94 | } else if ("testStepFinished" in element) { 95 | globalInfo.stepsNumber++; 96 | const step = testSteps[element.testStepFinished.testStepId] 97 | step.result = element.testStepFinished.testStepResult 98 | if (step.result.status === 'FAILED') { 99 | globalInfo.failedScenarioNumber++; 100 | globalInfo.failedStepsNumber++; 101 | } else if (step.result.status === 'PENDING') { 102 | globalInfo.pendingScenarioNumber++; 103 | globalInfo.pendingStepNumber++; 104 | } else if (step.result.status === 'UNDEFINED') { 105 | globalInfo.undefinedScenarioNumber++; 106 | globalInfo.undefinedStepsNumber++; 107 | } else if (step.result.status === 'SKIPPED') { 108 | globalInfo.skippedStepsNumber++ 109 | } else if (step.result.status === 'PASSED') { 110 | globalInfo.succeedStepsNumber++; 111 | } 112 | } 113 | }); 114 | 115 | globalInfo.succeedScenarioNumber = globalInfo.scenarioNumber 116 | - globalInfo.failedScenarioNumber - globalInfo.pendingScenarioNumber - globalInfo.undefinedScenarioNumber; 117 | 118 | return { 119 | get listAllScenarioByFile() { 120 | return features 121 | .map(feature => ({ 122 | file: feature.uri, 123 | name: feature.name, 124 | scenarios: feature.scenarios 125 | .flatMap(scenario => 126 | Object.values(scenario.pickles).map(pickle => ({ 127 | name: scenario.name, 128 | status: getTestCaseStatus(pickle.testCase) 129 | })) 130 | ) 131 | })) 132 | }, 133 | get globalInformation() { 134 | return globalInfo 135 | }, 136 | get failedSteps() { 137 | return getStepsByStatus(testSteps, 'FAILED') 138 | }, 139 | get undefinedSteps() { 140 | return getStepsByStatus(testSteps, 'UNDEFINED') 141 | }, 142 | get pendingSteps() { 143 | return getStepsByStatus(testSteps, 'PENDING') 144 | } 145 | } 146 | } 147 | 148 | function getStepsByStatus(testSteps, status) { 149 | return Object.values(testSteps) 150 | .filter(it => it.result.status === status) 151 | .map(it => { 152 | return ({ 153 | file: it.pickleStep.pickle.scenario.uri, 154 | line: it.pickleStep.location.line, 155 | title: it.pickleStep.pickle.name, 156 | step: it.pickleStep.name, 157 | error: it.result.message 158 | }) 159 | }) 160 | } 161 | 162 | function getTestCaseStatus(testCase) { 163 | const steps = testCase.steps; 164 | for (const step of steps) { 165 | if (step.result.status === 'FAILED') { 166 | return 'failed'; 167 | } else if (step.result.status === 'SKIPPED') { 168 | return 'skipped'; 169 | } else if (step.result.status === 'UNDEFINED') { 170 | return 'undefined'; 171 | } else if (step.result.status === 'PENDING') { 172 | return 'pending'; 173 | } 174 | } 175 | return 'success'; 176 | } 177 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deblockt/cucumber-report-annotations-action/9494d7d413bab67984452211bd3f54b08a7875c0/test.js --------------------------------------------------------------------------------