├── .eslintignore
├── .gitattributes
├── .github
├── dependabot.yml
├── linters
│ ├── .eslintrc.yml
│ ├── .yaml-lint.yml
│ └── tsconfig.json
└── workflows
│ ├── check-dist.yml
│ ├── ci.yml
│ ├── codeql.yml
│ └── linter.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── __fixtures__
│ ├── report-invalid.json
│ ├── report-nested.json
│ ├── report-sharded.json
│ ├── report-valid.json
│ └── report-without-duration.json
├── __snapshots__
│ └── report.test.ts.snap
├── formatting.test.ts
├── fs.test.ts
├── github.test.ts
├── icons.test.ts
├── index.test.ts
└── report.test.ts
├── action.yml
├── assets
├── comment-failed.png
└── comment-passed.png
├── dist
├── index.js
└── licenses.txt
├── package-lock.json
├── package.json
├── src
├── formatting.ts
├── fs.ts
├── github.ts
├── icons.ts
├── index.ts
└── report.ts
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib/
2 | dist/
3 | node_modules/
4 | coverage/
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | dist/** -diff linguist-generated=true
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: /
5 | labels:
6 | - dependencies
7 | schedule:
8 | interval: monthly
9 | - package-ecosystem: github-actions
10 | directory: /
11 | labels:
12 | - dependencies
13 | schedule:
14 | interval: monthly
15 |
--------------------------------------------------------------------------------
/.github/linters/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | node: true
3 | es6: true
4 | jest: true
5 |
6 | globals:
7 | Atomics: readonly
8 | SharedArrayBuffer: readonly
9 |
10 | ignorePatterns:
11 | - '!.*'
12 | - '**/node_modules/.*'
13 | - '**/dist/.*'
14 | - '**/coverage/.*'
15 | - '*.json'
16 |
17 | parser: '@typescript-eslint/parser'
18 |
19 | parserOptions:
20 | ecmaVersion: 2023
21 | sourceType: module
22 | project:
23 | - './.github/linters/tsconfig.json'
24 | - './tsconfig.json'
25 |
26 | plugins:
27 | - jest
28 | - '@typescript-eslint'
29 |
30 | extends:
31 | - eslint:recommended
32 | - plugin:@typescript-eslint/eslint-recommended
33 | - plugin:@typescript-eslint/recommended
34 | - plugin:github/recommended
35 | - plugin:jest/recommended
36 |
37 | rules:
38 | {
39 | 'camelcase': 'off',
40 | 'eslint-comments/no-use': 'off',
41 | 'eslint-comments/no-unused-disable': 'off',
42 | 'i18n-text/no-en': 'off',
43 | 'import/no-namespace': 'off',
44 | 'no-console': 'off',
45 | 'no-unused-vars': 'off',
46 | 'prettier/prettier': 'error',
47 | 'semi': 'off',
48 | 'no-shadow': 'off',
49 | 'github/no-then': 'off',
50 | 'no-irregular-whitespace': 'off',
51 | '@typescript-eslint/array-type': 'error',
52 | '@typescript-eslint/await-thenable': 'error',
53 | '@typescript-eslint/ban-ts-comment': 'error',
54 | '@typescript-eslint/consistent-type-assertions': 'error',
55 | '@typescript-eslint/explicit-member-accessibility':
56 | ['error', { 'accessibility': 'no-public' }],
57 | '@typescript-eslint/explicit-function-return-type':
58 | ['error', { 'allowExpressions': true }],
59 | '@typescript-eslint/func-call-spacing': ['error', 'never'],
60 | '@typescript-eslint/no-array-constructor': 'error',
61 | '@typescript-eslint/no-empty-interface': 'error',
62 | '@typescript-eslint/no-explicit-any': 'warn',
63 | '@typescript-eslint/no-extraneous-class': 'error',
64 | '@typescript-eslint/no-for-in-array': 'error',
65 | '@typescript-eslint/no-inferrable-types': 'error',
66 | '@typescript-eslint/no-misused-new': 'error',
67 | '@typescript-eslint/no-namespace': 'error',
68 | '@typescript-eslint/no-non-null-assertion': 'warn',
69 | '@typescript-eslint/no-require-imports': 'error',
70 | '@typescript-eslint/no-unnecessary-qualifier': 'error',
71 | '@typescript-eslint/no-unnecessary-type-assertion': 'error',
72 | '@typescript-eslint/no-unused-vars': 'error',
73 | '@typescript-eslint/no-useless-constructor': 'error',
74 | '@typescript-eslint/no-var-requires': 'error',
75 | '@typescript-eslint/prefer-for-of': 'warn',
76 | '@typescript-eslint/prefer-function-type': 'warn',
77 | '@typescript-eslint/prefer-includes': 'error',
78 | '@typescript-eslint/prefer-string-starts-ends-with': 'error',
79 | '@typescript-eslint/promise-function-async': 'error',
80 | '@typescript-eslint/require-array-sort-compare': 'error',
81 | '@typescript-eslint/restrict-plus-operands': 'error',
82 | '@typescript-eslint/semi': ['error', 'never'],
83 | '@typescript-eslint/space-before-function-paren': 'off',
84 | '@typescript-eslint/type-annotation-spacing': 'error',
85 | '@typescript-eslint/unbound-method': 'error'
86 | }
87 |
--------------------------------------------------------------------------------
/.github/linters/.yaml-lint.yml:
--------------------------------------------------------------------------------
1 | rules:
2 | document-end: disable
3 | document-start:
4 | level: warning
5 | present: false
6 | line-length:
7 | level: warning
8 | max: 80
9 | allow-non-breakable-words: true
10 | allow-non-breakable-inline-mappings: true
11 |
--------------------------------------------------------------------------------
/.github/linters/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "noEmit": true
6 | },
7 | "include": ["../../__tests__/**/*", "../../src/**/*"],
8 | "exclude": ["../../dist", "../../node_modules", "../../coverage", "*.json"]
9 | }
10 |
--------------------------------------------------------------------------------
/.github/workflows/check-dist.yml:
--------------------------------------------------------------------------------
1 | # In TypeScript actions, `dist/index.js` is a special file. When you reference
2 | # an action with `uses:`, `dist/index.js` is the code that will be run. For this
3 | # project, the `dist/index.js` file is generated from other source files through
4 | # the build process. We need to make sure that the checked-in `dist/index.js`
5 | # file matches what is expected from the build.
6 | #
7 | # This workflow will fail if the checked-in `dist/index.js` file does not match
8 | # what is expected from the build.
9 |
10 | name: Check dist
11 |
12 | on:
13 | push:
14 | branches:
15 | - main
16 | paths-ignore:
17 | - '**.md'
18 | pull_request:
19 | paths-ignore:
20 | - '**.md'
21 | workflow_dispatch:
22 |
23 | jobs:
24 | check-dist:
25 | name: Check dist/
26 | runs-on: ubuntu-latest
27 |
28 | permissions:
29 | contents: read
30 | statuses: write
31 |
32 | steps:
33 | - name: Checkout
34 | id: checkout
35 | uses: actions/checkout@v4
36 |
37 | - name: Setup Node.js
38 | uses: actions/setup-node@v3
39 | with:
40 | node-version: 18
41 | cache: npm
42 |
43 | - name: Install Dependencies
44 | id: install
45 | run: npm ci
46 |
47 | - name: Build dist/ Directory
48 | id: build
49 | run: npm run bundle
50 |
51 | - name: Compare Expected and Actual Directories
52 | id: diff
53 | run: |
54 | if [ "$(git diff --ignore-space-at-eol --text dist/ | wc -l)" -gt "0" ]; then
55 | echo "Detected uncommitted changes after build. See status below:"
56 | git diff --ignore-space-at-eol --text dist/
57 | exit 1
58 | fi
59 |
60 | # If index.js was different than expected, upload the expected version as
61 | # a workflow artifact.
62 | - uses: actions/upload-artifact@v4
63 | if: ${{ failure() && steps.diff.conclusion == 'failure' }}
64 | with:
65 | name: dist
66 | path: dist/
67 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 | - 'releases/*'
9 |
10 | jobs:
11 | test-typescript:
12 | name: TypeScript Tests
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Setup Node.js
20 | id: setup-node
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: 18
24 | cache: npm
25 |
26 | - name: Install Dependencies
27 | id: npm-ci
28 | run: npm ci
29 |
30 | - name: Check Format
31 | id: npm-format-check
32 | run: npm run format:check
33 |
34 | - name: Lint
35 | id: npm-lint
36 | run: npm run lint
37 |
38 | # - name: Test
39 | # id: npm-ci-test
40 | # run: npm run ci-test
41 |
42 | # test-action:
43 | # name: GitHub Actions Test
44 | # runs-on: ubuntu-latest
45 |
46 | # steps:
47 | # - name: Checkout
48 | # uses: actions/checkout@v4
49 |
50 | # - name: Test Local Action
51 | # id: test-action
52 | # uses: ./
53 | # with:
54 | # milliseconds: 1000
55 |
56 | # - name: Print Output
57 | # id: output
58 | # run: echo "${{ steps.test-action.outputs.time }}"
59 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: CodeQL
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | schedule:
11 | - cron: '31 7 * * 3'
12 |
13 | jobs:
14 | analyze:
15 | name: Analyze
16 | runs-on: ubuntu-latest
17 |
18 | permissions:
19 | actions: read
20 | checks: write
21 | contents: read
22 | security-events: write
23 |
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | language:
28 | - TypeScript
29 |
30 | steps:
31 | - name: Checkout
32 | id: checkout
33 | uses: actions/checkout@v4
34 |
35 | - name: Initialize CodeQL
36 | id: initialize
37 | uses: github/codeql-action/init@v2
38 | with:
39 | languages: ${{ matrix.language }}
40 | source-root: src
41 |
42 | - name: Autobuild
43 | id: autobuild
44 | uses: github/codeql-action/autobuild@v2
45 |
46 | - name: Perform CodeQL Analysis
47 | id: analyze
48 | uses: github/codeql-action/analyze@v2
49 |
--------------------------------------------------------------------------------
/.github/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches-ignore:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | name: Lint Code Base
14 | runs-on: ubuntu-latest
15 |
16 | permissions:
17 | contents: read
18 | packages: read
19 | statuses: write
20 |
21 | steps:
22 | - name: Checkout
23 | id: checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Setup Node.js
27 | id: setup-node
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 18
31 | cache: npm
32 |
33 | - name: Install Dependencies
34 | id: install
35 | run: npm ci
36 |
37 | - name: Lint Code Base
38 | id: super-linter
39 | uses: super-linter/super-linter/slim@v5
40 | env:
41 | DEFAULT_BRANCH: main
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | TYPESCRIPT_DEFAULT_STYLE: prettier
44 | VALIDATE_JSCPD: false
45 | VALIDATE_MARKDOWN: false
46 | VALIDATE_NATURAL_LANGUAGE: false
47 |
--------------------------------------------------------------------------------
/.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 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | jspm_packages/
46 |
47 | # TypeScript v1 declaration files
48 | typings/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Optional REPL history
60 | .node_repl_history
61 |
62 | # Output of 'npm pack'
63 | *.tgz
64 |
65 | # Yarn Integrity file
66 | .yarn-integrity
67 |
68 | # dotenv environment variables file
69 | .env
70 | .env.test
71 |
72 | # parcel-bundler cache (https://parceljs.org/)
73 | .cache
74 |
75 | # next.js build output
76 | .next
77 |
78 | # nuxt.js build output
79 | .nuxt
80 |
81 | # vuepress build output
82 | .vuepress/dist
83 |
84 | # Serverless directories
85 | .serverless/
86 |
87 | # FuseBox cache
88 | .fusebox/
89 |
90 | # DynamoDB Local files
91 | .dynamodb/
92 |
93 | # OS metadata
94 | .DS_Store
95 | Thumbs.db
96 |
97 | # Ignore built ts files
98 | __tests__/runner/*
99 |
100 | # IDE files
101 | .idea
102 | .vscode
103 | *.code-workspace
104 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | coverage/
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 2,
4 | "useTabs": true,
5 | "semi": false,
6 | "singleQuote": true,
7 | "quoteProps": "consistent",
8 | "trailingComma": "none",
9 | "bracketSpacing": true,
10 | "arrowParens": "always",
11 | "proseWrap": "always",
12 | "htmlWhitespaceSensitivity": "css",
13 | "endOfLine": "lf"
14 | }
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [3.9.0] - 2025-04-26
4 |
5 | - Add output variable for raw report data (@seongminn)
6 |
7 | ## [3.8.0] - 2025-01-30
8 |
9 | - Allow using a different action for creating and updating the comments
10 |
11 | ## [3.7.0] - 2024-11-25
12 |
13 | - Add option to display a footer with additional content below the summary (@mskelton)
14 |
15 | ## [3.6.0] - 2024-10-05
16 |
17 | - Add option to display npm command for re-running failed tests (@mskelton)
18 | - Render commit sha as clickable link to commit page (@mskelton)
19 |
20 | ## [3.5.2] - 2024-07-08
21 |
22 | - Update action input docs
23 |
24 | ## [3.5.1] - 2024-07-07
25 |
26 | - Display correct commit sha when triggered from pull request comment
27 |
28 | ## [3.5.0] - 2024-07-07
29 |
30 | - Support pull request comment as workflow trigger
31 |
32 | ## [3.4.0] - 2024-07-02
33 |
34 | - Add ability to display custom information by @fungairino
35 |
36 | ## [3.3.0] - 2024-04-21
37 |
38 | - Add support for indefinitely nested specs
39 | - Switch from `→` to `›` for joining test paths
40 |
41 | ## [3.2.0] - 2024-04-11
42 |
43 | - Allow all workflow event types
44 |
45 | ## [3.1.0] - 2024-04-11
46 |
47 | - Support manual workflow dispatch
48 |
49 | ## [3.0.1] - 2024-04-02
50 |
51 | - Bump dependencies
52 |
53 | ## [3.0.0] - 2023-12-13
54 |
55 | - Distinguish between reports by workflow name
56 | - Distinguish between reports by new optional `report-tag` input
57 | - Allow comments by non-bot users
58 |
59 | ## [2.1.2] - 2023-09-26
60 |
61 | - Calculate total duration if missing from Playwright report
62 | - Only display fractions for sub-minute durations
63 |
64 | ## [2.1.1] - 2023-09-10
65 |
66 | - Add `job-summary` input var
67 |
68 | ## [2.1.0] - 2023-09-10
69 |
70 | - Create job summary
71 |
72 | ## [2.0.0] - 2023-09-09
73 |
74 | - Rename action
75 | - Create output var for rendered summary
76 |
77 | ## [1.1.2] - 2023-09-09
78 |
79 | - Display report link
80 |
81 | ## [1.1.1] - 2023-09-09
82 |
83 | - Correctly count sharded tests
84 |
85 | ## [1.1.0] - 2023-09-08
86 |
87 | - Improve formatting
88 |
89 | ## [1.0.0] - 2023-09-07
90 |
91 | - Initial release
92 |
93 | [3.9.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.9.0
94 | [3.8.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.8.0
95 | [3.7.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.7.0
96 | [3.6.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.6.0
97 | [3.5.2]: https://github.com/daun/playwright-report-summary/releases/tag/v3.5.2
98 | [3.5.1]: https://github.com/daun/playwright-report-summary/releases/tag/v3.5.1
99 | [3.5.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.5.0
100 | [3.4.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.4.0
101 | [3.3.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.3.0
102 | [3.2.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.2.0
103 | [3.1.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.1.0
104 | [3.0.1]: https://github.com/daun/playwright-report-summary/releases/tag/v3.0.1
105 | [3.0.0]: https://github.com/daun/playwright-report-summary/releases/tag/v3.0.0
106 | [2.1.2]: https://github.com/daun/playwright-report-summary/releases/tag/v2.1.2
107 | [2.1.1]: https://github.com/daun/playwright-report-summary/releases/tag/v2.1.1
108 | [2.1.0]: https://github.com/daun/playwright-report-summary/releases/tag/v2.1.0
109 | [2.0.0]: https://github.com/daun/playwright-report-summary/releases/tag/v2.0.0
110 | [1.1.2]: https://github.com/daun/playwright-report-summary/releases/tag/v1.1.2
111 | [1.1.1]: https://github.com/daun/playwright-report-summary/releases/tag/v1.1.1
112 | [1.1.0]: https://github.com/daun/playwright-report-summary/releases/tag/v1.1.0
113 | [1.0.0]: https://github.com/daun/playwright-report-summary/releases/tag/v1.0.0
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2023 Philipp Daun
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  Playwright Report Summary
2 |
3 | [](https://github.com/super-linter/super-linter)
4 | 
5 |
6 | A GitHub action to report Playwright test results as a pull request comment.
7 |
8 | - Parse the JSON test report generated by Playwright
9 | - Generate a markdown summary of the test results
10 | - Post the summary as a pull request comment
11 | - Uses GitHub's official [icons](https://primer.style/design/foundations/icons) and [color scheme](https://primer.style/design/foundations/color)
12 |
13 | ## Examples
14 |
15 |
16 |
17 |
18 |
19 | ## Usage
20 |
21 | ### Basic usage
22 |
23 | Playwright must be configured to [generate a JSON report](https://playwright.dev/docs/test-reporters#json-reporter)
24 | and write it to disk. This action receives the report file path as input, in this case `results.json`.
25 |
26 | Note the `if: always()` to ensure the report comment is created even if the tests failed.
27 |
28 | ```yaml
29 | jobs:
30 | test:
31 | name: Run playwright tests
32 | needs: install
33 | steps:
34 | - uses: actions/checkout@v4
35 | - uses: actions/setup-node@v3
36 | with:
37 | node-version: 20
38 |
39 | - run: PLAYWRIGHT_JSON_OUTPUT_NAME=results.json npx playwright test --reporter=json
40 |
41 | - uses: daun/playwright-report-summary@v3
42 | if: always()
43 | with:
44 | report-file: results.json
45 | ```
46 |
47 | ### Usage with custom commenting logic
48 |
49 | The action will do the work of creating comments and updating them whenever tests re-run. If you
50 | require custom logic for creating and updating comments, you can disable the default logic and use
51 | any other action in combination with the `summary` output of this action.
52 |
53 | A few recommended actions are
54 | [Sticky Pull Request Comment](https://github.com/marketplace/actions/sticky-pull-request-comment)
55 | and [Create or Update Comment](https://github.com/marketplace/actions/create-or-update-comment).
56 |
57 | ```diff
58 | - uses: daun/playwright-report-summary@v3
59 | if: always()
60 | id: summary
61 | with:
62 | report-file: results.json
63 | + create-comment: false
64 |
65 | + - uses: marocchino/sticky-pull-request-comment@v2
66 | + with:
67 | + message: ${{ steps.summary.outputs.summary }}
68 | ```
69 |
70 | ## Options
71 |
72 | ```yaml
73 | - uses: daun/playwright-report-summary@v3
74 | if: always()
75 | with:
76 | # The GitHub access token to use for API requests
77 | # Defaults to the standard GITHUB_TOKEN
78 | github-token: ''
79 |
80 | # Path to the JSON report file generated by Playwright. Required.
81 | report-file: 'results.json'
82 |
83 | # A unique tag to represent this report when reporting on multiple test runs
84 | # Defaults to the current workflow name
85 | report-tag: ''
86 |
87 | # URL to a published html report, uploaded by another action in a previous step.
88 | # Example pipeline: https://playwright.dev/docs/test-sharding#publishing-report-on-the-web
89 | report-url: 'https://user.github.io/repo/yyyy-mm-dd-id/'
90 |
91 | # Whether the action should create the actual comment. Set to false to implement
92 | # your own commenting logic.
93 | # Default: true
94 | create-comment: true
95 |
96 | # Title/headline to use for the created pull request comment.
97 | # Default: 'Playwright test results'
98 | comment-title: 'Test results'
99 |
100 | # Additional information to include in the summary comment, markdown-formatted
101 | # Default: ''
102 | custom-info: 'For more information, [see our readme](http://link)'
103 |
104 | # Create a job summary comment for the workflow run
105 | # Default: false
106 | job-summary: false
107 |
108 | # Icon style to use: octicons | emojis
109 | # Default: octicons
110 | icon-style: 'octicons'
111 |
112 | # Command used to run tests. If provided, a command to re-run failed or
113 | # flaky tests will be printed for each section.
114 | # Default: ''
115 | test-command: 'npm run test --'
116 |
117 | # Additional content to add to the comment below the test report.
118 | # Default: ''
119 | footer: ''
120 | ```
121 |
122 | ## Output
123 |
124 | The action creates three output variables:
125 |
126 | ### summary
127 |
128 | The rendered markdown summary of the test report.
129 |
130 | ### comment-id
131 |
132 | The ID of the comment that was created or updated
133 |
134 | ### report-data
135 |
136 | The raw data of the test report, as a JSON-encoded string. This is
137 | useful for creating custom summaries or debugging. You can get an idea
138 | of the data structure by checking out the [ReportSummary interface](https://github.com/daun/playwright-report-summary/blob/main/src/report.ts#L13).
139 |
140 | ## License
141 |
142 | [MIT](./LICENSE)
143 |
--------------------------------------------------------------------------------
/__tests__/__fixtures__/report-invalid.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "configFile": "/project/tests/config/playwright.config.ts",
4 | "rootDir": "/project/tests/functional",
5 | "metadata": {
6 | "actualWorkers": 5,
7 | "totalTime": 611.735
8 | }
9 | },
10 | "tests": [
11 | {
12 | "title": "add.spec.ts",
13 | "file": "add.spec.ts",
14 | "column": 0,
15 | "line": 0,
16 | "specs": [],
17 | "suites": [
18 | {
19 | "title": "add",
20 | "file": "add.spec.ts",
21 | "line": 5,
22 | "column": 6,
23 | "specs": [
24 | {
25 | "title": "returns",
26 | "ok": true,
27 | "tags": [],
28 | "tests": [
29 | {
30 | "timeout": 5000,
31 | "annotations": [],
32 | "expectedStatus": "passed",
33 | "projectId": "chromium",
34 | "projectName": "chromium",
35 | "results": [
36 | {
37 | "workerIndex": 0,
38 | "status": "passed",
39 | "duration": 12,
40 | "errors": [],
41 | "stdout": [],
42 | "stderr": [],
43 | "retry": 0,
44 | "startTime": "2023-09-08T11:41:22.242Z",
45 | "attachments": []
46 | }
47 | ],
48 | "status": "expected"
49 | }
50 | ],
51 | "id": "2f4bb8d916741bacc6cb-23c1b2167130e38ec767",
52 | "file": "add.spec.ts",
53 | "line": 6,
54 | "column": 2
55 | },
56 | {
57 | "title": "adds",
58 | "ok": true,
59 | "tags": [],
60 | "tests": [
61 | {
62 | "timeout": 5000,
63 | "annotations": [],
64 | "expectedStatus": "passed",
65 | "projectId": "chromium",
66 | "projectName": "chromium",
67 | "results": [
68 | {
69 | "workerIndex": 1,
70 | "status": "passed",
71 | "duration": 14,
72 | "errors": [],
73 | "stdout": [],
74 | "stderr": [],
75 | "retry": 0,
76 | "startTime": "2023-09-08T11:41:22.240Z",
77 | "attachments": []
78 | }
79 | ],
80 | "status": "expected"
81 | }
82 | ],
83 | "id": "2f4bb8d916741bacc6cb-328ccc869357d9515391",
84 | "file": "add.spec.ts",
85 | "line": 9,
86 | "column": 2
87 | },
88 | {
89 | "title": "returns",
90 | "ok": true,
91 | "tags": [],
92 | "tests": [
93 | {
94 | "timeout": 5000,
95 | "annotations": [],
96 | "expectedStatus": "passed",
97 | "projectId": "firefox",
98 | "projectName": "firefox",
99 | "results": [
100 | {
101 | "workerIndex": 5,
102 | "status": "passed",
103 | "duration": 12,
104 | "errors": [],
105 | "stdout": [],
106 | "stderr": [],
107 | "retry": 0,
108 | "startTime": "2023-09-08T11:41:22.441Z",
109 | "attachments": []
110 | }
111 | ],
112 | "status": "expected"
113 | }
114 | ],
115 | "id": "2f4bb8d916741bacc6cb-acf8e9042a34b90fed6a",
116 | "file": "add.spec.ts",
117 | "line": 6,
118 | "column": 2
119 | },
120 | {
121 | "title": "adds",
122 | "ok": true,
123 | "tags": [],
124 | "tests": [
125 | {
126 | "timeout": 5000,
127 | "annotations": [],
128 | "expectedStatus": "passed",
129 | "projectId": "firefox",
130 | "projectName": "firefox",
131 | "results": [
132 | {
133 | "workerIndex": 6,
134 | "status": "passed",
135 | "duration": 13,
136 | "errors": [],
137 | "stdout": [],
138 | "stderr": [],
139 | "retry": 0,
140 | "startTime": "2023-09-08T11:41:22.437Z",
141 | "attachments": []
142 | }
143 | ],
144 | "status": "expected"
145 | }
146 | ],
147 | "id": "2f4bb8d916741bacc6cb-ab12f000ed9a4eea5a16",
148 | "file": "add.spec.ts",
149 | "line": 9,
150 | "column": 2
151 | },
152 | {
153 | "title": "returns",
154 | "ok": true,
155 | "tags": [],
156 | "tests": [
157 | {
158 | "timeout": 5000,
159 | "annotations": [],
160 | "expectedStatus": "passed",
161 | "projectId": "webkit",
162 | "projectName": "webkit",
163 | "results": [
164 | {
165 | "workerIndex": 10,
166 | "status": "passed",
167 | "duration": 15,
168 | "errors": [],
169 | "stdout": [],
170 | "stderr": [],
171 | "retry": 0,
172 | "startTime": "2023-09-08T11:41:22.644Z",
173 | "attachments": []
174 | }
175 | ],
176 | "status": "expected"
177 | }
178 | ],
179 | "id": "2f4bb8d916741bacc6cb-1072f29bbb9cb3f8bbc9",
180 | "file": "add.spec.ts",
181 | "line": 6,
182 | "column": 2
183 | },
184 | {
185 | "title": "adds",
186 | "ok": true,
187 | "tags": [],
188 | "tests": [
189 | {
190 | "timeout": 5000,
191 | "annotations": [],
192 | "expectedStatus": "passed",
193 | "projectId": "webkit",
194 | "projectName": "webkit",
195 | "results": [
196 | {
197 | "workerIndex": 12,
198 | "status": "passed",
199 | "duration": 12,
200 | "errors": [],
201 | "stdout": [],
202 | "stderr": [],
203 | "retry": 0,
204 | "startTime": "2023-09-08T11:41:22.651Z",
205 | "attachments": []
206 | }
207 | ],
208 | "status": "expected"
209 | }
210 | ],
211 | "id": "2f4bb8d916741bacc6cb-fd19a15b8b57bf62098b",
212 | "file": "add.spec.ts",
213 | "line": 9,
214 | "column": 2
215 | }
216 | ]
217 | }
218 | ]
219 | }
220 | ]
221 | }
222 |
--------------------------------------------------------------------------------
/__tests__/__fixtures__/report-sharded.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "forbidOnly": false,
4 | "fullyParallel": false,
5 | "globalSetup": null,
6 | "globalTeardown": null,
7 | "globalTimeout": 0,
8 | "grep": {},
9 | "grepInvert": null,
10 | "maxFailures": 0,
11 | "metadata": {
12 | "totalTime": 5003.32,
13 | "actualWorkers": 20
14 | },
15 | "preserveOutput": "always",
16 | "projects": [],
17 | "reporter": [
18 | [
19 | "list"
20 | ]
21 | ],
22 | "reportSlowTests": {
23 | "max": 5,
24 | "threshold": 15000
25 | },
26 | "rootDir": "/project",
27 | "quiet": false,
28 | "shard": null,
29 | "updateSnapshots": "missing",
30 | "version": "1.37.1",
31 | "workers": 20,
32 | "webServer": null,
33 | "listOnly": false
34 | },
35 | "suites": [
36 | {
37 | "title": "add.spec.ts",
38 | "file": "add.spec.ts",
39 | "column": 0,
40 | "line": 0,
41 | "specs": [],
42 | "suites": [
43 | {
44 | "title": "add",
45 | "file": "add.spec.ts",
46 | "line": 5,
47 | "column": 6,
48 | "specs": [
49 | {
50 | "title": "returns",
51 | "ok": true,
52 | "tags": [],
53 | "tests": [
54 | {
55 | "timeout": 5000,
56 | "annotations": [],
57 | "expectedStatus": "passed",
58 | "projectId": "chromium038c2392796d4a22",
59 | "projectName": "chromium",
60 | "results": [
61 | {
62 | "workerIndex": 0,
63 | "status": "passed",
64 | "duration": 35,
65 | "errors": [],
66 | "stdout": [],
67 | "stderr": [],
68 | "retry": 0,
69 | "startTime": "2023-09-09T10:45:05.447Z",
70 | "attachments": []
71 | }
72 | ],
73 | "status": "expected"
74 | },
75 | {
76 | "timeout": 5000,
77 | "annotations": [],
78 | "expectedStatus": "passed",
79 | "projectId": "firefox1eb99aebfb39e9e7",
80 | "projectName": "firefox",
81 | "results": [
82 | {
83 | "workerIndex": 2,
84 | "status": "passed",
85 | "duration": 33,
86 | "errors": [],
87 | "stdout": [],
88 | "stderr": [],
89 | "retry": 0,
90 | "startTime": "2023-09-09T10:45:16.110Z",
91 | "attachments": []
92 | }
93 | ],
94 | "status": "expected"
95 | },
96 | {
97 | "timeout": 5000,
98 | "annotations": [],
99 | "expectedStatus": "passed",
100 | "projectId": "webkit6128e7aebeb94118",
101 | "projectName": "webkit",
102 | "results": [
103 | {
104 | "workerIndex": 4,
105 | "status": "passed",
106 | "duration": 38,
107 | "errors": [],
108 | "stdout": [],
109 | "stderr": [],
110 | "retry": 0,
111 | "startTime": "2023-09-09T10:45:54.243Z",
112 | "attachments": []
113 | }
114 | ],
115 | "status": "expected"
116 | }
117 | ],
118 | "id": "2f4bb8d916741bacc6cb-23c1b2167130e38ec767038c2392796d4a22",
119 | "file": "add.spec.ts",
120 | "line": 6,
121 | "column": 2
122 | },
123 | {
124 | "title": "adds",
125 | "ok": true,
126 | "tags": [],
127 | "tests": [
128 | {
129 | "timeout": 5000,
130 | "annotations": [],
131 | "expectedStatus": "passed",
132 | "projectId": "chromium038c2392796d4a22",
133 | "projectName": "chromium",
134 | "results": [
135 | {
136 | "workerIndex": 1,
137 | "status": "passed",
138 | "duration": 40,
139 | "errors": [],
140 | "stdout": [],
141 | "stderr": [],
142 | "retry": 0,
143 | "startTime": "2023-09-09T10:45:05.444Z",
144 | "attachments": []
145 | }
146 | ],
147 | "status": "expected"
148 | },
149 | {
150 | "timeout": 5000,
151 | "annotations": [],
152 | "expectedStatus": "passed",
153 | "projectId": "firefox1eb99aebfb39e9e7",
154 | "projectName": "firefox",
155 | "results": [
156 | {
157 | "workerIndex": 3,
158 | "status": "passed",
159 | "duration": 39,
160 | "errors": [],
161 | "stdout": [],
162 | "stderr": [],
163 | "retry": 0,
164 | "startTime": "2023-09-09T10:45:16.105Z",
165 | "attachments": []
166 | }
167 | ],
168 | "status": "expected"
169 | },
170 | {
171 | "timeout": 5000,
172 | "annotations": [],
173 | "expectedStatus": "passed",
174 | "projectId": "webkit6128e7aebeb94118",
175 | "projectName": "webkit",
176 | "results": [
177 | {
178 | "workerIndex": 4,
179 | "status": "passed",
180 | "duration": 19,
181 | "errors": [],
182 | "stdout": [],
183 | "stderr": [],
184 | "retry": 0,
185 | "startTime": "2023-09-09T10:45:54.296Z",
186 | "attachments": []
187 | }
188 | ],
189 | "status": "expected"
190 | }
191 | ],
192 | "id": "2f4bb8d916741bacc6cb-328ccc869357d9515391038c2392796d4a22",
193 | "file": "add.spec.ts",
194 | "line": 9,
195 | "column": 2
196 | }
197 | ]
198 | }
199 | ]
200 | },
201 | {
202 | "title": "divide.spec.ts",
203 | "file": "divide.spec.ts",
204 | "column": 0,
205 | "line": 0,
206 | "specs": [],
207 | "suites": [
208 | {
209 | "title": "divide",
210 | "file": "divide.spec.ts",
211 | "line": 5,
212 | "column": 6,
213 | "specs": [
214 | {
215 | "title": "returns",
216 | "ok": true,
217 | "tags": [],
218 | "tests": [
219 | {
220 | "timeout": 5000,
221 | "annotations": [],
222 | "expectedStatus": "passed",
223 | "projectId": "chromium038c2392796d4a22",
224 | "projectName": "chromium",
225 | "results": [
226 | {
227 | "workerIndex": 2,
228 | "status": "passed",
229 | "duration": 35,
230 | "errors": [],
231 | "stdout": [],
232 | "stderr": [],
233 | "retry": 0,
234 | "startTime": "2023-09-09T10:45:05.443Z",
235 | "attachments": []
236 | }
237 | ],
238 | "status": "expected"
239 | },
240 | {
241 | "timeout": 5000,
242 | "annotations": [],
243 | "expectedStatus": "passed",
244 | "projectId": "firefox1eb99aebfb39e9e7",
245 | "projectName": "firefox",
246 | "results": [
247 | {
248 | "workerIndex": 4,
249 | "status": "passed",
250 | "duration": 37,
251 | "errors": [],
252 | "stdout": [],
253 | "stderr": [],
254 | "retry": 0,
255 | "startTime": "2023-09-09T10:45:16.110Z",
256 | "attachments": []
257 | }
258 | ],
259 | "status": "expected"
260 | },
261 | {
262 | "timeout": 5000,
263 | "annotations": [],
264 | "expectedStatus": "passed",
265 | "projectId": "webkit6128e7aebeb94118",
266 | "projectName": "webkit",
267 | "results": [
268 | {
269 | "workerIndex": 5,
270 | "status": "passed",
271 | "duration": 32,
272 | "errors": [],
273 | "stdout": [],
274 | "stderr": [],
275 | "retry": 0,
276 | "startTime": "2023-09-09T10:45:54.745Z",
277 | "attachments": []
278 | }
279 | ],
280 | "status": "expected"
281 | }
282 | ],
283 | "id": "abd65a3ec2f3a35a080f-6441d08247ddeb3814bf038c2392796d4a22",
284 | "file": "divide.spec.ts",
285 | "line": 6,
286 | "column": 2
287 | },
288 | {
289 | "title": "divides",
290 | "ok": true,
291 | "tags": [],
292 | "tests": [
293 | {
294 | "timeout": 5000,
295 | "annotations": [],
296 | "expectedStatus": "passed",
297 | "projectId": "chromium038c2392796d4a22",
298 | "projectName": "chromium",
299 | "results": [
300 | {
301 | "workerIndex": 3,
302 | "status": "passed",
303 | "duration": 39,
304 | "errors": [],
305 | "stdout": [],
306 | "stderr": [],
307 | "retry": 0,
308 | "startTime": "2023-09-09T10:45:05.444Z",
309 | "attachments": []
310 | }
311 | ],
312 | "status": "expected"
313 | },
314 | {
315 | "timeout": 5000,
316 | "annotations": [],
317 | "expectedStatus": "passed",
318 | "projectId": "firefox1eb99aebfb39e9e7",
319 | "projectName": "firefox",
320 | "results": [
321 | {
322 | "workerIndex": 2,
323 | "status": "passed",
324 | "duration": 18,
325 | "errors": [],
326 | "stdout": [],
327 | "stderr": [],
328 | "retry": 0,
329 | "startTime": "2023-09-09T10:45:16.156Z",
330 | "attachments": []
331 | }
332 | ],
333 | "status": "expected"
334 | },
335 | {
336 | "timeout": 5000,
337 | "annotations": [],
338 | "expectedStatus": "passed",
339 | "projectId": "webkitbd6f400eea527a0b",
340 | "projectName": "webkit",
341 | "results": [
342 | {
343 | "workerIndex": 0,
344 | "status": "passed",
345 | "duration": 39,
346 | "errors": [],
347 | "stdout": [],
348 | "stderr": [],
349 | "retry": 0,
350 | "startTime": "2023-09-09T10:46:14.314Z",
351 | "attachments": []
352 | }
353 | ],
354 | "status": "expected"
355 | }
356 | ],
357 | "id": "abd65a3ec2f3a35a080f-acf768063c5e76dcf174038c2392796d4a22",
358 | "file": "divide.spec.ts",
359 | "line": 9,
360 | "column": 2
361 | }
362 | ]
363 | }
364 | ]
365 | },
366 | {
367 | "title": "multiply.spec.ts",
368 | "file": "multiply.spec.ts",
369 | "column": 0,
370 | "line": 0,
371 | "specs": [],
372 | "suites": [
373 | {
374 | "title": "multiply",
375 | "file": "multiply.spec.ts",
376 | "line": 5,
377 | "column": 6,
378 | "specs": [
379 | {
380 | "title": "returns",
381 | "ok": true,
382 | "tags": [],
383 | "tests": [
384 | {
385 | "timeout": 5000,
386 | "annotations": [],
387 | "expectedStatus": "passed",
388 | "projectId": "chromium038c2392796d4a22",
389 | "projectName": "chromium",
390 | "results": [
391 | {
392 | "workerIndex": 4,
393 | "status": "passed",
394 | "duration": 41,
395 | "errors": [],
396 | "stdout": [],
397 | "stderr": [],
398 | "retry": 0,
399 | "startTime": "2023-09-09T10:45:05.443Z",
400 | "attachments": []
401 | }
402 | ],
403 | "status": "expected"
404 | },
405 | {
406 | "timeout": 5000,
407 | "annotations": [],
408 | "expectedStatus": "passed",
409 | "projectId": "firefox1eb99aebfb39e9e7",
410 | "projectName": "firefox",
411 | "results": [
412 | {
413 | "workerIndex": 5,
414 | "status": "passed",
415 | "duration": 36,
416 | "errors": [],
417 | "stdout": [],
418 | "stderr": [],
419 | "retry": 0,
420 | "startTime": "2023-09-09T10:45:16.577Z",
421 | "attachments": []
422 | }
423 | ],
424 | "status": "expected"
425 | },
426 | {
427 | "timeout": 5000,
428 | "annotations": [],
429 | "expectedStatus": "passed",
430 | "projectId": "webkitbd6f400eea527a0b",
431 | "projectName": "webkit",
432 | "results": [
433 | {
434 | "workerIndex": 1,
435 | "status": "passed",
436 | "duration": 42,
437 | "errors": [],
438 | "stdout": [],
439 | "stderr": [],
440 | "retry": 0,
441 | "startTime": "2023-09-09T10:46:14.319Z",
442 | "attachments": []
443 | }
444 | ],
445 | "status": "expected"
446 | }
447 | ],
448 | "id": "e7c88bf2960b23764a9d-a054f04aedfc95a37bb3038c2392796d4a22",
449 | "file": "multiply.spec.ts",
450 | "line": 6,
451 | "column": 2
452 | },
453 | {
454 | "title": "multiplies",
455 | "ok": true,
456 | "tags": [],
457 | "tests": [
458 | {
459 | "timeout": 5000,
460 | "annotations": [],
461 | "expectedStatus": "passed",
462 | "projectId": "chromium038c2392796d4a22",
463 | "projectName": "chromium",
464 | "results": [
465 | {
466 | "workerIndex": 2,
467 | "status": "passed",
468 | "duration": 20,
469 | "errors": [],
470 | "stdout": [],
471 | "stderr": [],
472 | "retry": 0,
473 | "startTime": "2023-09-09T10:45:05.496Z",
474 | "attachments": []
475 | }
476 | ],
477 | "status": "expected"
478 | },
479 | {
480 | "timeout": 5000,
481 | "annotations": [],
482 | "expectedStatus": "passed",
483 | "projectId": "firefox6128e7aebeb94118",
484 | "projectName": "firefox",
485 | "results": [
486 | {
487 | "workerIndex": 0,
488 | "status": "failed",
489 | "duration": 41,
490 | "error": {
491 | "message": "\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m7\u001b[39m\nReceived: \u001b[31m6\u001b[39m",
492 | "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m7\u001b[39m\nReceived: \u001b[31m6\u001b[39m\n at /project/tests/functional/multiply.spec.ts:10:26",
493 | "location": {
494 | "file": "/project/tests/functional/multiply.spec.ts",
495 | "column": 26,
496 | "line": 10
497 | },
498 | "snippet": "\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'multiplies'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(multiply(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[33mMath\u001b[39m\u001b[33m.\u001b[39mrandom() \u001b[33m>\u001b[39m \u001b[35m0.5\u001b[39m \u001b[33m?\u001b[39m \u001b[35m6\u001b[39m \u001b[33m:\u001b[39m \u001b[35m7\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m \ttest(\u001b[32m'skips'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m \t\ttest\u001b[33m.\u001b[39mskip()\u001b[33m;\u001b[39m\u001b[0m"
499 | },
500 | "errors": [
501 | {
502 | "location": {
503 | "file": "/project/tests/functional/multiply.spec.ts",
504 | "column": 26,
505 | "line": 10
506 | },
507 | "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m7\u001b[39m\nReceived: \u001b[31m6\u001b[39m\n\n\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'multiplies'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(multiply(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[33mMath\u001b[39m\u001b[33m.\u001b[39mrandom() \u001b[33m>\u001b[39m \u001b[35m0.5\u001b[39m \u001b[33m?\u001b[39m \u001b[35m6\u001b[39m \u001b[33m:\u001b[39m \u001b[35m7\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m \ttest(\u001b[32m'skips'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m \t\ttest\u001b[33m.\u001b[39mskip()\u001b[33m;\u001b[39m\u001b[0m\n\n\u001b[2m at /project/tests/functional/multiply.spec.ts:10:26\u001b[22m"
508 | }
509 | ],
510 | "stdout": [],
511 | "stderr": [],
512 | "retry": 0,
513 | "startTime": "2023-09-09T10:45:54.241Z",
514 | "attachments": [],
515 | "errorLocation": {
516 | "file": "/project/tests/functional/multiply.spec.ts",
517 | "column": 26,
518 | "line": 10
519 | }
520 | },
521 | {
522 | "workerIndex": 6,
523 | "status": "passed",
524 | "duration": 44,
525 | "errors": [],
526 | "stdout": [],
527 | "stderr": [],
528 | "retry": 1,
529 | "startTime": "2023-09-09T10:45:54.747Z",
530 | "attachments": [
531 | {
532 | "name": "trace",
533 | "contentType": "application/zip",
534 | "path": "/project/blobs/resources/d9fd85ea0eba4d32a4c0e9a0390bb889582f5139.zip"
535 | }
536 | ]
537 | }
538 | ],
539 | "status": "flaky"
540 | },
541 | {
542 | "timeout": 5000,
543 | "annotations": [],
544 | "expectedStatus": "passed",
545 | "projectId": "webkitbd6f400eea527a0b",
546 | "projectName": "webkit",
547 | "results": [
548 | {
549 | "workerIndex": 2,
550 | "status": "passed",
551 | "duration": 36,
552 | "errors": [],
553 | "stdout": [],
554 | "stderr": [],
555 | "retry": 0,
556 | "startTime": "2023-09-09T10:46:14.324Z",
557 | "attachments": []
558 | }
559 | ],
560 | "status": "expected"
561 | }
562 | ],
563 | "id": "e7c88bf2960b23764a9d-a5e9ce9b2e8260f09823038c2392796d4a22",
564 | "file": "multiply.spec.ts",
565 | "line": 9,
566 | "column": 2
567 | },
568 | {
569 | "title": "skips",
570 | "ok": true,
571 | "tags": [],
572 | "tests": [
573 | {
574 | "timeout": 5000,
575 | "annotations": [
576 | {
577 | "type": "skip"
578 | }
579 | ],
580 | "expectedStatus": "skipped",
581 | "projectId": "chromium038c2392796d4a22",
582 | "projectName": "chromium",
583 | "results": [
584 | {
585 | "workerIndex": 0,
586 | "status": "skipped",
587 | "duration": 20,
588 | "errors": [],
589 | "stdout": [],
590 | "stderr": [],
591 | "retry": 0,
592 | "startTime": "2023-09-09T10:45:05.501Z",
593 | "attachments": []
594 | }
595 | ],
596 | "status": "skipped"
597 | },
598 | {
599 | "timeout": 5000,
600 | "annotations": [
601 | {
602 | "type": "skip"
603 | }
604 | ],
605 | "expectedStatus": "skipped",
606 | "projectId": "firefox6128e7aebeb94118",
607 | "projectName": "firefox",
608 | "results": [
609 | {
610 | "workerIndex": 1,
611 | "status": "skipped",
612 | "duration": 36,
613 | "errors": [],
614 | "stdout": [],
615 | "stderr": [],
616 | "retry": 0,
617 | "startTime": "2023-09-09T10:45:54.243Z",
618 | "attachments": []
619 | }
620 | ],
621 | "status": "skipped"
622 | },
623 | {
624 | "timeout": 5000,
625 | "annotations": [
626 | {
627 | "type": "skip"
628 | }
629 | ],
630 | "expectedStatus": "skipped",
631 | "projectId": "webkitbd6f400eea527a0b",
632 | "projectName": "webkit",
633 | "results": [
634 | {
635 | "workerIndex": 3,
636 | "status": "skipped",
637 | "duration": 36,
638 | "errors": [],
639 | "stdout": [],
640 | "stderr": [],
641 | "retry": 0,
642 | "startTime": "2023-09-09T10:46:14.308Z",
643 | "attachments": []
644 | }
645 | ],
646 | "status": "skipped"
647 | }
648 | ],
649 | "id": "e7c88bf2960b23764a9d-669ef72c0b906305e786038c2392796d4a22",
650 | "file": "multiply.spec.ts",
651 | "line": 12,
652 | "column": 2
653 | }
654 | ]
655 | }
656 | ]
657 | },
658 | {
659 | "title": "subtract.spec.ts",
660 | "file": "subtract.spec.ts",
661 | "column": 0,
662 | "line": 0,
663 | "specs": [],
664 | "suites": [
665 | {
666 | "title": "subtract",
667 | "file": "subtract.spec.ts",
668 | "line": 5,
669 | "column": 6,
670 | "specs": [
671 | {
672 | "title": "returns",
673 | "ok": true,
674 | "tags": [],
675 | "tests": [
676 | {
677 | "timeout": 5000,
678 | "annotations": [],
679 | "expectedStatus": "passed",
680 | "projectId": "chromium1eb99aebfb39e9e7",
681 | "projectName": "chromium",
682 | "results": [
683 | {
684 | "workerIndex": 0,
685 | "status": "passed",
686 | "duration": 38,
687 | "errors": [],
688 | "stdout": [],
689 | "stderr": [],
690 | "retry": 0,
691 | "startTime": "2023-09-09T10:45:16.111Z",
692 | "attachments": []
693 | }
694 | ],
695 | "status": "expected"
696 | },
697 | {
698 | "timeout": 5000,
699 | "annotations": [],
700 | "expectedStatus": "passed",
701 | "projectId": "firefox6128e7aebeb94118",
702 | "projectName": "firefox",
703 | "results": [
704 | {
705 | "workerIndex": 2,
706 | "status": "passed",
707 | "duration": 39,
708 | "errors": [],
709 | "stdout": [],
710 | "stderr": [],
711 | "retry": 0,
712 | "startTime": "2023-09-09T10:45:54.245Z",
713 | "attachments": []
714 | }
715 | ],
716 | "status": "expected"
717 | },
718 | {
719 | "timeout": 5000,
720 | "annotations": [],
721 | "expectedStatus": "passed",
722 | "projectId": "webkitbd6f400eea527a0b",
723 | "projectName": "webkit",
724 | "results": [
725 | {
726 | "workerIndex": 4,
727 | "status": "passed",
728 | "duration": 39,
729 | "errors": [],
730 | "stdout": [],
731 | "stderr": [],
732 | "retry": 0,
733 | "startTime": "2023-09-09T10:46:14.314Z",
734 | "attachments": []
735 | }
736 | ],
737 | "status": "expected"
738 | }
739 | ],
740 | "id": "6ac0882294147c5cf865-ce417e4a0eefba5d5d531eb99aebfb39e9e7",
741 | "file": "subtract.spec.ts",
742 | "line": 6,
743 | "column": 2
744 | },
745 | {
746 | "title": "subtracts",
747 | "ok": true,
748 | "tags": [],
749 | "tests": [
750 | {
751 | "timeout": 5000,
752 | "annotations": [],
753 | "expectedStatus": "passed",
754 | "projectId": "chromium1eb99aebfb39e9e7",
755 | "projectName": "chromium",
756 | "results": [
757 | {
758 | "workerIndex": 1,
759 | "status": "passed",
760 | "duration": 35,
761 | "errors": [],
762 | "stdout": [],
763 | "stderr": [],
764 | "retry": 0,
765 | "startTime": "2023-09-09T10:45:16.111Z",
766 | "attachments": []
767 | }
768 | ],
769 | "status": "expected"
770 | },
771 | {
772 | "timeout": 5000,
773 | "annotations": [],
774 | "expectedStatus": "passed",
775 | "projectId": "firefox6128e7aebeb94118",
776 | "projectName": "firefox",
777 | "results": [
778 | {
779 | "workerIndex": 3,
780 | "status": "passed",
781 | "duration": 38,
782 | "errors": [],
783 | "stdout": [],
784 | "stderr": [],
785 | "retry": 0,
786 | "startTime": "2023-09-09T10:45:54.243Z",
787 | "attachments": []
788 | }
789 | ],
790 | "status": "expected"
791 | },
792 | {
793 | "timeout": 5000,
794 | "annotations": [],
795 | "expectedStatus": "passed",
796 | "projectId": "webkitbd6f400eea527a0b",
797 | "projectName": "webkit",
798 | "results": [
799 | {
800 | "workerIndex": 3,
801 | "status": "failed",
802 | "duration": 26,
803 | "error": {
804 | "message": "\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m",
805 | "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at /project/tests/functional/subtract.spec.ts:10:26",
806 | "location": {
807 | "file": "/project/tests/functional/subtract.spec.ts",
808 | "column": 26,
809 | "line": 10
810 | },
811 | "snippet": "\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m"
812 | },
813 | "errors": [
814 | {
815 | "location": {
816 | "file": "/project/tests/functional/subtract.spec.ts",
817 | "column": 26,
818 | "line": 10
819 | },
820 | "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n\n\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m\n\n\u001b[2m at /project/tests/functional/subtract.spec.ts:10:26\u001b[22m"
821 | }
822 | ],
823 | "stdout": [],
824 | "stderr": [],
825 | "retry": 0,
826 | "startTime": "2023-09-09T10:46:14.361Z",
827 | "attachments": [],
828 | "errorLocation": {
829 | "file": "/project/tests/functional/subtract.spec.ts",
830 | "column": 26,
831 | "line": 10
832 | }
833 | },
834 | {
835 | "workerIndex": 5,
836 | "status": "failed",
837 | "duration": 49,
838 | "error": {
839 | "message": "\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m",
840 | "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at /project/tests/functional/subtract.spec.ts:10:26",
841 | "location": {
842 | "file": "/project/tests/functional/subtract.spec.ts",
843 | "column": 26,
844 | "line": 10
845 | },
846 | "snippet": "\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m"
847 | },
848 | "errors": [
849 | {
850 | "location": {
851 | "file": "/project/tests/functional/subtract.spec.ts",
852 | "column": 26,
853 | "line": 10
854 | },
855 | "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n\n\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m\n\n\u001b[2m at /project/tests/functional/subtract.spec.ts:10:26\u001b[22m"
856 | }
857 | ],
858 | "stdout": [],
859 | "stderr": [],
860 | "retry": 1,
861 | "startTime": "2023-09-09T10:46:14.818Z",
862 | "attachments": [
863 | {
864 | "name": "trace",
865 | "contentType": "application/zip",
866 | "path": "/project/blobs/resources/f2597886a6d84319818a588535d5abc864ee9cbf.zip"
867 | }
868 | ],
869 | "errorLocation": {
870 | "file": "/project/tests/functional/subtract.spec.ts",
871 | "column": 26,
872 | "line": 10
873 | }
874 | },
875 | {
876 | "workerIndex": 6,
877 | "status": "failed",
878 | "duration": 36,
879 | "error": {
880 | "message": "\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m",
881 | "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at /project/tests/functional/subtract.spec.ts:10:26",
882 | "location": {
883 | "file": "/project/tests/functional/subtract.spec.ts",
884 | "column": 26,
885 | "line": 10
886 | },
887 | "snippet": "\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m"
888 | },
889 | "errors": [
890 | {
891 | "location": {
892 | "file": "/project/tests/functional/subtract.spec.ts",
893 | "column": 26,
894 | "line": 10
895 | },
896 | "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n\n\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m\n\n\u001b[2m at /project/tests/functional/subtract.spec.ts:10:26\u001b[22m"
897 | }
898 | ],
899 | "stdout": [],
900 | "stderr": [],
901 | "retry": 2,
902 | "startTime": "2023-09-09T10:46:15.294Z",
903 | "attachments": [],
904 | "errorLocation": {
905 | "file": "/project/tests/functional/subtract.spec.ts",
906 | "column": 26,
907 | "line": 10
908 | }
909 | },
910 | {
911 | "workerIndex": 7,
912 | "status": "failed",
913 | "duration": 36,
914 | "error": {
915 | "message": "\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m",
916 | "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at /project/tests/functional/subtract.spec.ts:10:26",
917 | "location": {
918 | "file": "/project/tests/functional/subtract.spec.ts",
919 | "column": 26,
920 | "line": 10
921 | },
922 | "snippet": "\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m"
923 | },
924 | "errors": [
925 | {
926 | "location": {
927 | "file": "/project/tests/functional/subtract.spec.ts",
928 | "column": 26,
929 | "line": 10
930 | },
931 | "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n\n\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m\n\n\u001b[2m at /project/tests/functional/subtract.spec.ts:10:26\u001b[22m"
932 | }
933 | ],
934 | "stdout": [],
935 | "stderr": [],
936 | "retry": 3,
937 | "startTime": "2023-09-09T10:46:15.766Z",
938 | "attachments": [],
939 | "errorLocation": {
940 | "file": "/project/tests/functional/subtract.spec.ts",
941 | "column": 26,
942 | "line": 10
943 | }
944 | },
945 | {
946 | "workerIndex": 8,
947 | "status": "failed",
948 | "duration": 35,
949 | "error": {
950 | "message": "\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m",
951 | "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n at /project/tests/functional/subtract.spec.ts:10:26",
952 | "location": {
953 | "file": "/project/tests/functional/subtract.spec.ts",
954 | "column": 26,
955 | "line": 10
956 | },
957 | "snippet": "\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m"
958 | },
959 | "errors": [
960 | {
961 | "location": {
962 | "file": "/project/tests/functional/subtract.spec.ts",
963 | "column": 26,
964 | "line": 10
965 | },
966 | "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m4\u001b[39m\nReceived: \u001b[31m1\u001b[39m\n\n\u001b[0m \u001b[90m 8 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 9 |\u001b[39m \ttest(\u001b[32m'subtracts'\u001b[39m\u001b[33m,\u001b[39m \u001b[36masync\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\n\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 10 |\u001b[39m \t\texpect(subtract(\u001b[35m3\u001b[39m\u001b[33m,\u001b[39m \u001b[35m2\u001b[39m))\u001b[33m.\u001b[39mtoBe(\u001b[35m4\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m |\u001b[39m \t\t \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 11 |\u001b[39m \t})\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 12 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\n\u001b[0m \u001b[90m 13 |\u001b[39m\u001b[0m\n\n\u001b[2m at /project/tests/functional/subtract.spec.ts:10:26\u001b[22m"
967 | }
968 | ],
969 | "stdout": [],
970 | "stderr": [],
971 | "retry": 4,
972 | "startTime": "2023-09-09T10:46:16.234Z",
973 | "attachments": [],
974 | "errorLocation": {
975 | "file": "/project/tests/functional/subtract.spec.ts",
976 | "column": 26,
977 | "line": 10
978 | }
979 | }
980 | ],
981 | "status": "unexpected"
982 | }
983 | ],
984 | "id": "6ac0882294147c5cf865-17a50ac4c8699683fcf21eb99aebfb39e9e7",
985 | "file": "subtract.spec.ts",
986 | "line": 9,
987 | "column": 2
988 | }
989 | ]
990 | }
991 | ]
992 | }
993 | ],
994 | "errors": []
995 | }
996 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/report.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renderReportSummary matches snapshot 1`] = `
4 | "### Test Report
5 |
6 |  **2 failed**
7 |  **10 passed**
8 |  **1 flaky**
9 |  **1 skipped**
10 |
11 | #### Details
12 |
13 |  [Open report ↗︎](https://example.com/report)
14 |  14 tests across 4 suites
15 |  1.1 seconds
16 |  1234567
17 |  For more information, see our [documentation](https://example.com/docs)
18 |
19 | Failed tests
20 |
21 | chromium › add.spec.ts › add › adds
22 | firefox › add.spec.ts › add › adds
23 |
24 |
25 | Flaky tests
26 |
27 | chromium › multiply.spec.ts › multiply › multiplies
28 |
29 |
30 | Skipped tests
31 |
32 | chromium › multiply.spec.ts › multiply › skips
33 |
34 | "
35 | `;
36 |
--------------------------------------------------------------------------------
/__tests__/formatting.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for src/formatting.ts
3 | */
4 |
5 | import { expect } from '@jest/globals'
6 | import { formatDuration, upperCaseFirst, renderMarkdownTable } from '../src/formatting'
7 |
8 | describe('formatDuration', () => {
9 | it('returns a string', async () => {
10 | expect(typeof formatDuration(3000) === 'string').toBe(true)
11 | })
12 | it('formats milliseconds', async () => {
13 | expect(formatDuration(500)).toBe('0.5 seconds')
14 | })
15 | it('formats seconds', async () => {
16 | expect(formatDuration(3000)).toBe('3 seconds')
17 | })
18 | it('formats singular seconds', async () => {
19 | expect(formatDuration(1000)).toBe('1 second')
20 | })
21 | it('formats minutes', async () => {
22 | expect(formatDuration(330000)).toBe('5 minutes, 30 seconds')
23 | })
24 | it('formats singular minutes', async () => {
25 | expect(formatDuration(60000)).toBe('1 minute')
26 | })
27 | it('formats hours', async () => {
28 | // (5*1000)+(15*1000*60)+(2*1000*60*60)
29 | expect(formatDuration(8105000)).toBe('2 hours, 15 minutes, 5 seconds')
30 | })
31 | it('formats singular hours', async () => {
32 | expect(formatDuration(3600000)).toBe('1 hour')
33 | })
34 | it('formats days', async () => {
35 | // (8*1000)+(36*1000*60)+(13*1000*60*60)+(3*1000*60*60*24)
36 | expect(formatDuration(308168000)).toBe('3 days, 13 hours, 36 minutes, 8 seconds')
37 | })
38 | it('formats singular days', async () => {
39 | expect(formatDuration(86400000)).toBe('1 day')
40 | })
41 | })
42 |
43 | describe('upperCaseFirst', () => {
44 | it('returns a string', async () => {
45 | expect(typeof upperCaseFirst('lorem') === 'string').toBe(true)
46 | })
47 | it('uppercases the first letter', async () => {
48 | expect(upperCaseFirst('lorem')).toBe('Lorem')
49 | })
50 | })
51 |
52 | describe('renderMarkdownTable', () => {
53 | it('returns a string', async () => {
54 | expect(
55 | typeof renderMarkdownTable([
56 | ['A', 'B'],
57 | ['C', 'D']
58 | ]) === 'string'
59 | ).toBe(true)
60 | })
61 | it('generates the correct markup', async () => {
62 | const expected = `| |\n| :--- | :---: |\n| A | B |\n| C | D |`
63 | expect(
64 | renderMarkdownTable([
65 | ['A', 'B'],
66 | ['C', 'D']
67 | ])
68 | ).toBe(expected)
69 | })
70 | it('returns empty string for empty data', async () => {
71 | expect(renderMarkdownTable([])).toBe('')
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/__tests__/fs.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for src/fs.ts
3 | */
4 |
5 | import { expect } from '@jest/globals'
6 | import { fileExists, readFile } from '../src/fs'
7 |
8 | describe('fileExists', () => {
9 | it('returns a Promise', async () => {
10 | expect(fileExists('package.json') instanceof Promise).toBe(true)
11 | })
12 | it('resolves to a boolean', async () => {
13 | expect(typeof (await fileExists('package.json')) === 'boolean').toBe(true)
14 | })
15 | it('returns true for existing files', async () => {
16 | expect(await fileExists('package.json')).toBe(true)
17 | })
18 | it('returns false for non-existing files', async () => {
19 | expect(await fileExists('no-such-icon.json')).toBe(false)
20 | })
21 | })
22 |
23 | describe('readFile', () => {
24 | it('returns a Promise', async () => {
25 | expect(readFile('LICENSE') instanceof Promise).toBe(true)
26 | })
27 | it('resolves to a string', async () => {
28 | expect(typeof (await readFile('LICENSE')) === 'string').toBe(true)
29 | })
30 | it('returns file contents', async () => {
31 | expect(await readFile('LICENSE')).toMatch(/THE SOFTWARE IS PROVIDED/)
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/__tests__/github.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for src/github.ts
3 | */
4 |
5 | import { expect } from '@jest/globals'
6 |
7 | import { createIssueComment, createPullRequestReview, getIssueComments, updateIssueComment } from '../src/github'
8 |
9 | describe('github', () => {
10 | let octokit: any
11 |
12 | beforeEach(() => {
13 | octokit = {
14 | rest: {
15 | issues: {
16 | listComments: jest.fn(async () => ({ data: [{ id: 1 }, { id: 2 }] })),
17 | updateComment: jest.fn(async (data: any) => ({ data: { ...data, id: data.comment_id } })),
18 | createComment: jest.fn(async (data: any) => ({ data: { ...data, id: 4 } }))
19 | },
20 | pulls: {
21 | createReview: jest.fn(async (data: object) => ({ data: { ...data, id: 5 } }))
22 | }
23 | }
24 | }
25 | jest.clearAllMocks()
26 | })
27 |
28 | describe('getIssueComments', () => {
29 | it('calls issues.listComments with correct parameters', async () => {
30 | const params = { owner: 'owner', repo: 'repo', issue_number: 123 }
31 | const expectedArguments = { ...params }
32 |
33 | await getIssueComments(octokit, params)
34 |
35 | expect(octokit.rest.issues.listComments).toHaveBeenCalledWith(expectedArguments)
36 | })
37 |
38 | it('returns the comment data', async () => {
39 | const params = { owner: 'owner', repo: 'repo', issue_number: 123 }
40 | const expectedResult = [{ id: 1 }, { id: 2 }]
41 |
42 | const result = await getIssueComments(octokit, params)
43 |
44 | expect(result).toMatchObject(expectedResult)
45 | })
46 |
47 | it('throws an error if listComments fails', async () => {
48 | octokit.rest.issues.listComments.mockRejectedValue(new Error('API error'))
49 | const params = { owner: 'owner', repo: 'repo', issue_number: 123 }
50 |
51 | await expect(getIssueComments(octokit, params)).rejects.toThrow('API error')
52 | })
53 | })
54 |
55 | describe('createIssueComment', () => {
56 | it('calls issues.createComment with correct parameters', async () => {
57 | const params = { owner: 'owner', repo: 'repo', issue_number: 123, body: 'body' }
58 | const expectedArguments = { ...params }
59 |
60 | await createIssueComment(octokit, params)
61 |
62 | expect(octokit.rest.issues.createComment).toHaveBeenCalledWith(expectedArguments)
63 | })
64 |
65 | it('returns the comment data', async () => {
66 | const params = { owner: 'owner', repo: 'repo', issue_number: 123, body: 'body' }
67 | const expectedResult = { ...params, id: expect.any(Number) }
68 |
69 | const result = await createIssueComment(octokit, params)
70 |
71 | expect(result).toMatchObject(expectedResult)
72 | })
73 |
74 | it('throws an error if createComment fails', async () => {
75 | octokit.rest.issues.createComment.mockRejectedValue(new Error('API error'))
76 | const params = { owner: 'owner', repo: 'repo', issue_number: 123, body: 'body' }
77 |
78 | await expect(createIssueComment(octokit, params)).rejects.toThrow('API error')
79 | })
80 | })
81 |
82 | describe('updateIssueComment', () => {
83 | it('calls issues.updateComment with correct parameters', async () => {
84 | const params = { owner: 'owner', repo: 'repo', comment_id: 123, body: 'body' }
85 | const expectedArguments = { ...params }
86 |
87 | await updateIssueComment(octokit, params)
88 |
89 | expect(octokit.rest.issues.updateComment).toHaveBeenCalledWith(expectedArguments)
90 | })
91 |
92 | it('returns the comment data', async () => {
93 | const params = { owner: 'owner', repo: 'repo', comment_id: 123, body: 'body' }
94 | const expectedResult = { ...params, id: expect.any(Number) }
95 |
96 | const result = await updateIssueComment(octokit, params)
97 |
98 | expect(result).toMatchObject(expectedResult)
99 | })
100 |
101 | it('throws an error if updateComment fails', async () => {
102 | octokit.rest.issues.updateComment.mockRejectedValue(new Error('API error'))
103 | const params = { owner: 'owner', repo: 'repo', comment_id: 123, body: 'body' }
104 |
105 | await expect(updateIssueComment(octokit, params)).rejects.toThrow('API error')
106 | })
107 | })
108 |
109 | describe('createPullRequestReview', () => {
110 | it('calls pulls.createReview with correct parameters', async () => {
111 | const params = { owner: 'owner', repo: 'repo', pull_number: 123, body: 'body' }
112 | const expectedArguments = { ...params, event: 'COMMENT' }
113 |
114 | await createPullRequestReview(octokit, params)
115 |
116 | expect(octokit.rest.pulls.createReview).toHaveBeenCalledWith(expectedArguments)
117 | })
118 |
119 | it('returns the review data', async () => {
120 | const params = { owner: 'owner', repo: 'repo', pull_number: 123, body: 'body' }
121 | const expectedResult = { ...params, id: expect.any(Number) }
122 |
123 | const result = await createPullRequestReview(octokit, params)
124 |
125 | expect(result).toMatchObject(expectedResult)
126 | })
127 |
128 | it('throws an error if createReview fails', async () => {
129 | octokit.rest.pulls.createReview.mockRejectedValue(new Error('API error'))
130 | const params = { owner: 'owner', repo: 'repo', pull_number: 123, body: 'body' }
131 |
132 | await expect(createPullRequestReview(octokit, params)).rejects.toThrow('API error')
133 | })
134 | })
135 | })
136 |
--------------------------------------------------------------------------------
/__tests__/icons.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for src/icons.ts
3 | */
4 |
5 | import { expect } from '@jest/globals'
6 | import { renderIcon } from '../src/icons'
7 |
8 | describe('renderIcon', () => {
9 | it('returns a string', async () => {
10 | expect(typeof renderIcon('passed') === 'string').toBe(true)
11 | })
12 | it('returns an empty string for undefined icons', async () => {
13 | expect(renderIcon('no-such-icon')).toBe('')
14 | })
15 | it('renders markdown images', async () => {
16 | expect(renderIcon('passed')).toMatch(/^[!]\[([\w\s\d]+)\]\((https?:\/\/[\w\d./?&=_-]+)\)$/i)
17 | })
18 | it('colorizes markdown images', async () => {
19 | expect(renderIcon('passed')).toMatch(/\?.*?\bcolor=[\w]{6}\b/i)
20 | })
21 | it('renders emojis', async () => {
22 | expect(renderIcon('passed', { iconStyle: 'emojis' })).toBe('✅')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for the action's entrypoint, src/index.ts
3 | *
4 | * These should be run as if the action was called from a workflow.
5 | * Specifically, the inputs listed in `action.yml` should be set as environment
6 | * variables following the pattern `INPUT_`.
7 | */
8 |
9 | import { Context } from '@actions/github/lib/context'
10 | import * as actionsCore from '@actions/core'
11 | import * as actionsGitHub from '@actions/github'
12 |
13 | import * as index from '../src/index'
14 | import * as fs from '../src/fs'
15 | import * as report from '../src/report'
16 | import * as github from '../src/github'
17 |
18 | // Mock the GitHub Actions core library
19 | jest.spyOn(actionsCore, 'info').mockImplementation(jest.fn())
20 | jest.spyOn(actionsCore, 'warning').mockImplementation(jest.fn())
21 | jest.spyOn(actionsCore, 'error').mockImplementation(jest.fn())
22 | const debugMock = jest.spyOn(actionsCore, 'debug').mockImplementation(jest.fn())
23 | const getInputMock = jest.spyOn(actionsCore, 'getInput').mockImplementation((name: string) => inputs[name] || '')
24 | const setFailedMock = jest.spyOn(actionsCore, 'setFailed').mockImplementation(jest.fn())
25 | const setOutputMock = jest.spyOn(actionsCore, 'setOutput').mockImplementation(jest.fn())
26 |
27 | // Mock the fs module
28 | const readFileMock = jest.spyOn(fs, 'readFile')
29 |
30 | // Mock the report module
31 | const parseReportMock = jest.spyOn(report, 'parseReport')
32 | const renderReportSummaryMock = jest.spyOn(report, 'renderReportSummary')
33 |
34 | // Mock the github module
35 | const createIssueCommentMock = jest.spyOn(github, 'createIssueComment')
36 |
37 | // Mock the GitHub Actions context library
38 | // const contextMock = jest.spyOn(github, 'context')
39 |
40 | // Mock the GitHub Actions Octokit instance
41 | type Octokit = ReturnType
42 |
43 | const octokitMock = {
44 | rest: {
45 | issues: {
46 | listComments: jest.fn(async () => ({ data: [{ id: 1 }, { id: 2 }] })),
47 | updateComment: jest.fn(async (data: any) => ({ data: { ...data, id: data.comment_id } })),
48 | createComment: jest.fn(async (data: any) => ({ data: { ...data, id: 4 } }))
49 | },
50 | pulls: {
51 | createReview: jest.fn(async (data: object) => ({ data: { ...data, id: 5 } }))
52 | }
53 | }
54 | } as unknown as Octokit
55 |
56 | const getOctokitMock = jest.spyOn(actionsGitHub, 'getOctokit').mockImplementation(() => octokitMock)
57 |
58 | // Mock the action's entrypoint
59 | const runMock = jest.spyOn(index, 'run')
60 |
61 | // Mark as GitHub action environment
62 | // process.env.GITHUB_ACTIONS = 'true'
63 |
64 | // Shallow clone original @actions/github context
65 | // @ts-expect-error missing issue and repo keys
66 | const originalContext: Context = { issue: {}, ...actionsGitHub.context }
67 |
68 | const defaultContext = {
69 | eventName: 'pull_request',
70 | repo: {
71 | owner: 'some-owner',
72 | repo: 'some-repo'
73 | },
74 | issue: {
75 | owner: 'some-owner',
76 | number: 12345
77 | },
78 | payload: {
79 | issue: {
80 | number: 12345
81 | },
82 | pull_request: {
83 | base: {
84 | ref: 'main',
85 | sha: 'abc123'
86 | },
87 | head: {
88 | ref: 'feature-branch',
89 | sha: 'def456'
90 | }
91 | }
92 | }
93 | }
94 |
95 | // Inputs for mock @actions/core
96 | let inputs: Record = {}
97 |
98 | function setContext(context: any): void {
99 | Object.defineProperty(actionsGitHub, 'context', { value: context, writable: true })
100 | }
101 |
102 | describe('action', () => {
103 | beforeAll(() => {})
104 |
105 | beforeEach(() => {
106 | setContext(defaultContext)
107 | jest.clearAllMocks()
108 | })
109 |
110 | afterEach(() => {
111 | // Restore @actions/github context
112 | setContext(originalContext)
113 | })
114 |
115 | afterAll(() => {
116 | // Restore
117 | jest.restoreAllMocks()
118 | })
119 |
120 | it('reads its inputs', async () => {
121 | inputs = {
122 | 'report-file': '__tests__/__fixtures__/report-valid.json',
123 | 'comment-title': 'Custom comment title'
124 | }
125 |
126 | await index.run()
127 |
128 | expect(runMock).toHaveReturned()
129 | expect(getInputMock).toHaveBeenCalledWith('github-token')
130 | expect(getInputMock).toHaveBeenCalledWith('report-file', { required: true })
131 | expect(getInputMock).toHaveBeenCalledWith('report-url')
132 | expect(getInputMock).toHaveBeenCalledWith('report-tag')
133 | expect(getInputMock).toHaveBeenCalledWith('comment-title')
134 | expect(getInputMock).toHaveBeenCalledWith('icon-style')
135 | expect(getInputMock).toHaveBeenCalledWith('create-comment')
136 | expect(getInputMock).toHaveBeenCalledWith('job-summary')
137 | expect(getInputMock).toHaveBeenCalledWith('test-command')
138 | expect(getInputMock).toHaveBeenCalledWith('footer')
139 | })
140 |
141 | it('debugs its inputs', async () => {
142 | inputs = {
143 | 'report-file': '__tests__/__fixtures__/report-valid.json',
144 | 'comment-title': 'Custom comment title'
145 | }
146 |
147 | await index.run()
148 |
149 | expect(runMock).toHaveReturned()
150 | expect(debugMock).toHaveBeenNthCalledWith(1, 'Report file: __tests__/__fixtures__/report-valid.json')
151 | expect(debugMock).toHaveBeenNthCalledWith(2, 'Report url: (none)')
152 | expect(debugMock).toHaveBeenNthCalledWith(3, 'Report tag: (none)')
153 | expect(debugMock).toHaveBeenNthCalledWith(4, 'Comment title: Custom comment title')
154 | expect(debugMock).toHaveBeenNthCalledWith(5, 'Creating comment? yes')
155 | expect(debugMock).toHaveBeenNthCalledWith(6, 'Creating job summary? no')
156 | })
157 |
158 | it('creates an Octokit instance', async () => {
159 | inputs = {
160 | 'github-token': 'some-token'
161 | }
162 |
163 | await index.run()
164 | expect(runMock).toHaveReturned()
165 | expect(getOctokitMock).toHaveBeenCalledWith('some-token')
166 | })
167 |
168 | it('reads the supplied report file', async () => {
169 | inputs = {
170 | 'report-file': '__tests__/__fixtures__/report-valid.json'
171 | }
172 |
173 | await index.run()
174 |
175 | expect(runMock).toHaveReturned()
176 | expect(readFileMock).toHaveBeenNthCalledWith(
177 | 1,
178 | expect.stringMatching(/__tests__[/]__fixtures__[/]report-valid.json$/)
179 | )
180 | })
181 |
182 | it('parses the report and renders a summary', async () => {
183 | inputs = {
184 | 'report-file': '__tests__/__fixtures__/report-valid.json'
185 | }
186 |
187 | await index.run()
188 | expect(runMock).toHaveReturned()
189 |
190 | expect(parseReportMock).toHaveBeenNthCalledWith(1, expect.any(String))
191 | expect(renderReportSummaryMock).toHaveBeenNthCalledWith(
192 | 1,
193 | expect.any(Object),
194 | expect.objectContaining({
195 | commit: 'def456',
196 | title: 'Playwright test results',
197 | reportUrl: expect.any(String),
198 | iconStyle: expect.any(String)
199 | })
200 | )
201 | })
202 |
203 | it('sets a summary output', async () => {
204 | inputs = {
205 | 'report-file': '__tests__/__fixtures__/report-valid.json'
206 | }
207 |
208 | await index.run()
209 |
210 | expect(runMock).toHaveReturned()
211 | expect(setOutputMock).toHaveBeenNthCalledWith(1, 'summary', expect.stringContaining('# Playwright test results'))
212 | })
213 |
214 | it('sets a comment id output', async () => {
215 | inputs = {
216 | 'report-file': '__tests__/__fixtures__/report-valid.json'
217 | }
218 |
219 | await index.run()
220 |
221 | expect(runMock).toHaveReturned()
222 | expect(setOutputMock).toHaveBeenNthCalledWith(2, 'comment-id', expect.anything())
223 | })
224 |
225 | it('sets a report data output', async () => {
226 | inputs = {
227 | 'report-file': '__tests__/__fixtures__/report-valid.json'
228 | }
229 |
230 | await index.run()
231 |
232 | expect(runMock).toHaveReturned()
233 | expect(setOutputMock).toHaveBeenNthCalledWith(3, 'report-data', expect.stringContaining('"failed":['))
234 | })
235 |
236 | it('creates a comment', async () => {
237 | inputs = {
238 | 'report-file': '__tests__/__fixtures__/report-valid.json'
239 | }
240 |
241 | await index.run()
242 |
243 | expect(runMock).toHaveReturned()
244 | expect(createIssueCommentMock).toHaveBeenCalledWith(
245 | expect.anything(),
246 | expect.objectContaining({
247 | body: expect.stringContaining('# Playwright test results')
248 | })
249 | )
250 | })
251 |
252 | it('can disable creating a comment', async () => {
253 | inputs = {
254 | 'report-file': '__tests__/__fixtures__/report-valid.json',
255 | 'create-comment': 'false'
256 | }
257 |
258 | await index.run()
259 |
260 | expect(runMock).toHaveReturned()
261 | expect(createIssueCommentMock).not.toHaveBeenCalled()
262 | })
263 |
264 | it('sets a failed status', async () => {
265 | inputs = {
266 | 'report-file': 'file-does-not-exist.json'
267 | }
268 |
269 | await index.run()
270 |
271 | expect(runMock).toHaveReturned()
272 | expect(setFailedMock).toHaveBeenNthCalledWith(
273 | 1,
274 | 'Report file file-does-not-exist.json not found. Make sure Playwright is configured to generate a JSON report.'
275 | )
276 | })
277 | })
278 |
--------------------------------------------------------------------------------
/__tests__/report.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for src/report.ts
3 | */
4 |
5 | import { expect } from '@jest/globals'
6 | import { readFile } from '../src/fs'
7 | import {
8 | ReportSummary,
9 | buildTitle,
10 | isValidReport,
11 | parseReport,
12 | parseReportFiles,
13 | parseReportSuites,
14 | renderReportSummary
15 | } from '../src/report'
16 |
17 | const defaultReport = 'report-valid.json'
18 | const invalidReport = 'report-invalid.json'
19 | const reportWithoutDuration = 'report-without-duration.json'
20 | const shardedReport = 'report-sharded.json'
21 | const nestedReport = 'report-nested.json'
22 |
23 | async function getReport(file = defaultReport): Promise {
24 | return await readFile(`__tests__/__fixtures__/${file}`)
25 | }
26 |
27 | async function getParsedReport(file = defaultReport): Promise {
28 | return parseReport(await getReport(file))
29 | }
30 |
31 | describe('isValidReport', () => {
32 | it('detects valid reports', async () => {
33 | const report = await getReport()
34 | expect(isValidReport(JSON.parse(report))).toBe(true)
35 | })
36 | it('detects invalid reports', async () => {
37 | const report = await getReport(invalidReport)
38 | expect(isValidReport([])).toBe(false)
39 | expect(isValidReport('')).toBe(false)
40 | expect(isValidReport(JSON.parse(report))).toBe(false)
41 | })
42 | })
43 |
44 | describe('buildTitle', () => {
45 | it('returns an object with path and title', async () => {
46 | const result = buildTitle('A', 'B')
47 | expect(result).toBeInstanceOf(Object)
48 | expect(result.path).toBeDefined()
49 | expect(result.title).toBeDefined()
50 | })
51 | it('concatenates and filters title segments', async () => {
52 | const { title } = buildTitle('A', 'B', '', 'C')
53 | expect(title).toBe('A › B › C')
54 | })
55 | it('concatenates and filters path segments', async () => {
56 | const { path } = buildTitle('A', '', 'B', 'C')
57 | expect(path).toStrictEqual(['A', 'B', 'C'])
58 | })
59 | })
60 |
61 | describe('parseReportFiles', () => {
62 | it('returns an array of root filenames', async () => {
63 | const report = JSON.parse(await getReport(nestedReport))
64 | const files = parseReportFiles(report)
65 | expect(files).toStrictEqual(['add.spec.ts', 'nested.spec.ts'])
66 | })
67 | })
68 |
69 | describe('parseReportSuites', () => {
70 | it('returns an array of root suite summaries', async () => {
71 | const report = JSON.parse(await getReport(nestedReport))
72 | const suites = parseReportSuites(report)
73 | expect(suites).toBeInstanceOf(Array)
74 | expect(suites.length).toBe(2)
75 | expect(suites[0].title).toBe('add.spec.ts')
76 | })
77 | })
78 |
79 | describe('parseReport', () => {
80 | it('returns an object', async () => {
81 | const parsed = await getParsedReport()
82 | expect(typeof parsed === 'object').toBe(true)
83 | })
84 | it('returns playwright version', async () => {
85 | const parsed = await getParsedReport()
86 | expect(parsed.version).toBe('1.37.1')
87 | })
88 | it('returns total duration', async () => {
89 | const parsed = await getParsedReport()
90 | expect(parsed.duration).toBe(1118.34)
91 | })
92 | it('calculates duration if missing', async () => {
93 | const parsed = await getParsedReport(reportWithoutDuration)
94 | expect(parsed.duration).toBe(943)
95 | })
96 | it('returns workers', async () => {
97 | const parsed = await getParsedReport()
98 | expect(parsed.workers).toBe(5)
99 | })
100 | it('returns shards', async () => {
101 | const parsed = await getParsedReport()
102 | expect(parsed.shards).toBe(2)
103 | })
104 | it('returns files', async () => {
105 | const parsed = await getParsedReport()
106 | expect(parsed.files.length).toBe(4)
107 | })
108 | it('returns suites', async () => {
109 | const parsed = await getParsedReport()
110 | expect(parsed.suites.length).toBe(4)
111 | })
112 | it('returns specs', async () => {
113 | const parsed = await getParsedReport()
114 | expect(parsed.specs.length).toBe(14)
115 | })
116 | it('counts tests', async () => {
117 | const parsed = await getParsedReport()
118 | expect(parsed.tests.length).toBe(14)
119 | expect(parsed.failed.length).toBe(2)
120 | expect(parsed.passed.length).toBe(10)
121 | expect(parsed.flaky.length).toBe(1)
122 | expect(parsed.skipped.length).toBe(1)
123 | })
124 | it('counts sharded tests', async () => {
125 | const parsed = await getParsedReport(shardedReport)
126 | expect(parsed.tests.length).toBe(27)
127 | expect(parsed.failed.length).toBe(1)
128 | expect(parsed.passed.length).toBe(22)
129 | expect(parsed.flaky.length).toBe(1)
130 | expect(parsed.skipped.length).toBe(3)
131 | })
132 | it('counts nested suites', async () => {
133 | const parsed = await getParsedReport(nestedReport)
134 | expect(parsed.suites.length).toBe(2)
135 | expect(parsed.specs.length).toBe(45)
136 | expect(parsed.tests.length).toBe(45)
137 | })
138 | })
139 |
140 | describe('renderReportSummary', () => {
141 | const renderOptions = {
142 | title: 'Test Report',
143 | reportUrl: 'https://example.com/report',
144 | customInfo: 'For more information, see our [documentation](https://example.com/docs)',
145 | commit: '1234567'
146 | }
147 | const getReportSummary = async (): Promise =>
148 | renderReportSummary(parseReport(await getReport()), renderOptions)
149 | it('returns a string', async () => {
150 | const summary = await getReportSummary()
151 | expect(typeof summary === 'string').toBe(true)
152 | })
153 | it('matches snapshot', async () => {
154 | const summary = await getReportSummary()
155 | expect(summary).toMatchSnapshot()
156 | })
157 | })
158 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Playwright report comment'
2 | description: 'Report Playwright test results as pull request comments'
3 | author: 'Philipp Daun '
4 |
5 | branding:
6 | icon: 'message-circle'
7 | color: 'purple'
8 |
9 | inputs:
10 | github-token:
11 | description: 'The GitHub access token'
12 | required: false
13 | default: ${{ github.token }}
14 | report-file:
15 | description: 'Path to the generated json report file'
16 | required: true
17 | report-tag:
18 | description: 'Tag reports to distinguish test runs in the same workflow instead of overwriting'
19 | required: false
20 | report-url:
21 | description: 'URL to the generated html report, if uploaded'
22 | required: false
23 | create-comment:
24 | description: 'Create a pull request comment with the test result summary'
25 | required: false
26 | default: 'true'
27 | comment-title:
28 | description: 'Customize the title of the pull request comment'
29 | required: false
30 | default: 'Playwright test results'
31 | custom-info:
32 | description: 'Additional information to include in the summary comment, markdown-formatted'
33 | required: false
34 | job-summary:
35 | description: 'Create a job summary comment for the workflow run'
36 | required: false
37 | default: 'false'
38 | icon-style:
39 | description: 'The icons to use: octicons or emoji'
40 | required: false
41 | default: 'octicons'
42 | test-command:
43 | description: 'The command used to run the tests'
44 | required: false
45 | footer:
46 | description: 'Additional content to add to the comment below the test report'
47 | required: false
48 | default: ''
49 |
50 | outputs:
51 | summary:
52 | description: 'The rendered markdown summary of the test report'
53 | comment-id:
54 | description: 'The ID of the comment that was created or updated'
55 | report-data:
56 | description: 'The raw data of the test report'
57 |
58 | runs:
59 | using: node20
60 | main: dist/index.js
61 |
--------------------------------------------------------------------------------
/assets/comment-failed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daun/playwright-report-summary/2fe141d67923f13be8fa607f6462aea837865492/assets/comment-failed.png
--------------------------------------------------------------------------------
/assets/comment-passed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daun/playwright-report-summary/2fe141d67923f13be8fa607f6462aea837865492/assets/comment-passed.png
--------------------------------------------------------------------------------
/dist/licenses.txt:
--------------------------------------------------------------------------------
1 | @actions/core
2 | MIT
3 | The MIT License (MIT)
4 |
5 | Copyright 2019 GitHub
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8 |
9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12 |
13 | @actions/exec
14 | MIT
15 | The MIT License (MIT)
16 |
17 | Copyright 2019 GitHub
18 |
19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
20 |
21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 |
25 | @actions/github
26 | MIT
27 | The MIT License (MIT)
28 |
29 | Copyright 2019 GitHub
30 |
31 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
32 |
33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
34 |
35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36 |
37 | @actions/http-client
38 | MIT
39 | Actions Http Client for Node.js
40 |
41 | Copyright (c) GitHub, Inc.
42 |
43 | All rights reserved.
44 |
45 | MIT License
46 |
47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
48 | associated documentation files (the "Software"), to deal in the Software without restriction,
49 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
50 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
51 | subject to the following conditions:
52 |
53 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
54 |
55 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
56 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
57 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
58 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
59 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
60 |
61 |
62 | @actions/io
63 | MIT
64 | The MIT License (MIT)
65 |
66 | Copyright 2019 GitHub
67 |
68 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
69 |
70 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
71 |
72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
73 |
74 | @fastify/busboy
75 | MIT
76 | Copyright Brian White. All rights reserved.
77 |
78 | Permission is hereby granted, free of charge, to any person obtaining a copy
79 | of this software and associated documentation files (the "Software"), to
80 | deal in the Software without restriction, including without limitation the
81 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
82 | sell copies of the Software, and to permit persons to whom the Software is
83 | furnished to do so, subject to the following conditions:
84 |
85 | The above copyright notice and this permission notice shall be included in
86 | all copies or substantial portions of the Software.
87 |
88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
89 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
90 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
91 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
92 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
94 | IN THE SOFTWARE.
95 |
96 | @octokit/auth-token
97 | MIT
98 | The MIT License
99 |
100 | Copyright (c) 2019 Octokit contributors
101 |
102 | Permission is hereby granted, free of charge, to any person obtaining a copy
103 | of this software and associated documentation files (the "Software"), to deal
104 | in the Software without restriction, including without limitation the rights
105 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
106 | copies of the Software, and to permit persons to whom the Software is
107 | furnished to do so, subject to the following conditions:
108 |
109 | The above copyright notice and this permission notice shall be included in
110 | all copies or substantial portions of the Software.
111 |
112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
115 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
116 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
117 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
118 | THE SOFTWARE.
119 |
120 |
121 | @octokit/core
122 | MIT
123 | The MIT License
124 |
125 | Copyright (c) 2019 Octokit contributors
126 |
127 | Permission is hereby granted, free of charge, to any person obtaining a copy
128 | of this software and associated documentation files (the "Software"), to deal
129 | in the Software without restriction, including without limitation the rights
130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
131 | copies of the Software, and to permit persons to whom the Software is
132 | furnished to do so, subject to the following conditions:
133 |
134 | The above copyright notice and this permission notice shall be included in
135 | all copies or substantial portions of the Software.
136 |
137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
143 | THE SOFTWARE.
144 |
145 |
146 | @octokit/endpoint
147 | MIT
148 | The MIT License
149 |
150 | Copyright (c) 2018 Octokit contributors
151 |
152 | Permission is hereby granted, free of charge, to any person obtaining a copy
153 | of this software and associated documentation files (the "Software"), to deal
154 | in the Software without restriction, including without limitation the rights
155 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
156 | copies of the Software, and to permit persons to whom the Software is
157 | furnished to do so, subject to the following conditions:
158 |
159 | The above copyright notice and this permission notice shall be included in
160 | all copies or substantial portions of the Software.
161 |
162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
168 | THE SOFTWARE.
169 |
170 |
171 | @octokit/graphql
172 | MIT
173 | The MIT License
174 |
175 | Copyright (c) 2018 Octokit contributors
176 |
177 | Permission is hereby granted, free of charge, to any person obtaining a copy
178 | of this software and associated documentation files (the "Software"), to deal
179 | in the Software without restriction, including without limitation the rights
180 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
181 | copies of the Software, and to permit persons to whom the Software is
182 | furnished to do so, subject to the following conditions:
183 |
184 | The above copyright notice and this permission notice shall be included in
185 | all copies or substantial portions of the Software.
186 |
187 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
188 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
189 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
190 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
191 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
192 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
193 | THE SOFTWARE.
194 |
195 |
196 | @octokit/plugin-paginate-rest
197 | MIT
198 | MIT License Copyright (c) 2019 Octokit contributors
199 |
200 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
201 |
202 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
203 |
204 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
205 |
206 |
207 | @octokit/plugin-rest-endpoint-methods
208 | MIT
209 | MIT License Copyright (c) 2019 Octokit contributors
210 |
211 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
212 |
213 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
214 |
215 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
216 |
217 |
218 | @octokit/request
219 | MIT
220 | The MIT License
221 |
222 | Copyright (c) 2018 Octokit contributors
223 |
224 | Permission is hereby granted, free of charge, to any person obtaining a copy
225 | of this software and associated documentation files (the "Software"), to deal
226 | in the Software without restriction, including without limitation the rights
227 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
228 | copies of the Software, and to permit persons to whom the Software is
229 | furnished to do so, subject to the following conditions:
230 |
231 | The above copyright notice and this permission notice shall be included in
232 | all copies or substantial portions of the Software.
233 |
234 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
235 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
236 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
237 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
238 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
239 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
240 | THE SOFTWARE.
241 |
242 |
243 | @octokit/request-error
244 | MIT
245 | The MIT License
246 |
247 | Copyright (c) 2019 Octokit contributors
248 |
249 | Permission is hereby granted, free of charge, to any person obtaining a copy
250 | of this software and associated documentation files (the "Software"), to deal
251 | in the Software without restriction, including without limitation the rights
252 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
253 | copies of the Software, and to permit persons to whom the Software is
254 | furnished to do so, subject to the following conditions:
255 |
256 | The above copyright notice and this permission notice shall be included in
257 | all copies or substantial portions of the Software.
258 |
259 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
260 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
261 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
262 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
263 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
264 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
265 | THE SOFTWARE.
266 |
267 |
268 | @vercel/ncc
269 | MIT
270 | Copyright 2018 ZEIT, Inc.
271 |
272 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
273 |
274 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
275 |
276 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
277 |
278 | before-after-hook
279 | Apache-2.0
280 | Apache License
281 | Version 2.0, January 2004
282 | http://www.apache.org/licenses/
283 |
284 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
285 |
286 | 1. Definitions.
287 |
288 | "License" shall mean the terms and conditions for use, reproduction,
289 | and distribution as defined by Sections 1 through 9 of this document.
290 |
291 | "Licensor" shall mean the copyright owner or entity authorized by
292 | the copyright owner that is granting the License.
293 |
294 | "Legal Entity" shall mean the union of the acting entity and all
295 | other entities that control, are controlled by, or are under common
296 | control with that entity. For the purposes of this definition,
297 | "control" means (i) the power, direct or indirect, to cause the
298 | direction or management of such entity, whether by contract or
299 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
300 | outstanding shares, or (iii) beneficial ownership of such entity.
301 |
302 | "You" (or "Your") shall mean an individual or Legal Entity
303 | exercising permissions granted by this License.
304 |
305 | "Source" form shall mean the preferred form for making modifications,
306 | including but not limited to software source code, documentation
307 | source, and configuration files.
308 |
309 | "Object" form shall mean any form resulting from mechanical
310 | transformation or translation of a Source form, including but
311 | not limited to compiled object code, generated documentation,
312 | and conversions to other media types.
313 |
314 | "Work" shall mean the work of authorship, whether in Source or
315 | Object form, made available under the License, as indicated by a
316 | copyright notice that is included in or attached to the work
317 | (an example is provided in the Appendix below).
318 |
319 | "Derivative Works" shall mean any work, whether in Source or Object
320 | form, that is based on (or derived from) the Work and for which the
321 | editorial revisions, annotations, elaborations, or other modifications
322 | represent, as a whole, an original work of authorship. For the purposes
323 | of this License, Derivative Works shall not include works that remain
324 | separable from, or merely link (or bind by name) to the interfaces of,
325 | the Work and Derivative Works thereof.
326 |
327 | "Contribution" shall mean any work of authorship, including
328 | the original version of the Work and any modifications or additions
329 | to that Work or Derivative Works thereof, that is intentionally
330 | submitted to Licensor for inclusion in the Work by the copyright owner
331 | or by an individual or Legal Entity authorized to submit on behalf of
332 | the copyright owner. For the purposes of this definition, "submitted"
333 | means any form of electronic, verbal, or written communication sent
334 | to the Licensor or its representatives, including but not limited to
335 | communication on electronic mailing lists, source code control systems,
336 | and issue tracking systems that are managed by, or on behalf of, the
337 | Licensor for the purpose of discussing and improving the Work, but
338 | excluding communication that is conspicuously marked or otherwise
339 | designated in writing by the copyright owner as "Not a Contribution."
340 |
341 | "Contributor" shall mean Licensor and any individual or Legal Entity
342 | on behalf of whom a Contribution has been received by Licensor and
343 | subsequently incorporated within the Work.
344 |
345 | 2. Grant of Copyright License. Subject to the terms and conditions of
346 | this License, each Contributor hereby grants to You a perpetual,
347 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
348 | copyright license to reproduce, prepare Derivative Works of,
349 | publicly display, publicly perform, sublicense, and distribute the
350 | Work and such Derivative Works in Source or Object form.
351 |
352 | 3. Grant of Patent License. Subject to the terms and conditions of
353 | this License, each Contributor hereby grants to You a perpetual,
354 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
355 | (except as stated in this section) patent license to make, have made,
356 | use, offer to sell, sell, import, and otherwise transfer the Work,
357 | where such license applies only to those patent claims licensable
358 | by such Contributor that are necessarily infringed by their
359 | Contribution(s) alone or by combination of their Contribution(s)
360 | with the Work to which such Contribution(s) was submitted. If You
361 | institute patent litigation against any entity (including a
362 | cross-claim or counterclaim in a lawsuit) alleging that the Work
363 | or a Contribution incorporated within the Work constitutes direct
364 | or contributory patent infringement, then any patent licenses
365 | granted to You under this License for that Work shall terminate
366 | as of the date such litigation is filed.
367 |
368 | 4. Redistribution. You may reproduce and distribute copies of the
369 | Work or Derivative Works thereof in any medium, with or without
370 | modifications, and in Source or Object form, provided that You
371 | meet the following conditions:
372 |
373 | (a) You must give any other recipients of the Work or
374 | Derivative Works a copy of this License; and
375 |
376 | (b) You must cause any modified files to carry prominent notices
377 | stating that You changed the files; and
378 |
379 | (c) You must retain, in the Source form of any Derivative Works
380 | that You distribute, all copyright, patent, trademark, and
381 | attribution notices from the Source form of the Work,
382 | excluding those notices that do not pertain to any part of
383 | the Derivative Works; and
384 |
385 | (d) If the Work includes a "NOTICE" text file as part of its
386 | distribution, then any Derivative Works that You distribute must
387 | include a readable copy of the attribution notices contained
388 | within such NOTICE file, excluding those notices that do not
389 | pertain to any part of the Derivative Works, in at least one
390 | of the following places: within a NOTICE text file distributed
391 | as part of the Derivative Works; within the Source form or
392 | documentation, if provided along with the Derivative Works; or,
393 | within a display generated by the Derivative Works, if and
394 | wherever such third-party notices normally appear. The contents
395 | of the NOTICE file are for informational purposes only and
396 | do not modify the License. You may add Your own attribution
397 | notices within Derivative Works that You distribute, alongside
398 | or as an addendum to the NOTICE text from the Work, provided
399 | that such additional attribution notices cannot be construed
400 | as modifying the License.
401 |
402 | You may add Your own copyright statement to Your modifications and
403 | may provide additional or different license terms and conditions
404 | for use, reproduction, or distribution of Your modifications, or
405 | for any such Derivative Works as a whole, provided Your use,
406 | reproduction, and distribution of the Work otherwise complies with
407 | the conditions stated in this License.
408 |
409 | 5. Submission of Contributions. Unless You explicitly state otherwise,
410 | any Contribution intentionally submitted for inclusion in the Work
411 | by You to the Licensor shall be under the terms and conditions of
412 | this License, without any additional terms or conditions.
413 | Notwithstanding the above, nothing herein shall supersede or modify
414 | the terms of any separate license agreement you may have executed
415 | with Licensor regarding such Contributions.
416 |
417 | 6. Trademarks. This License does not grant permission to use the trade
418 | names, trademarks, service marks, or product names of the Licensor,
419 | except as required for reasonable and customary use in describing the
420 | origin of the Work and reproducing the content of the NOTICE file.
421 |
422 | 7. Disclaimer of Warranty. Unless required by applicable law or
423 | agreed to in writing, Licensor provides the Work (and each
424 | Contributor provides its Contributions) on an "AS IS" BASIS,
425 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
426 | implied, including, without limitation, any warranties or conditions
427 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
428 | PARTICULAR PURPOSE. You are solely responsible for determining the
429 | appropriateness of using or redistributing the Work and assume any
430 | risks associated with Your exercise of permissions under this License.
431 |
432 | 8. Limitation of Liability. In no event and under no legal theory,
433 | whether in tort (including negligence), contract, or otherwise,
434 | unless required by applicable law (such as deliberate and grossly
435 | negligent acts) or agreed to in writing, shall any Contributor be
436 | liable to You for damages, including any direct, indirect, special,
437 | incidental, or consequential damages of any character arising as a
438 | result of this License or out of the use or inability to use the
439 | Work (including but not limited to damages for loss of goodwill,
440 | work stoppage, computer failure or malfunction, or any and all
441 | other commercial damages or losses), even if such Contributor
442 | has been advised of the possibility of such damages.
443 |
444 | 9. Accepting Warranty or Additional Liability. While redistributing
445 | the Work or Derivative Works thereof, You may choose to offer,
446 | and charge a fee for, acceptance of support, warranty, indemnity,
447 | or other liability obligations and/or rights consistent with this
448 | License. However, in accepting such obligations, You may act only
449 | on Your own behalf and on Your sole responsibility, not on behalf
450 | of any other Contributor, and only if You agree to indemnify,
451 | defend, and hold each Contributor harmless for any liability
452 | incurred by, or claims asserted against, such Contributor by reason
453 | of your accepting any such warranty or additional liability.
454 |
455 | END OF TERMS AND CONDITIONS
456 |
457 | APPENDIX: How to apply the Apache License to your work.
458 |
459 | To apply the Apache License to your work, attach the following
460 | boilerplate notice, with the fields enclosed by brackets "{}"
461 | replaced with your own identifying information. (Don't include
462 | the brackets!) The text should be enclosed in the appropriate
463 | comment syntax for the file format. We also recommend that a
464 | file or class name and description of purpose be included on the
465 | same "printed page" as the copyright notice for easier
466 | identification within third-party archives.
467 |
468 | Copyright 2018 Gregor Martynus and other contributors.
469 |
470 | Licensed under the Apache License, Version 2.0 (the "License");
471 | you may not use this file except in compliance with the License.
472 | You may obtain a copy of the License at
473 |
474 | http://www.apache.org/licenses/LICENSE-2.0
475 |
476 | Unless required by applicable law or agreed to in writing, software
477 | distributed under the License is distributed on an "AS IS" BASIS,
478 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
479 | See the License for the specific language governing permissions and
480 | limitations under the License.
481 |
482 |
483 | deprecation
484 | ISC
485 | The ISC License
486 |
487 | Copyright (c) Gregor Martynus and contributors
488 |
489 | Permission to use, copy, modify, and/or distribute this software for any
490 | purpose with or without fee is hereby granted, provided that the above
491 | copyright notice and this permission notice appear in all copies.
492 |
493 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
494 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
495 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
496 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
497 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
498 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
499 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
500 |
501 |
502 | is-plain-object
503 | MIT
504 | The MIT License (MIT)
505 |
506 | Copyright (c) 2014-2017, Jon Schlinkert.
507 |
508 | Permission is hereby granted, free of charge, to any person obtaining a copy
509 | of this software and associated documentation files (the "Software"), to deal
510 | in the Software without restriction, including without limitation the rights
511 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
512 | copies of the Software, and to permit persons to whom the Software is
513 | furnished to do so, subject to the following conditions:
514 |
515 | The above copyright notice and this permission notice shall be included in
516 | all copies or substantial portions of the Software.
517 |
518 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
519 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
520 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
521 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
522 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
523 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
524 | THE SOFTWARE.
525 |
526 |
527 | node-fetch
528 | MIT
529 | The MIT License (MIT)
530 |
531 | Copyright (c) 2016 David Frank
532 |
533 | Permission is hereby granted, free of charge, to any person obtaining a copy
534 | of this software and associated documentation files (the "Software"), to deal
535 | in the Software without restriction, including without limitation the rights
536 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
537 | copies of the Software, and to permit persons to whom the Software is
538 | furnished to do so, subject to the following conditions:
539 |
540 | The above copyright notice and this permission notice shall be included in all
541 | copies or substantial portions of the Software.
542 |
543 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
544 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
545 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
546 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
547 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
548 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
549 | SOFTWARE.
550 |
551 |
552 |
553 | once
554 | ISC
555 | The ISC License
556 |
557 | Copyright (c) Isaac Z. Schlueter and Contributors
558 |
559 | Permission to use, copy, modify, and/or distribute this software for any
560 | purpose with or without fee is hereby granted, provided that the above
561 | copyright notice and this permission notice appear in all copies.
562 |
563 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
564 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
565 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
566 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
567 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
568 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
569 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
570 |
571 |
572 | tr46
573 | MIT
574 |
575 | tunnel
576 | MIT
577 | The MIT License (MIT)
578 |
579 | Copyright (c) 2012 Koichi Kobayashi
580 |
581 | Permission is hereby granted, free of charge, to any person obtaining a copy
582 | of this software and associated documentation files (the "Software"), to deal
583 | in the Software without restriction, including without limitation the rights
584 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
585 | copies of the Software, and to permit persons to whom the Software is
586 | furnished to do so, subject to the following conditions:
587 |
588 | The above copyright notice and this permission notice shall be included in
589 | all copies or substantial portions of the Software.
590 |
591 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
592 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
593 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
594 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
595 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
596 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
597 | THE SOFTWARE.
598 |
599 |
600 | undici
601 | MIT
602 | MIT License
603 |
604 | Copyright (c) Matteo Collina and Undici contributors
605 |
606 | Permission is hereby granted, free of charge, to any person obtaining a copy
607 | of this software and associated documentation files (the "Software"), to deal
608 | in the Software without restriction, including without limitation the rights
609 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
610 | copies of the Software, and to permit persons to whom the Software is
611 | furnished to do so, subject to the following conditions:
612 |
613 | The above copyright notice and this permission notice shall be included in all
614 | copies or substantial portions of the Software.
615 |
616 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
617 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
618 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
619 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
620 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
621 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
622 | SOFTWARE.
623 |
624 |
625 | universal-user-agent
626 | ISC
627 | # [ISC License](https://spdx.org/licenses/ISC)
628 |
629 | Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m)
630 |
631 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
632 |
633 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
634 |
635 |
636 | webidl-conversions
637 | BSD-2-Clause
638 | # The BSD 2-Clause License
639 |
640 | Copyright (c) 2014, Domenic Denicola
641 | All rights reserved.
642 |
643 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
644 |
645 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
646 |
647 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
648 |
649 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
650 |
651 |
652 | whatwg-url
653 | MIT
654 | The MIT License (MIT)
655 |
656 | Copyright (c) 2015–2016 Sebastian Mayr
657 |
658 | Permission is hereby granted, free of charge, to any person obtaining a copy
659 | of this software and associated documentation files (the "Software"), to deal
660 | in the Software without restriction, including without limitation the rights
661 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
662 | copies of the Software, and to permit persons to whom the Software is
663 | furnished to do so, subject to the following conditions:
664 |
665 | The above copyright notice and this permission notice shall be included in
666 | all copies or substantial portions of the Software.
667 |
668 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
669 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
670 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
671 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
672 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
673 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
674 | THE SOFTWARE.
675 |
676 |
677 | wrappy
678 | ISC
679 | The ISC License
680 |
681 | Copyright (c) Isaac Z. Schlueter and Contributors
682 |
683 | Permission to use, copy, modify, and/or distribute this software for any
684 | purpose with or without fee is hereby granted, provided that the above
685 | copyright notice and this permission notice appear in all copies.
686 |
687 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
688 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
689 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
690 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
691 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
692 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
693 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
694 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playwright-report-summary",
3 | "description": "A GitHub action to report Playwright test results as pull-request comments",
4 | "version": "3.3.0",
5 | "author": "Philipp Daun ",
6 | "homepage": "https://github.com/daun/playwright-report-summary",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/daun/playwright-report-summary.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/daun/playwright-report-summary/issues"
13 | },
14 | "keywords": [
15 | "actions",
16 | "node",
17 | "testing",
18 | "reporting",
19 | "test report",
20 | "playwright",
21 | "pull request"
22 | ],
23 | "exports": {
24 | ".": "./dist/index.js"
25 | },
26 | "engines": {
27 | "node": ">=16"
28 | },
29 | "scripts": {
30 | "bundle": "npm run format:write && npm run package",
31 | "ci-test": "jest",
32 | "format:write": "prettier --write '**/*.ts'",
33 | "format:check": "prettier --check '**/*.ts'",
34 | "lint": "npx eslint . -c ./.github/linters/.eslintrc.yml",
35 | "package": "ncc build src/index.ts --license licenses.txt",
36 | "package:watch": "npm run package -- --watch",
37 | "test": "jest",
38 | "test:update": "jest -u",
39 | "all": "npm run format:write && npm run lint && npm run test && npm run package"
40 | },
41 | "license": "MIT",
42 | "jest": {
43 | "preset": "ts-jest",
44 | "verbose": true,
45 | "clearMocks": true,
46 | "testEnvironment": "node",
47 | "moduleFileExtensions": [
48 | "js",
49 | "ts"
50 | ],
51 | "testMatch": [
52 | "**/*.test.ts"
53 | ],
54 | "testPathIgnorePatterns": [
55 | "/node_modules/",
56 | "/dist/"
57 | ],
58 | "transform": {
59 | "^.+\\.ts$": "ts-jest"
60 | },
61 | "coverageReporters": [
62 | "json-summary",
63 | "text",
64 | "lcov"
65 | ],
66 | "collectCoverage": true,
67 | "collectCoverageFrom": [
68 | "./src/**"
69 | ]
70 | },
71 | "dependencies": {
72 | "@actions/core": "^1.10.1",
73 | "@actions/github": "^5.1.1"
74 | },
75 | "devDependencies": {
76 | "@octokit/openapi-types": "^22.0.0",
77 | "@playwright/test": "^1.50.1",
78 | "@types/jest": "^29.5.13",
79 | "@types/node": "^22.15.29",
80 | "@typescript-eslint/eslint-plugin": "^7.14.1",
81 | "@typescript-eslint/parser": "^7.18.0",
82 | "@vercel/ncc": "^0.38.0",
83 | "eslint": "^8.49.0",
84 | "eslint-plugin-github": "^5.1.8",
85 | "eslint-plugin-jest": "^28.8.3",
86 | "eslint-plugin-jsonc": "^2.20.1",
87 | "eslint-plugin-prettier": "^5.2.3",
88 | "jest": "^29.7.0",
89 | "js-yaml": "^4.1.0",
90 | "prettier": "^3.5.3",
91 | "prettier-eslint": "^16.3.0",
92 | "ts-jest": "^29.2.5",
93 | "typescript": "^5.7.3"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/formatting.ts:
--------------------------------------------------------------------------------
1 | export function renderMarkdownTable(rows: string[][], headers: string[] = []): string {
2 | if (!rows.length) {
3 | return ''
4 | }
5 | const align = [':---', ':---:', ':---:', ':---:'].slice(0, rows[0].length)
6 | const lines = [headers, align, ...rows].filter(Boolean)
7 | return lines.map((columns) => `| ${columns.join(' | ')} |`).join('\n')
8 | }
9 |
10 | export function renderAccordion(summary: string, content: string, { open = false }: { open?: boolean } = {}): string {
11 | summary = `${summary}`
12 | content = `\n\n${content.trim()}\n\n`
13 | return `${summary}\n\n${content.trim()}\n\n `
14 | }
15 |
16 | export function renderCodeBlock(code: string, lang = ''): string {
17 | return `\`\`\`${lang}\n${code}\n\`\`\``
18 | }
19 |
20 | export function formatDuration(milliseconds: number): string {
21 | const SECOND = 1000
22 | const MINUTE = 60 * SECOND
23 | const HOUR = 60 * MINUTE
24 | const DAY = 24 * HOUR
25 |
26 | let remaining = milliseconds
27 |
28 | const days = Math.floor(remaining / DAY)
29 | remaining %= DAY
30 |
31 | const hours = Math.floor(remaining / HOUR)
32 | remaining %= HOUR
33 |
34 | const minutes = Math.floor(remaining / MINUTE)
35 | remaining %= MINUTE
36 |
37 | const seconds = +(remaining / SECOND).toFixed(minutes ? 0 : 1)
38 |
39 | return [
40 | days && `${days} ${n('day', days)}`,
41 | hours && `${hours} ${n('hour', hours)}`,
42 | minutes && `${minutes} ${n('minute', minutes)}`,
43 | seconds && `${seconds} ${n('second', seconds)}`
44 | ]
45 | .filter(Boolean)
46 | .join(', ')
47 | }
48 |
49 | export function upperCaseFirst(str: string): string {
50 | return str.charAt(0).toUpperCase() + str.slice(1)
51 | }
52 |
53 | export function n(str: string, count: number): string {
54 | return count === 1 ? str : `${str}s`
55 | }
56 |
--------------------------------------------------------------------------------
/src/fs.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs/promises'
2 |
3 | export async function fileExists(filename: string): Promise {
4 | try {
5 | await fs.access(filename, fs.constants.F_OK)
6 | return true
7 | } catch (e) {
8 | return false
9 | }
10 | }
11 |
12 | export async function readFile(path: string): Promise {
13 | return await fs.readFile(path, { encoding: 'utf8' })
14 | }
15 |
--------------------------------------------------------------------------------
/src/github.ts:
--------------------------------------------------------------------------------
1 | import { getOctokit } from '@actions/github'
2 |
3 | // eslint-disable-next-line import/no-unresolved
4 | import { components } from '@octokit/openapi-types'
5 |
6 | type IssueComment = components['schemas']['issue-comment']
7 | type PullRequestReview = components['schemas']['pull-request-review']
8 | type Octokit = ReturnType
9 |
10 | export async function getPullRequestInfo(
11 | octokit: Octokit,
12 | params: { owner: string; repo: string; pull_number: number }
13 | ): Promise<{ ref: string; sha: string }> {
14 | const { data: pr } = await octokit.rest.pulls.get(params)
15 | const { ref } = pr.base
16 | const { sha } = pr.head
17 | return { ref, sha }
18 | }
19 |
20 | export async function getIssueComments(
21 | octokit: Octokit,
22 | params: { owner: string; repo: string; issue_number: number }
23 | ): Promise {
24 | const { data: comments } = await octokit.rest.issues.listComments(params)
25 | return comments
26 | }
27 |
28 | export async function createIssueComment(
29 | octokit: Octokit,
30 | params: { owner: string; repo: string; issue_number: number; body: string }
31 | ): Promise {
32 | const { data: comment } = await octokit.rest.issues.createComment(params)
33 | return comment
34 | }
35 |
36 | export async function updateIssueComment(
37 | octokit: Octokit,
38 | params: { owner: string; repo: string; comment_id: number; body: string }
39 | ): Promise {
40 | const { data: comment } = await octokit.rest.issues.updateComment(params)
41 | return comment
42 | }
43 |
44 | export async function createPullRequestReview(
45 | octokit: Octokit,
46 | params: { owner: string; repo: string; pull_number: number; body: string }
47 | ): Promise {
48 | const { data: review } = await octokit.rest.pulls.createReview({ ...params, event: 'COMMENT' })
49 | return review
50 | }
51 |
--------------------------------------------------------------------------------
/src/icons.ts:
--------------------------------------------------------------------------------
1 | type IconSet = Record
2 | type IconColors = Record
3 |
4 | const iconSize = 14
5 | const defaultIconStyle = 'octicons'
6 |
7 | export const icons: Record = {
8 | octicons: {
9 | failed: 'stop',
10 | passed: 'check-circle',
11 | flaky: 'alert',
12 | skipped: 'skip',
13 | stats: 'pulse',
14 | duration: 'clock',
15 | link: 'link-external',
16 | report: 'package',
17 | commit: 'git-pull-request',
18 | info: 'info'
19 | },
20 | emojis: {
21 | failed: '❌',
22 | passed: '✅',
23 | flaky: '⚠️',
24 | skipped: '⏭️',
25 | stats: '',
26 | duration: '',
27 | link: '',
28 | report: '',
29 | commit: '',
30 | info: 'ℹ️'
31 | }
32 | }
33 |
34 | const iconColors: IconColors = {
35 | failed: 'da3633',
36 | passed: '3fb950',
37 | flaky: 'd29922',
38 | skipped: '0967d9',
39 | icon: 'abb4bf'
40 | }
41 |
42 | export function renderIcon(
43 | status: string,
44 | { iconStyle = defaultIconStyle }: { iconStyle?: keyof typeof icons } = {}
45 | ): string {
46 | if (iconStyle === 'emojis') {
47 | return icons.emojis[status] || ''
48 | } else {
49 | const color = iconColors[status] || iconColors.icon
50 | return createOcticonUrl(icons.octicons[status], { label: status, color })
51 | }
52 | }
53 |
54 | function createOcticonUrl(icon: string, { label = 'icon', color = iconColors.icon, size = iconSize } = {}): string {
55 | if (icon) {
56 | return ``
57 | } else {
58 | return ''
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import {
3 | getInput,
4 | getBooleanInput,
5 | setOutput,
6 | setFailed,
7 | startGroup,
8 | endGroup,
9 | debug,
10 | warning,
11 | summary as setSummary
12 | } from '@actions/core'
13 | import { context, getOctokit } from '@actions/github'
14 | import { fileExists, readFile } from './fs'
15 | import { parseReport, renderReportSummary, getCommitUrl } from './report'
16 | import {
17 | getIssueComments,
18 | createIssueComment,
19 | updateIssueComment,
20 | createPullRequestReview,
21 | getPullRequestInfo
22 | } from './github'
23 |
24 | /**
25 | * The main function for the action.
26 | */
27 | export async function run(): Promise {
28 | try {
29 | await report()
30 | } catch (error) {
31 | if (error instanceof Error) {
32 | setFailed(error.message)
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Parse the Playwright report and post a comment on the PR.
39 | */
40 | export async function report(): Promise {
41 | const cwd = process.cwd()
42 |
43 | const {
44 | workflow,
45 | eventName,
46 | repo: { owner, repo },
47 | payload
48 | } = context
49 | const { number: issueNumber } = context.issue || {}
50 |
51 | const token = getInput('github-token')
52 | const reportFile = getInput('report-file', { required: true })
53 | const reportUrl = getInput('report-url')
54 | const reportTag = getInput('report-tag') || workflow
55 | const commentTitle = getInput('comment-title') || 'Playwright test results'
56 | const customInfo = getInput('custom-info')
57 | const iconStyle = getInput('icon-style') || 'octicons'
58 | const createComment = getInput('create-comment') ? getBooleanInput('create-comment') : true
59 | const createJobSummary = getInput('job-summary') ? getBooleanInput('job-summary') : false
60 | const testCommand = getInput('test-command')
61 | const footer = getInput('footer')
62 |
63 | debug(`Report file: ${reportFile}`)
64 | debug(`Report url: ${reportUrl || '(none)'}`)
65 | debug(`Report tag: ${reportTag || '(none)'}`)
66 | debug(`Comment title: ${commentTitle}`)
67 | debug(`Creating comment? ${createComment ? 'yes' : 'no'}`)
68 | debug(`Creating job summary? ${createJobSummary ? 'yes' : 'no'}`)
69 |
70 | let ref: string = context.ref
71 | let sha: string = context.sha
72 | let pr: number | null = null
73 | let commitUrl: string | undefined
74 |
75 | const octokit = getOctokit(token)
76 |
77 | switch (eventName) {
78 | case 'push':
79 | ref = payload.ref
80 | sha = payload.after
81 | commitUrl = getCommitUrl(payload.repository?.html_url, sha)
82 | console.log(`Commit pushed onto ${ref} (${sha})`)
83 | break
84 |
85 | case 'pull_request':
86 | case 'pull_request_target':
87 | ref = payload.pull_request?.base?.ref
88 | sha = payload.pull_request?.head?.sha
89 | pr = issueNumber
90 | commitUrl = getCommitUrl(payload.repository?.html_url, sha)
91 | console.log(`PR #${pr} targeting ${ref} (${sha})`)
92 | break
93 |
94 | case 'issue_comment':
95 | if (payload.issue?.pull_request) {
96 | pr = issueNumber
97 | ;({ ref, sha } = await getPullRequestInfo(octokit, { owner, repo, pull_number: pr }))
98 | console.log(`Comment on PR #${pr} targeting ${ref} (${sha})`)
99 | } else {
100 | console.log(`Comment on issue #${issueNumber}`)
101 | }
102 | break
103 |
104 | case 'workflow_dispatch':
105 | console.log(`Workflow dispatched on ${ref} (${sha})`)
106 | break
107 |
108 | default:
109 | console.warn(`Unsupported event type: ${eventName}`)
110 | break
111 | }
112 |
113 | const reportPath = path.resolve(cwd, reportFile)
114 | const reportExists = await fileExists(reportPath)
115 | if (!reportExists) {
116 | debug(`Failed to find report file at path ${reportPath}`)
117 | throw new Error(
118 | `Report file ${reportFile} not found. Make sure Playwright is configured to generate a JSON report.`
119 | )
120 | }
121 |
122 | const data = await readFile(reportPath)
123 | const report = parseReport(data)
124 | const summary = renderReportSummary(report, {
125 | commit: sha,
126 | commitUrl,
127 | title: commentTitle,
128 | customInfo,
129 | reportUrl,
130 | iconStyle,
131 | testCommand,
132 | footer
133 | })
134 |
135 | let commentId = null
136 |
137 | if (createComment) {
138 | const prefix = ``
139 | const body = `${prefix}\n\n${summary}`
140 |
141 | if (!pr) {
142 | console.log('No PR associated with this action run. Not posting a check or comment.')
143 | } else {
144 | startGroup(`Commenting test report on PR`)
145 | try {
146 | const comments = await getIssueComments(octokit, { owner, repo, issue_number: pr })
147 | const existingComment = comments.findLast((c) => c.body?.includes(prefix))
148 | commentId = existingComment?.id || null
149 | } catch (error: unknown) {
150 | console.error(`Error fetching existing comments: ${(error as Error).message}`)
151 | }
152 |
153 | if (commentId) {
154 | console.log(`Found previous comment #${commentId}`)
155 | try {
156 | await updateIssueComment(octokit, { owner, repo, comment_id: commentId, body })
157 | console.log(`Updated previous comment #${commentId}`)
158 | } catch (error: unknown) {
159 | console.error(`Error updating previous comment: ${(error as Error).message}`)
160 | commentId = null
161 | }
162 | }
163 |
164 | if (!commentId) {
165 | console.log('Creating new comment')
166 | try {
167 | const newComment = await createIssueComment(octokit, { owner, repo, issue_number: pr, body })
168 | commentId = newComment.id
169 | console.log(`Created new comment #${commentId}`)
170 | } catch (error: unknown) {
171 | console.error(`Error creating comment: ${(error as Error).message}`)
172 | console.log(`Submitting PR review comment instead...`)
173 | try {
174 | const { issue } = context
175 | const review = await createPullRequestReview(octokit, {
176 | owner,
177 | repo: issue.repo,
178 | pull_number: issue.number,
179 | body
180 | })
181 | console.log(`Created pull request review: #${review.id}`)
182 | } catch (error: unknown) {
183 | console.error(`Error creating PR review: ${(error as Error).message}`)
184 | }
185 | }
186 | }
187 | endGroup()
188 | }
189 |
190 | if (!commentId && pr) {
191 | const intro = `Unable to comment on your PR — this can happen for PR's originating from a fork without write permissions. You can copy the test results directly into a comment using the markdown summary below:`
192 | warning(`${intro}\n\n${body}`, { title: 'Unable to comment on PR' })
193 | }
194 | }
195 |
196 | if (createJobSummary) {
197 | setSummary.addRaw(summary).write()
198 | }
199 |
200 | setOutput('summary', summary)
201 | setOutput('comment-id', commentId)
202 | setOutput('report-data', JSON.stringify(report))
203 | }
204 |
205 | if (process.env.GITHUB_ACTIONS === 'true') {
206 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
207 | run()
208 | }
209 |
--------------------------------------------------------------------------------
/src/report.ts:
--------------------------------------------------------------------------------
1 | import {
2 | JSONReport,
3 | JSONReportSpec,
4 | JSONReportSuite,
5 | JSONReportTest,
6 | JSONReportTestResult
7 | } from '@playwright/test/reporter'
8 | import { debug } from '@actions/core'
9 |
10 | import { formatDuration, n, renderAccordion, renderCodeBlock, upperCaseFirst } from './formatting'
11 | import { icons, renderIcon } from './icons'
12 |
13 | export interface ReportSummary {
14 | version: string
15 | started: Date
16 | duration: number
17 | workers: number
18 | shards: number
19 | projects: string[]
20 | files: string[]
21 | suites: SuiteSummary[]
22 | specs: SpecSummary[]
23 | tests: TestSummary[]
24 | failed: TestSummary[]
25 | passed: TestSummary[]
26 | flaky: TestSummary[]
27 | skipped: TestSummary[]
28 | results: TestResultSummary[]
29 | }
30 |
31 | interface SuiteSummary {
32 | file: string
33 | line: number
34 | column: number
35 | path: string[]
36 | title: string
37 | level: number
38 | root: boolean
39 | specs: SpecSummary[]
40 | }
41 |
42 | interface SpecSummary {
43 | ok: boolean
44 | file: string
45 | line: number
46 | column: number
47 | path: string[]
48 | title: string
49 | tests: TestSummary[]
50 | }
51 |
52 | interface TestSummary {
53 | passed: boolean
54 | failed: boolean
55 | flaky: boolean
56 | skipped: boolean
57 | file: string
58 | line: number
59 | column: number
60 | path: string[]
61 | title: string
62 | results: TestResultSummary[]
63 | }
64 |
65 | interface TestResultSummary {
66 | duration: number
67 | started: Date
68 | }
69 |
70 | interface ReportRenderOptions {
71 | commit?: string
72 | commitUrl?: string
73 | message?: string
74 | title?: string
75 | customInfo?: string
76 | reportUrl?: string
77 | iconStyle?: keyof typeof icons
78 | testCommand?: string
79 | footer?: string
80 | }
81 |
82 | export function isValidReport(report: unknown): report is JSONReport {
83 | return report !== null && typeof report === 'object' && 'config' in report && 'errors' in report && 'suites' in report
84 | }
85 |
86 | export function makeReport(data: string): JSONReport {
87 | const report: JSONReport = JSON.parse(data)
88 | if (isValidReport(report)) {
89 | return report
90 | } else {
91 | debug('Invalid report file')
92 | debug(data)
93 | throw new Error('Invalid JSON report file')
94 | }
95 | }
96 |
97 | export function parseReport(data: string): ReportSummary {
98 | const report = makeReport(data)
99 | const files = parseReportFiles(report)
100 | const allSuites = parseReportSuites(report)
101 | const suites = allSuites.filter((suite) => suite.root)
102 | const specs = allSuites.flatMap((suite) => suite.specs)
103 |
104 | const tests = specs.flatMap((spec) => spec.tests)
105 | const results = tests.flatMap((test) => test.results)
106 | const failed = tests.filter((test) => test.failed)
107 | const passed = tests.filter((test) => test.passed)
108 | const flaky = tests.filter((test) => test.flaky)
109 | const skipped = tests.filter((test) => test.skipped)
110 |
111 | const { duration, started } = getTotalDuration(report, results)
112 | const version: string = report.config.version
113 | const workers: number = report.config.metadata.actualWorkers || report.config.workers || 1
114 | const shards: number = report.config.shard?.total || 0
115 | const projects: string[] = report.config.projects.map((p) => p.name)
116 |
117 | return {
118 | version,
119 | started,
120 | duration,
121 | workers,
122 | shards,
123 | projects,
124 | files,
125 | suites,
126 | specs,
127 | tests,
128 | results,
129 | failed,
130 | passed,
131 | flaky,
132 | skipped
133 | }
134 | }
135 |
136 | export function parseReportFiles({ suites }: JSONReport): string[] {
137 | return suites.map((suite) => suite.file)
138 | }
139 |
140 | export function parseReportSuites({ suites }: JSONReport): SuiteSummary[] {
141 | return suites.map((suite) => parseSuite(suite))
142 | }
143 |
144 | function parseSuite(suite: JSONReportSuite, parents: string[] = []): SuiteSummary {
145 | const { file, line, column } = suite
146 | const { title, path } = buildTitle(...parents, suite.title)
147 | const level = parents.length
148 | const root = level === 0
149 |
150 | const directSpecs = (suite.specs ?? []).map((spec) => parseSpec(spec, [...parents, suite.title]))
151 | const nestedSuites = (suite.suites ?? []).map((child) => parseSuite(child, [...parents, suite.title]))
152 | const nestedSpecs = nestedSuites.flatMap((suite) => suite.specs)
153 | const specs = [...nestedSpecs, ...directSpecs]
154 |
155 | return { file, line, column, path, title, level, root, specs }
156 | }
157 |
158 | function parseSpec(spec: JSONReportSpec, parents: string[] = []): SpecSummary {
159 | const { ok, file, line, column } = spec
160 | const { title, path } = buildTitle(...parents, spec.title)
161 | const tests = spec.tests.map((test) => parseTest(test, spec, parents))
162 | return { ok, file, line, column, path, title, tests }
163 | }
164 |
165 | function parseTest(test: JSONReportTest, spec: JSONReportSpec, parents: string[] = []): TestSummary {
166 | const { file, line, column } = spec
167 | const { status, projectName: project } = test
168 | const { title, path } = buildTitle(project, ...parents, spec.title)
169 | const results = test.results.map((result) => parseTestResult(result))
170 | const passed = status === 'expected'
171 | const failed = status === 'unexpected'
172 | const skipped = status === 'skipped'
173 | const flaky = status === 'flaky'
174 | return { passed, failed, flaky, skipped, results, title, path, file, line, column }
175 | }
176 |
177 | function parseTestResult({ duration, startTime }: JSONReportTestResult): TestResultSummary {
178 | return { duration, started: new Date(startTime) }
179 | }
180 |
181 | export function buildTitle(...paths: string[]): { title: string; path: string[] } {
182 | const path = paths.filter(Boolean)
183 | const title = path.join(' › ')
184 | return { title, path }
185 | }
186 |
187 | export function renderReportSummary(
188 | report: ReportSummary,
189 | { commit, commitUrl, message, title, customInfo, reportUrl, iconStyle, testCommand, footer }: ReportRenderOptions = {}
190 | ): string {
191 | const { duration, failed, passed, flaky, skipped } = report
192 | const icon = (symbol: string): string => renderIcon(symbol, { iconStyle })
193 | const paragraphs = []
194 |
195 | // Title
196 |
197 | paragraphs.push(`### ${title}`)
198 |
199 | // Passed/failed tests
200 |
201 | const tests = [
202 | failed.length ? `${icon('failed')} **${failed.length} failed**` : ``,
203 | passed.length ? `${icon('passed')} **${passed.length} passed** ` : ``,
204 | flaky.length ? `${icon('flaky')} **${flaky.length} flaky** ` : ``,
205 | skipped.length ? `${icon('skipped')} **${skipped.length} skipped**` : ``
206 | ]
207 | paragraphs.push(tests.filter(Boolean).join(' \n'))
208 |
209 | // Stats about test run
210 |
211 | paragraphs.push(`#### Details`)
212 |
213 | const shortCommit = commit?.slice(0, 7)
214 | const commitText = commitUrl ? `[${shortCommit}](${commitUrl})` : shortCommit
215 |
216 | const stats = [
217 | reportUrl ? `${icon('report')} [Open report ↗︎](${reportUrl})` : '',
218 | `${icon('stats')} ${report.tests.length} ${n('test', report.tests.length)} across ${report.suites.length} ${n(
219 | 'suite',
220 | report.suites.length
221 | )}`,
222 | `${icon('duration')} ${duration ? formatDuration(duration) : 'unknown'}`,
223 | commitText && message ? `${icon('commit')} ${message} (${commitText})` : '',
224 | commitText && !message ? `${icon('commit')} ${commitText}` : '',
225 | customInfo ? `${icon('info')} ${customInfo}` : ''
226 | ]
227 | paragraphs.push(stats.filter(Boolean).join(' \n'))
228 |
229 | // Lists of failed/skipped tests
230 |
231 | const listStatuses = ['failed', 'flaky', 'skipped'] as const
232 | const details = listStatuses.map((status) => {
233 | const tests = report[status]
234 | if (tests.length) {
235 | const summary = `${upperCaseFirst(status)} tests`
236 | const content = renderTestList(tests, status !== 'skipped' ? testCommand : undefined)
237 | const open = status === 'failed'
238 | return renderAccordion(summary, content, { open })
239 | }
240 | })
241 | paragraphs.push(
242 | details
243 | .filter(Boolean)
244 | .map((md) => (md as string).trim())
245 | .join('\n')
246 | )
247 |
248 | if (footer) {
249 | paragraphs.push(footer)
250 | }
251 |
252 | return paragraphs
253 | .map((p) => p.trim())
254 | .filter(Boolean)
255 | .join('\n\n')
256 | }
257 |
258 | function renderTestList(tests: TestSummary[], testCommand: string | undefined): string {
259 | const list = tests.map((test) => ` ${test.title}`).join('\n')
260 | if (!testCommand) {
261 | return list
262 | }
263 |
264 | const testIds = tests.map((test) => `${test.file}:${test.line}`).join(' ')
265 | const command = `${testCommand} ${testIds}`
266 |
267 | return `${list}\n\n${renderCodeBlock(command)}`
268 | }
269 |
270 | function getTotalDuration(report: JSONReport, results: TestResultSummary[]): { duration: number; started: Date } {
271 | let duration = 0
272 | let started = new Date()
273 | const { totalTime } = report.config.metadata
274 | if (totalTime) {
275 | duration = totalTime
276 | } else {
277 | const sorted = results.sort((a, b) => a.started.getTime() - b.started.getTime())
278 | const first = sorted[0]
279 | const last = sorted[sorted.length - 1]
280 | if (first && last) {
281 | started = first.started
282 | duration = last.started.getTime() + last.duration - first.started.getTime()
283 | }
284 | }
285 | return { duration, started }
286 | }
287 |
288 | export function getCommitUrl(repoUrl: string | undefined, sha: string): string | undefined {
289 | return repoUrl ? `${repoUrl}/commit/${sha}` : undefined
290 | }
291 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "module": "nodenext",
6 | "rootDir": "./src",
7 | "moduleResolution": "nodenext",
8 | "baseUrl": "./",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "noImplicitAny": true,
12 | "esModuleInterop": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "strict": true,
15 | "skipLibCheck": true,
16 | "newLine": "lf"
17 | },
18 | "exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"]
19 | }
20 |
--------------------------------------------------------------------------------