├── .nvmrc ├── .eslintignore ├── .gitignore ├── docs └── example-main.png ├── .github └── dependabot.yml ├── tasks ├── index.js ├── junit-parser.js ├── tasks.js ├── js-parser.js ├── github-api.js └── annotations.js ├── index.test.js ├── Makefile ├── action.yml ├── LICENSE ├── package.json ├── index.js ├── README.md └── .eslintrc.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v12 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .cache/ 3 | .eslintcache 4 | -------------------------------------------------------------------------------- /docs/example-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgnusG/jest-report-action/HEAD/docs/example-main.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '04:00' 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /tasks/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | readAndParseXMLFile, 3 | parseTestInformation, 4 | parseTestsuite 5 | } from './junit-parser'; 6 | 7 | export { createAnnotationsFromTestsuites } from './tasks'; 8 | 9 | export { publishTestResults } from './github-api'; 10 | 11 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | import { readAndParseXMLFile, parseTestsuite, createAnnotationsFromTestsuites } from './tasks'; 2 | 3 | async function testIntegration() { 4 | const { testsuites: jest } = await readAndParseXMLFile('junit.xml'); 5 | const testsuites = jest.testsuite.map(parseTestsuite({ $config: { workingDir: './extension/' } })); 6 | 7 | const annotations = await createAnnotationsFromTestsuites(testsuites); 8 | 9 | console.log(annotations); 10 | } 11 | 12 | testIntegration().catch(error => console.error(error)); 13 | 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | npm run build:unminified 3 | 4 | build_mini: 5 | npm run build:minified 6 | 7 | lint: 8 | npm run lint:code 9 | 10 | build_and_stage: 11 | ( \ 12 | make build_mini && \ 13 | git add dist \ 14 | ) 15 | 16 | push: 17 | git push --follow-tags 18 | 19 | bump_major: 20 | ( \ 21 | npm version major -m "Build latest version %s" && \ 22 | make push \ 23 | ) 24 | 25 | bump_minor: 26 | ( \ 27 | npm version minor -m "Build latest version %s" && \ 28 | make push \ 29 | ) 30 | 31 | bump_patch: 32 | ( \ 33 | npm version patch -m "Build latest version %s" && \ 34 | make push \ 35 | ) 36 | 37 | test_manual: 38 | node -r esm index.test.js 39 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Jest Github Action Reporter 2 | author: IgnusG 3 | description: Jest Junit Github Actions annotation reporter made easy 4 | branding: 5 | icon: list 6 | color: green 7 | inputs: 8 | access-token: 9 | description: Github Token (Auto-Created) 10 | required: true 11 | junit-file: 12 | description: Location/Name of the JUnit File generated by jest-junit 13 | default: junit.xml 14 | run-name: 15 | description: The Name of the Action's Run (Default is build) 16 | default: build 17 | check-name: 18 | description: Customize the name of the check - in case there are multiple 19 | default: Jest 20 | working-directory: 21 | description: Customize the path for source files and the xml file 22 | default: . 23 | runs: 24 | using: node12 25 | main: dist/index.js 26 | -------------------------------------------------------------------------------- /tasks/junit-parser.js: -------------------------------------------------------------------------------- 1 | import * as xmlParser from 'xml2js'; 2 | 3 | import { promises as fs } from 'fs'; 4 | 5 | export async function readAndParseXMLFile(file, { $fs = fs, $xmlParser = xmlParser } = {}) { 6 | const data = await $fs.readFile(file); 7 | const parser = new $xmlParser.Parser(); 8 | 9 | const json = await parser.parseStringPromise(data); 10 | 11 | return json; 12 | } 13 | 14 | export function parseTestInformation(testsuiteRoot) { 15 | const { '$': { tests, failures, time } } = testsuiteRoot; 16 | 17 | return { tests, failures, time }; 18 | } 19 | 20 | export function parseTestcase(testcase) { 21 | const { '$': { classname, name, time }, failure } = testcase; 22 | 23 | return { 24 | describe: classname, 25 | test: name, 26 | time, 27 | failure: failure !== undefined ? failure : false 28 | } 29 | } 30 | 31 | export function parseTestsuite({ $config } = {}) { 32 | return (testsuite) => { 33 | const { '$': { name, errors, failures, skipped, time }, testcase } = testsuite; 34 | 35 | return { 36 | path: `${ $config.workingDir }${ name }`, 37 | errors, failures, skipped, 38 | time, 39 | testcases: Array.isArray(testcase) ? testcase.map(parseTestcase) : [ parseTestcase(testcase) ] 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-report-action", 3 | "version": "2.3.3", 4 | "author": "", 5 | "description": "Parse junit output from jest-junit reporter and display test annotations", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "preversion": "make build_and_stage", 9 | "build": "parcel", 10 | "build:minified": "npm run build -- build index.js --target node --bundle-node-modules --no-source-maps", 11 | "build:unminified": "npm run build:minified -- --no-minify", 12 | "lint": "eslint", 13 | "lint:code": "npm run lint -- *.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/IgnusG/jest-report-action.git" 18 | }, 19 | "keywords": [ 20 | "github", 21 | "actions", 22 | "jest", 23 | "tests" 24 | ], 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/IgnusG/jest-report-action/issues" 28 | }, 29 | "homepage": "https://github.com/IgnusG/jest-report-action#readme", 30 | "dependencies": { 31 | "@actions/core": "^1.2.3", 32 | "@actions/github": "^2.1.1", 33 | "@babel/parser": "^7.9.4", 34 | "@babel/traverse": "^7.9.0", 35 | "@babel/types": "^7.9.0", 36 | "escape-string-regexp": "^2.0.0", 37 | "xml2js": "^0.4.23" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^6.8.0", 41 | "esm": "^3.2.25", 42 | "husky": "^4.2.5", 43 | "lint-staged": "^10.3.0", 44 | "parcel-bundler": "^1.12.4" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "lint-staged": { 52 | "*.js": "npm run lint -- --cache --fix" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers: 0 */ 2 | 3 | import * as core from '@actions/core'; 4 | 5 | import { 6 | readAndParseXMLFile, 7 | parseTestInformation, 8 | parseTestsuite, 9 | createAnnotationsFromTestsuites, 10 | publishTestResults 11 | } from './tasks'; 12 | 13 | function parseWorkingDir(dir) { 14 | if ((/\/$/).test(dir)) return dir; 15 | 16 | return `${ dir }/`; 17 | } 18 | 19 | const config = { 20 | accessToken: core.getInput('access-token'), 21 | junitFile: core.getInput('junit-file'), 22 | runName: core.getInput('run-name'), 23 | checkName: core.getInput('check-name'), 24 | workingDir: parseWorkingDir(core.getInput('working-directory')) 25 | } 26 | 27 | async function parseTestsAndPublishResults( 28 | { $config = config } = {} 29 | ) { 30 | const { testsuites: jest } = await readAndParseXMLFile(`${ $config.workingDir }${ $config.junitFile }`); 31 | 32 | const { time, tests, failures } = parseTestInformation(jest); 33 | 34 | const testsuites = jest.testsuite.map(parseTestsuite({ $config })); 35 | const { annotations, unknownFailures } = await createAnnotationsFromTestsuites(testsuites); 36 | 37 | let testInformation = { 38 | annotations, 39 | time, 40 | passed: tests - failures, 41 | failed: failures, 42 | total: tests, 43 | conclusion: failures > 0 ? 'failure' : 'success' 44 | } 45 | 46 | if (unknownFailures.length > 0) { 47 | testInformation = { 48 | ...testInformation, 49 | details: `Following tests failed, but could not be found in the source files:\n${ unknownFailures.map(fail => `- ${ fail }`).join('\n') }` 50 | }; 51 | } 52 | 53 | await publishTestResults(testInformation, { $config }); 54 | } 55 | 56 | parseTestsAndPublishResults().catch(error => { 57 | core.setFailed(`Something went wrong: ${ error }`); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /tasks/tasks.js: -------------------------------------------------------------------------------- 1 | /* eslint max-lines-per-function: 0 */ 2 | 3 | import { promises as fs } from 'fs'; 4 | 5 | import { findTestIn, parseJs } from './js-parser'; 6 | import { createAnnotation } from './annotations'; 7 | 8 | export async function createAnnotationsFromTestsuites(testsuites) { 9 | let annotations = []; 10 | let unknownFailures = []; 11 | 12 | for (let testsuite of testsuites) { 13 | let file = null; 14 | let extension = null; 15 | 16 | try { 17 | file = await fs.readFile(testsuite.path, { encoding: 'utf-8' }); 18 | 19 | const { groups: { extension: extensionResult } } = (/.*\.(?.*)$/).exec(testsuite.path); 20 | 21 | extension = extensionResult; 22 | } catch(error) { 23 | console.error('Unknown error occured while reading the file.', error); 24 | 25 | unknownFailures = [ 26 | ...unknownFailures, 27 | ...testsuite.testcases.map(({ describe, test }) => `${ describe } > ${ test }`) 28 | ]; 29 | 30 | continue; 31 | } 32 | 33 | let testAst = null; 34 | 35 | try { 36 | testAst = parseJs(file, extension); 37 | } catch(error) { 38 | console.error(`I probably don't understand the file extension .${ extension } yet. Or a different error occured for file ${ testsuite.path }.\n\n`, error); 39 | 40 | unknownFailures = [ 41 | ...unknownFailures, 42 | ...testsuite.testcases.map(({ describe, test }) => `${ describe } > ${ test }`) 43 | ]; 44 | 45 | continue; 46 | } 47 | 48 | for (let testcase of testsuite.testcases) { 49 | if (!testcase.failure) continue; 50 | 51 | const location = findTestIn(testAst)(testcase.describe, testcase.test) 52 | 53 | if (location === false) { 54 | console.error('The following testcase ', testcase.describe, ' > ', testcase.test, ' was not found'); 55 | unknownFailures = [ ...unknownFailures, `${ testcase.describe } > ${ testcase.test }` ]; 56 | } else { 57 | annotations = [ ...annotations, createAnnotation(testsuite, testcase, location) ]; 58 | } 59 | } 60 | } 61 | 62 | return { annotations, unknownFailures }; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /tasks/js-parser.js: -------------------------------------------------------------------------------- 1 | import { parse } from '@babel/parser'; 2 | import traverse from '@babel/traverse'; 3 | import * as t from '@babel/types'; 4 | 5 | import escapeStringRegexp from 'escape-string-regexp'; 6 | 7 | function isLiteralNamed(literalNode, names, { $t = t } = {}) { 8 | const isIdentifier = (node) => Array.isArray(names) 9 | ? names.some(name => $t.isIdentifier(node, { name })) 10 | : $t.isIdentifier(node, { name: names }); 11 | 12 | // Simple describe("") or test("") 13 | if (isIdentifier(literalNode)) return true; 14 | if ($t.isCallExpression(literalNode)) { 15 | let node = literalNode.callee; 16 | 17 | if (!$t.isMemberExpression(node)) return false; 18 | // Advanced describe.each([])("") or test.each([])("") 19 | if (!$t.isMemberExpression(node)) return isLiteralNamed(node.object, names); 20 | 21 | // Very advanced describe.skip.each([])("") or test.only.each([])("") 22 | return isLiteralNamed(node.object.object, names); 23 | } 24 | 25 | return false; 26 | } 27 | 28 | function isNameEquivalent(node, expected) { 29 | const rawValue = node.value; 30 | // Wildcard all special formatting values of Jest 31 | const regex = new RegExp( 32 | escapeStringRegexp(rawValue).replace(/%[psdifjo#]/g, '.*') 33 | ); 34 | 35 | return regex.test(expected); 36 | } 37 | 38 | export function findTestIn(ast, { $traverse = traverse } = {}) { 39 | return function findTest(expectedDescribeTitle, expectedTestTitle) { 40 | let resolved = false; 41 | 42 | $traverse(ast, { 43 | CallExpression(parentPath) { 44 | const { node: { callee: describe, arguments: [ describeTitle ] } } = parentPath; 45 | 46 | parentPath.stop(); 47 | 48 | if (!isLiteralNamed(describe, 'describe')) return; 49 | if (!isNameEquivalent(describeTitle, expectedDescribeTitle)) return; 50 | 51 | parentPath.traverse({ 52 | CallExpression(childPath) { 53 | const { node: { callee: test, loc: location, arguments: [ testTitle ] } } = childPath; 54 | 55 | if(!isLiteralNamed(test, ['test', 'it'])) return; 56 | if(!isNameEquivalent(testTitle, expectedTestTitle)) return; 57 | 58 | childPath.stop(); 59 | 60 | resolved = location; 61 | } 62 | }); 63 | } 64 | }); 65 | 66 | return resolved; 67 | } 68 | } 69 | 70 | export function parseJs(file, fileType) { 71 | const fileTypes = { 72 | jsx: [ 'jsx' ], 73 | tsx: [ 'typescript', 'jsx' ], 74 | ts: [ 'typescript' ] 75 | }; 76 | 77 | const config = { 78 | sourceType: 'module', 79 | plugins: fileTypes[fileType] || [] 80 | } 81 | 82 | return parse(file, config); 83 | } 84 | 85 | // Internal Dependencies 86 | export { 87 | isLiteralNamed as $_isLiteralNamed, 88 | isNameEquivalent as $_isNameEquivalent 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /tasks/github-api.js: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github'; 2 | 3 | // DEV: If we're on a PR, use the sha from the payload to prevent Ghost Check Runs 4 | const retrieveHeadSHA = ($github) => { 5 | if ($github.context.payload.pull_request) { 6 | return $github.context.payload.pull_request.head.sha; 7 | } 8 | 9 | return $github.context.sha; 10 | } 11 | 12 | async function createCheckWithAnnotations( 13 | { conclusion, summary, annotations }, 14 | { $octokit, $config, $github = github } 15 | ) { 16 | const checkRequest = { 17 | ...$github.context.repo, 18 | head_sha: retrieveHeadSHA($github), 19 | name: $config.checkName, 20 | conclusion, 21 | output: { 22 | title: 'Jest Test Results', 23 | summary, 24 | annotations 25 | } 26 | }; 27 | 28 | try { 29 | await $octokit.checks.create(checkRequest); 30 | } catch (error) { 31 | throw new Error(`Request to create annotations failed - request: ${ JSON.stringify(checkRequest) } - error: ${ error.message } `); 32 | } 33 | } 34 | 35 | async function updateCheckWithAnnotations(annotations, { $octokit, $github = github }) { 36 | const updateCheckRequest = { 37 | ...$github.context.repo, 38 | head_sha: retrieveHeadSHA($github), 39 | output: { annotations } 40 | }; 41 | 42 | try { 43 | await $octokit.checks.update(updateCheckRequest); 44 | } catch (error) { 45 | throw new Error(`Request to update annotations failed - request: ${ JSON.stringify(updateCheckRequest) } - error: ${ error.message } `); 46 | } 47 | } 48 | 49 | export async function publishTestResults(testInformation, { $github = github, $config }) { 50 | const zeroAnnotations = 0; 51 | const maximumAnnotations = 50; 52 | 53 | const { details, time, passed, failed, total, conclusion, annotations } = testInformation; 54 | 55 | const octokit = new $github.GitHub($config.accessToken); 56 | 57 | await createCheckWithAnnotations({ 58 | summary: 59 | '#### These are all the test results I was able to find from your jest-junit reporter\n' + 60 | `**${ total }** tests were completed in **${ time }s** with **${ passed }** passed ✔ and **${ failed }** failed ✖ tests.` + 61 | `${ details ? `\n\n${ details }` : '' }`, 62 | conclusion, 63 | annotations: annotations.slice(zeroAnnotations, maximumAnnotations) 64 | }, { $octokit: octokit, $config }); 65 | 66 | let batchedAnnotations = annotations.slice(maximumAnnotations); 67 | 68 | while (batchedAnnotations.length > zeroAnnotations) { 69 | await updateCheckWithAnnotations({ 70 | annotations: batchedAnnotations.slice(zeroAnnotations, maximumAnnotations) 71 | }, { $octokit: octokit }); 72 | 73 | batchedAnnotations = batchedAnnotations.slice(maximumAnnotations); 74 | } 75 | } 76 | 77 | // Internal Dependencies 78 | export { 79 | createCheckWithAnnotations as $_createCheckWithAnnotations, 80 | updateCheckWithAnnotations as $_updateCheckWithAnnotations 81 | } 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jest Github Actions Reporter 2 | 3 | Creates annotations based on the output of `jest-junit` (see [how to configure `jest-junit` properly](./README.md#jest-junit-configuration)) in your test files. 4 | 5 | ## Example 6 | 7 | .github/workflows/your-workflow.yml 8 | ``` 9 | - uses: IgnusG/jest-report-action@v{ current version } 10 | if: always() # Or use "continue-on-error: true" in previous test step 11 | with: 12 | access-token: ${{ secrets.GITHUB_TOKEN }} 13 | ``` 14 | 15 | Example of Jest Annotations 16 | 17 | ## Inputs 18 | 19 | ### `access-token` - **required** 20 | 21 | We'll need that to enrich the actions run with annotations. The secret is automatically generated by github.com. 22 | 23 | ### `junit-file` - *optional* 24 | 25 | The location and/or the name of the JUnit file. `jest-junit` uses junit.xml as a default, which is the default here too. If you haven't changed it, you can omit this input. 26 | 27 | ### `run-name` - *optional* 28 | 29 | The name of your run. This is typically `build` but can be configured individually. Make sure it matches your workflow config: 30 | 31 | .github/workflows/your-workflow.yml 32 | ```yaml 33 | build: # <- this one! 34 | steps: 35 | ... 36 | ``` 37 | 38 | ### `check-name` - *optional* 39 | 40 | A custom name that will appear in the PR's check window. Useful if you have multiple test suites - code, pipelines, publishing process etc., each run with separate jest commands. 41 | Be sure to run this action after each jest run, that way you can use the same name for the JUnit file - or omit it. 42 | 43 | Otherwise you have to give each generated JUnit file a unique name and pass it to `jest-report-action`. 44 | 45 | ### `working-directory` - *optional* 46 | 47 | The working directory, where the `junit-file`, as well as the sources (of test files) can be found. The default is the root directory of your project. 48 | 49 | ## `jest-junit` Configuration 50 | 51 | Have a look at how to call `jest-junit` in your workflows in the [documentation](https://www.npmjs.com/package/jest-junit#usage). 52 | A very simple example is calling jest with a custom `--reporters` parameter: 53 | 54 | package.json and other 55 | ``` 56 | jest --ci --reporters=default --reporters=jest-junit 57 | ``` 58 | 59 | To provide **correct annotation locations**, you need to configure `jest-junit` to format the xml file in a compatible way. 60 | 61 | Set these in either your package.json or through enviornment variables while running jest in your CI (Github). 62 | 63 | package.json 64 | ```json 65 | "jest-junit": { 66 | "suiteNameTemplate": "{filepath}", 67 | "classNameTemplate": "{classname}", 68 | "titleTemplate": "{title}" 69 | } 70 | ``` 71 | 72 | Refer to [`jest-junit` Documentation](https://www.npmjs.com/package/jest-junit#configuration) to see other ways to configure these. 73 | 74 | Thank you and have an **amazing day**! 75 | -------------------------------------------------------------------------------- /tasks/annotations.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | function cleanStackWithRelativePaths(stacktrace) { 4 | const filters = [ 5 | line => { 6 | // Remove jasmin stacks 7 | if (line.includes('node_modules/jest-jasmin')) return false; 8 | // Remove process queue stacks 9 | if (line.includes('internal/process/task_queues')) return false; 10 | // Remove empty promises 11 | // eslint-disable-next-line no-useless-escape 12 | if (line.trimStart() === 'at new Promise (\)') return false; 13 | 14 | return line; 15 | }, 16 | line => { 17 | const { groups: { file } } = (/.*\((?.*)\).?/).exec(line) || { groups: { file: false } }; 18 | 19 | return file 20 | ? line.replace(file, path.relative(process.cwd(), file)) 21 | : line; 22 | } 23 | ]; 24 | 25 | const applyFilters = line => filters.reduce((result, filter) => filter(result), line); 26 | const isNotEmpty = line => line !== false; 27 | 28 | 29 | return stacktrace 30 | .map(applyFilters) 31 | .filter(isNotEmpty); 32 | } 33 | 34 | function formatJestMessage(message) { 35 | const messageLines = message.split('\n'); 36 | 37 | // Skip first line (title) and one blank line 38 | const expectationStart = 2; 39 | const filterStacktrace = line => line.trimStart().startsWith('at '); 40 | 41 | try { 42 | const [ title ] = messageLines; 43 | 44 | const expectations = messageLines 45 | .slice(expectationStart) 46 | .filter(line => !filterStacktrace(line)) 47 | .join('\n'); 48 | 49 | const stacktrace = messageLines.filter(filterStacktrace); 50 | 51 | return { 52 | title, 53 | expectations, 54 | stacktrace: `Stacktrace:\n${ cleanStackWithRelativePaths(stacktrace).join('\n') }` 55 | } 56 | } catch(error) { 57 | console.error(`Failed to parse - falling back to "stupid" mode - error: ${ error.message }`); 58 | 59 | return { title: 'Test Failed', expectations: 'A fix a day keeps the debugger away...', stacktrace: message }; 60 | } 61 | } 62 | 63 | export function createAnnotation({ path: filePath }, testcase, location) { 64 | const { describe, test, failure: [ message ] } = testcase; 65 | 66 | const { title, expectations, stacktrace } = formatJestMessage(message); 67 | 68 | let annotation = { 69 | path: filePath, 70 | title: `${ describe } > ${ test }`, 71 | start_line: location.start.line, 72 | end_line: location.end.line, 73 | annotation_level: 'failure', 74 | message: `${ title }\n${ expectations }\n\n${ stacktrace }` 75 | }; 76 | 77 | if (location.start.line === location.end.line) { 78 | annotation = { 79 | ...annotation, 80 | start_column: location.start.column, 81 | end_column: location.end.column 82 | }; 83 | } 84 | 85 | return annotation; 86 | } 87 | 88 | // Internal Dependencies 89 | export { 90 | cleanStackWithRelativePaths as $_cleanStackWithRelativePaths, 91 | formatJestMessage as $_formatJestMessage 92 | }; 93 | 94 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es6': true, 4 | 'node': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'globals': { 8 | 'Atomics': 'readonly', 9 | 'SharedArrayBuffer': 'readonly' 10 | }, 11 | 'parserOptions': { 12 | 'ecmaVersion': 2018, 13 | 'sourceType': 'module' 14 | }, 15 | 'rules': { 16 | 'accessor-pairs': 'error', 17 | 'array-bracket-newline': 'error', 18 | 'array-bracket-spacing': 'off', 19 | 'array-callback-return': 'error', 20 | 'array-element-newline': 'off', 21 | 'arrow-body-style': 'error', 22 | 'arrow-parens': 'off', 23 | 'arrow-spacing': [ 24 | 'error', 25 | { 26 | 'after': true, 27 | 'before': true 28 | } 29 | ], 30 | 'block-scoped-var': 'error', 31 | 'block-spacing': 'error', 32 | 'brace-style': 'error', 33 | 'callback-return': 'error', 34 | 'capitalized-comments': [ 35 | 'error', 36 | 'always' 37 | ], 38 | 'class-methods-use-this': 'error', 39 | 'comma-dangle': 'error', 40 | 'comma-spacing': [ 41 | 'error', 42 | { 43 | 'after': true, 44 | 'before': false 45 | } 46 | ], 47 | 'comma-style': [ 48 | 'error', 49 | 'last' 50 | ], 51 | 'complexity': 'error', 52 | 'computed-property-spacing': 'error', 53 | 'consistent-return': 'error', 54 | 'consistent-this': 'error', 55 | 'curly': 'off', 56 | 'default-case': 'error', 57 | 'default-param-last': 'error', 58 | 'dot-location': [ 59 | 'error', 60 | 'property' 61 | ], 62 | 'dot-notation': [ 63 | 'error', 64 | { 65 | 'allowKeywords': true 66 | } 67 | ], 68 | 'eol-last': 'error', 69 | 'eqeqeq': 'error', 70 | 'func-call-spacing': 'error', 71 | 'func-name-matching': 'error', 72 | 'func-names': 'error', 73 | 'func-style': [ 74 | 'error', 75 | 'declaration', 76 | { 77 | 'allowArrowFunctions': true 78 | } 79 | ], 80 | 'function-paren-newline': 'off', 81 | 'generator-star-spacing': 'error', 82 | 'global-require': 'error', 83 | 'grouped-accessor-pairs': 'error', 84 | 'guard-for-in': 'error', 85 | 'handle-callback-err': 'error', 86 | 'id-blacklist': 'error', 87 | 'id-length': 'error', 88 | 'id-match': 'error', 89 | 'implicit-arrow-linebreak': [ 90 | 'error', 91 | 'beside' 92 | ], 93 | 'indent': 'off', 94 | 'indent-legacy': 'off', 95 | 'init-declarations': 'error', 96 | 'jsx-quotes': 'error', 97 | 'key-spacing': 'error', 98 | 'keyword-spacing': 'off', 99 | 'line-comment-position': 'error', 100 | 'linebreak-style': [ 101 | 'error', 102 | 'unix' 103 | ], 104 | 'lines-around-comment': 'error', 105 | 'lines-around-directive': 'error', 106 | 'lines-between-class-members': 'error', 107 | 'max-classes-per-file': 'error', 108 | 'max-depth': 'error', 109 | 'max-len': 'off', 110 | 'max-lines': 'error', 111 | 'max-lines-per-function': 'error', 112 | 'max-nested-callbacks': 'error', 113 | 'max-params': 'error', 114 | 'max-statements': 'off', 115 | 'max-statements-per-line': 'error', 116 | 'multiline-comment-style': 'error', 117 | 'multiline-ternary': [ 118 | 'error', 119 | 'always-multiline' 120 | ], 121 | 'new-cap': 'error', 122 | 'new-parens': 'error', 123 | 'newline-after-var': [ 124 | 'error', 125 | 'always' 126 | ], 127 | 'newline-before-return': 'error', 128 | 'newline-per-chained-call': 'error', 129 | 'no-alert': 'error', 130 | 'no-array-constructor': 'error', 131 | 'no-await-in-loop': 'off', 132 | 'no-bitwise': 'error', 133 | 'no-buffer-constructor': 'error', 134 | 'no-caller': 'error', 135 | 'no-catch-shadow': 'error', 136 | 'no-confusing-arrow': 'off', 137 | 'no-console': 'off', 138 | 'no-constructor-return': 'error', 139 | 'no-continue': 'off', 140 | 'no-div-regex': 'error', 141 | 'no-dupe-else-if': 'error', 142 | 'no-duplicate-imports': 'error', 143 | 'no-else-return': 'error', 144 | 'no-empty-function': 'error', 145 | 'no-eq-null': 'error', 146 | 'no-eval': 'error', 147 | 'no-extend-native': 'error', 148 | 'no-extra-bind': 'error', 149 | 'no-extra-label': 'error', 150 | 'no-extra-parens': 'error', 151 | 'no-floating-decimal': 'error', 152 | 'no-implicit-coercion': 'error', 153 | 'no-implicit-globals': 'error', 154 | 'no-implied-eval': 'error', 155 | 'no-import-assign': 'error', 156 | 'no-inline-comments': 'error', 157 | 'no-invalid-this': 'error', 158 | 'no-iterator': 'error', 159 | 'no-label-var': 'error', 160 | 'no-labels': 'error', 161 | 'no-lone-blocks': 'error', 162 | 'no-lonely-if': 'error', 163 | 'no-loop-func': 'error', 164 | 'no-magic-numbers': 'error', 165 | 'no-mixed-operators': 'error', 166 | 'no-mixed-requires': 'error', 167 | 'no-multi-assign': 'error', 168 | 'no-multi-spaces': 'error', 169 | 'no-multi-str': 'error', 170 | 'no-multiple-empty-lines': 'error', 171 | 'no-native-reassign': 'error', 172 | 'no-negated-condition': 'off', 173 | 'no-negated-in-lhs': 'error', 174 | 'no-nested-ternary': 'error', 175 | 'no-new': 'error', 176 | 'no-new-func': 'error', 177 | 'no-new-object': 'error', 178 | 'no-new-require': 'error', 179 | 'no-new-wrappers': 'error', 180 | 'no-octal-escape': 'error', 181 | 'no-param-reassign': 'error', 182 | 'no-path-concat': 'error', 183 | 'no-plusplus': 'error', 184 | 'no-process-env': 'error', 185 | 'no-process-exit': 'error', 186 | 'no-proto': 'error', 187 | 'no-restricted-globals': 'error', 188 | 'no-restricted-imports': 'error', 189 | 'no-restricted-modules': 'error', 190 | 'no-restricted-properties': 'error', 191 | 'no-restricted-syntax': 'error', 192 | 'no-return-assign': 'error', 193 | 'no-return-await': 'error', 194 | 'no-script-url': 'error', 195 | 'no-self-compare': 'error', 196 | 'no-sequences': 'error', 197 | 'no-setter-return': 'error', 198 | 'no-shadow': 'error', 199 | 'no-spaced-func': 'error', 200 | 'no-sync': 'error', 201 | 'no-tabs': 'error', 202 | 'no-template-curly-in-string': 'error', 203 | 'no-ternary': 'off', 204 | 'no-throw-literal': 'error', 205 | 'no-trailing-spaces': 'off', 206 | 'no-undef-init': 'error', 207 | 'no-undefined': 'off', 208 | 'no-underscore-dangle': 'error', 209 | 'no-unmodified-loop-condition': 'error', 210 | 'no-unneeded-ternary': 'error', 211 | 'no-unused-expressions': 'error', 212 | 'no-use-before-define': 'error', 213 | 'no-useless-call': 'error', 214 | 'no-useless-computed-key': 'error', 215 | 'no-useless-concat': 'error', 216 | 'no-useless-constructor': 'error', 217 | 'no-useless-rename': 'error', 218 | 'no-useless-return': 'error', 219 | 'no-var': 'error', 220 | 'no-void': 'error', 221 | 'no-warning-comments': 'error', 222 | 'no-whitespace-before-property': 'error', 223 | 'nonblock-statement-body-position': 'error', 224 | 'object-curly-newline': 'error', 225 | 'object-curly-spacing': [ 226 | 'error', 227 | 'always' 228 | ], 229 | 'object-property-newline': 'off', 230 | 'object-shorthand': 'error', 231 | 'one-var': 'off', 232 | 'one-var-declaration-per-line': 'error', 233 | 'operator-assignment': 'error', 234 | 'operator-linebreak': 'error', 235 | 'padded-blocks': 'off', 236 | 'padding-line-between-statements': 'error', 237 | 'prefer-arrow-callback': 'error', 238 | 'prefer-const': 'off', 239 | 'prefer-exponentiation-operator': 'error', 240 | 'prefer-named-capture-group': 'error', 241 | 'prefer-numeric-literals': 'error', 242 | 'prefer-object-spread': 'error', 243 | 'prefer-promise-reject-errors': 'error', 244 | 'prefer-reflect': 'error', 245 | 'prefer-regex-literals': 'error', 246 | 'prefer-rest-params': 'error', 247 | 'prefer-spread': 'error', 248 | 'prefer-template': 'error', 249 | 'quote-props': 'off', 250 | 'quotes': [ 251 | 'error', 252 | 'single' 253 | ], 254 | 'radix': 'error', 255 | 'require-atomic-updates': 'error', 256 | 'require-await': 'error', 257 | 'require-jsdoc': 'off', 258 | 'require-unicode-regexp': 'off', 259 | 'rest-spread-spacing': [ 260 | 'error', 261 | 'never' 262 | ], 263 | 'semi': 'off', 264 | 'semi-spacing': 'error', 265 | 'semi-style': [ 266 | 'error', 267 | 'last' 268 | ], 269 | 'sort-keys': 'off', 270 | 'sort-vars': 'error', 271 | 'space-before-blocks': 'error', 272 | 'space-before-function-paren': 'off', 273 | 'space-in-parens': [ 274 | 'error', 275 | 'never' 276 | ], 277 | 'space-infix-ops': 'error', 278 | 'space-unary-ops': 'error', 279 | 'spaced-comment': [ 280 | 'error', 281 | 'always' 282 | ], 283 | 'strict': 'error', 284 | 'switch-colon-spacing': 'error', 285 | 'symbol-description': 'error', 286 | 'template-curly-spacing': [ 287 | 'error', 288 | 'always' 289 | ], 290 | 'template-tag-spacing': 'error', 291 | 'unicode-bom': [ 292 | 'error', 293 | 'never' 294 | ], 295 | 'valid-jsdoc': 'error', 296 | 'vars-on-top': 'error', 297 | 'wrap-iife': 'error', 298 | 'wrap-regex': 'error', 299 | 'yield-star-spacing': 'error', 300 | 'yoda': [ 301 | 'error', 302 | 'never' 303 | ] 304 | } 305 | }; 306 | --------------------------------------------------------------------------------