├── .github
├── funding.yml
└── workflows
│ ├── main.yml
│ └── test.yaml
├── .gitignore
├── LICENSE
├── README.md
├── action.yml
├── dist
└── index.js
├── package-lock.json
├── package.json
├── src
├── create-comment.js
└── index.js
├── style.css
└── test
└── create-comment.test.js
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | github: []
2 | patreon: bartveneman
3 | open_collective: projectwallace
4 | custom: ['https://www.projectwallace.com/sponsor', 'https://www.paypal.me/bartveneman']
5 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CSS Quality
2 |
3 | on:
4 | pull_request:
5 | branches: [master]
6 |
7 | jobs:
8 | cssDiff:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: CSS Analytics Diff
14 | uses: ./
15 | with:
16 | project-wallace-token: ${{ secrets.PROJECT_WALLACE_TOKEN }}
17 | github-token: ${{ secrets.GITHUB_TOKEN }}
18 | css-path: ./style.css
19 | post-pr-comment: true
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Use Node
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: 12.x
15 | - name: Install dependencies
16 | run: npm ci
17 | - name: Run tests
18 | run: npm test
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Bart Veneman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project Wallace Diff Github Action
2 |
3 | This GitHub actions posts your CSS to [projectwallace.com](https://www.projectwallace.com?ref=gh-diff-action), calculates the change between the current state of your project and your PR, and comments the diff in the PR.
4 |
5 | 
6 |
7 | ## Usage
8 |
9 | ### Inputs
10 |
11 | | Name | Required | Example | Description |
12 | | ----------------------- | ---------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13 | | `github-token` | _required_ | `github-token: ${{ secrets.GITHUB_TOKEN }}` | This Action uses this token to post a comment with the diff. |
14 | | `project-wallace-token` | _required_ | `project-wallace-token: ${{ secrets.PROJECT_WALLACE_TOKEN }}` | The webhook token for your project on projectwallace.com. You can find this token in the project settings. You must add this token to your [repository secrets](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets) first! |
15 | | `css-path` | _required_ | `css-path: ./build/style.css` | Path to the CSS file that should be analyzed and compared to the data on projectwallace.com. |
16 | | `post-pr-comment` | _optional_ | `true` | Whether this action should post a comment to the PR with changes |
17 |
18 | ### Example
19 |
20 | ```yaml
21 | name: CSS Workflow
22 |
23 | on:
24 | pull_request: # only run this action on pull requests
25 | branches: [master] # and only to the master branch
26 |
27 | jobs:
28 | cssDiff:
29 | runs-on: ubuntu-latest
30 |
31 | steps:
32 | - uses: actions/checkout@v2
33 | - name: Project Wallace Diff
34 | uses: projectwallace/css-diff-action@master
35 | with:
36 | project-wallace-token: ${{ secrets.PROJECT_WALLACE_TOKEN }}
37 | github-token: ${{ secrets.GITHUB_TOKEN }}
38 | css-path: ./build/style.css
39 | ```
40 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'CSS Analytics Diff'
2 | description: 'Take CSS and get the changes per CSS Analytics metric'
3 | author: 'Bart Veneman (projectwallace.com)'
4 |
5 | inputs:
6 | github-token:
7 | required: true
8 | project-wallace-token:
9 | description: 'Webhook token of the project on projectwallace.com'
10 | required: true
11 | css-path:
12 | description: 'Path to the CSS you want analyzed'
13 | required: true
14 | post-pr-comment:
15 | description: Whether this action should post a comment to the PR with changes
16 | required: true
17 | default: true
18 |
19 | runs:
20 | using: 'node12'
21 | main: 'dist/index.js'
22 |
23 | branding:
24 | color: 'blue'
25 | icon: 'bar-chart-2'
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@projectwallace/css-diff-action",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "GitHub Action to report the CSS Analytics diff in a PR",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "test": "ava",
9 | "build": "ncc build src/index.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/bartveneman/gh-action-css-diff.git"
14 | },
15 | "keywords": [],
16 | "author": "Bart Veneman",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/bartveneman/gh-action-css-diff/issues"
20 | },
21 | "homepage": "https://github.com/bartveneman/gh-action-css-diff#readme",
22 | "devDependencies": {
23 | "@actions/core": "^1.6.0",
24 | "@actions/github": "^2.2.0",
25 | "@vercel/ncc": "^0.33.3",
26 | "ava": "^3.5.1",
27 | "got": "^10.7.0"
28 | },
29 | "dependencies": {
30 | "@sentry/node": "^7.16.0",
31 | "pretty-bytes": "^5.3.0"
32 | },
33 | "prettier": {
34 | "trailingComma": "es5",
35 | "bracketSpacing": true,
36 | "arrowParens": "always",
37 | "semi": false,
38 | "useTabs": true,
39 | "singleQuote": true
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/create-comment.js:
--------------------------------------------------------------------------------
1 | const prettyBytes = require('pretty-bytes')
2 |
3 | const DEPRECATED_METRICS = ['stylesheets.size', 'atrules.fontfaces.unique', 'atrules.fontface.unique']
4 |
5 | function formatNumber(number) {
6 | return Number.isInteger(number)
7 | ? new Intl.NumberFormat().format(number)
8 | : parseFloat(number).toFixed(3)
9 | }
10 |
11 | function formatFilesize(number, relative = false) {
12 | if (
13 | number === null ||
14 | Number.isNaN(number) ||
15 | typeof number === 'undefined'
16 | ) {
17 | return ''
18 | }
19 |
20 | return prettyBytes(number, { signed: relative })
21 | }
22 |
23 | function formatDiff(number) {
24 | if (
25 | number === null ||
26 | Number.isNaN(number) ||
27 | typeof number === 'undefined'
28 | ) {
29 | return ''
30 | }
31 |
32 | if (number === 0) {
33 | return 0
34 | }
35 |
36 | return number > 0 ? `+${formatNumber(number)}` : formatNumber(number)
37 | }
38 |
39 | function formatPercentage(number, decimals = 2) {
40 | if (
41 | number === null ||
42 | Number.isNaN(number) ||
43 | typeof number === 'undefined'
44 | ) {
45 | return ''
46 | }
47 |
48 | if (number === Infinity) {
49 | return '∞'
50 | }
51 |
52 | return `${number > 0 ? '+' : ''}${new Intl.NumberFormat('en-US', {
53 | style: 'percent',
54 | maximumFractionDigits: decimals,
55 | }).format(number)}`
56 | }
57 |
58 | function formatListItem({ key, value }) {
59 | if (key === 'atrules.fontfaces.unique') {
60 | return `
${Object.entries(value)
61 | .map(([prop, val]) => {
62 | return `${prop}
${val}
`
63 | })
64 | .join('')}
`
65 | }
66 |
67 | return `${value}
`
68 | }
69 |
70 | exports.createCommentMarkdown = ({ changes }) => {
71 | if (changes.length === 0) {
72 | return 'No changes in CSS Analytics detected'
73 | }
74 |
75 | return `
76 | ### CSS Analytics changes
77 |
78 | _Last updated: ${new Date().toISOString()}_
79 |
80 | | metric | current value | value after PR | difference |
81 | |--------|---------------|----------------|------------|
82 | ${changes
83 | .filter(({ key }) => !DEPRECATED_METRICS.includes(key))
84 | .map(({ title, diff, aggregate, key }) => {
85 | if (aggregate === 'list') {
86 | const oldValues = diff.diff
87 | .map((item) => {
88 | if (item.added) return ``
89 | if (item.removed)
90 | return `${formatListItem({
91 | key,
92 | value: item.value,
93 | })}`
94 | return `${formatListItem({ key, value: item.value })}`
95 | })
96 | .join('')
97 | const newValues = diff.diff
98 | .map((item) => {
99 | if (item.removed) return ``
100 | if (item.added)
101 | return `${formatListItem({
102 | key,
103 | value: item.value,
104 | })}`
105 | return `${formatListItem({ key, value: item.value })}`
106 | })
107 | .join('')
108 | return `| ${title} | ${oldValues}
| ${newValues}
| |`
109 | }
110 |
111 | if (key.includes('size')) {
112 | return `| ${title} | ${formatFilesize(
113 | diff.oldValue
114 | )} | ${formatFilesize(diff.newValue)} | ${formatFilesize(
115 | diff.diff.absolute,
116 | true
117 | )} (${formatPercentage(diff.diff.relative)}) |`
118 | }
119 |
120 | return `| ${title} | ${formatNumber(diff.oldValue)} | ${formatNumber(
121 | diff.newValue
122 | )} | ${formatDiff(diff.diff.absolute)} (${formatPercentage(
123 | diff.diff.relative
124 | )}) |`
125 | })
126 | .join('\n')}
127 | `
128 | .split('\n')
129 | .map((line) => line.trim())
130 | .join('\n')
131 | .trim()
132 | }
133 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const got = require('got')
3 | const core = require('@actions/core')
4 | const github = require('@actions/github')
5 | const Sentry = require("@sentry/node")
6 | const { createCommentMarkdown } = require('./create-comment')
7 |
8 | Sentry.init({
9 | dsn: "https://5f217579acd24d33af01995ed625d981@o50610.ingest.sentry.io/4504022978199552",
10 | });
11 |
12 | async function run() {
13 | try {
14 | const cssPath = core.getInput('css-path')
15 | const webhookToken = core.getInput('project-wallace-token')
16 | const githubToken = core.getInput('github-token')
17 | const shouldPostPrComment = core.getInput('post-pr-comment') === 'true'
18 | const { eventName, payload } = github.context
19 |
20 | if (eventName !== 'pull_request') return
21 | if (!shouldPostPrComment) return
22 |
23 | // Read CSS file
24 | const css = fs.readFileSync(cssPath, 'utf8')
25 |
26 | const redactedToken = '*'.padStart(16, '*') + webhookToken.slice(16)
27 |
28 | // POST CSS to projectwallace.com to get the diff
29 | const response = await got(
30 | `https://www.projectwallace.com/api/webhooks/v2/imports/preview?token=${webhookToken}`,
31 | {
32 | method: 'post',
33 | headers: {
34 | 'Content-Type': 'text/css',
35 | Accept: 'application/json',
36 | },
37 | body: css,
38 | }
39 | ).catch((error) => {
40 | Sentry.captureException(error, {
41 | tags: {
42 | step: 'fetching diff',
43 | token: redactedToken,
44 | }
45 | })
46 | core.setFailed(`Could not retrieve diff from projectwallace.com`)
47 | throw error
48 | })
49 |
50 | let diff
51 |
52 | try {
53 | const parsed = JSON.parse(response.body)
54 | diff = parsed.diff
55 | } catch (error) {
56 | Sentry.captureException(error, {
57 | tags: {
58 | step: 'parsing diff',
59 | token: redactedToken,
60 | }
61 | })
62 | console.error('Cannot parse JSON response from projectwallace.com')
63 | core.setFailed(error.message)
64 | }
65 |
66 | // POST the actual PR comment
67 | const formattedBody = createCommentMarkdown({ changes: diff })
68 | const owner = payload.repository.owner.login
69 | const repo = payload.repository.name
70 | const issue_number = payload.number
71 |
72 | const octokit = new github.GitHub(githubToken)
73 | let wallaceComment
74 |
75 | try {
76 | const response = await octokit.issues.listComments({
77 | owner,
78 | repo,
79 | issue_number,
80 | })
81 | const comments = response.data
82 | wallaceComment = comments.find(comment => comment.body.toLowerCase().includes('css analytics changes') || comment.body.includes('No changes in CSS Analytics detected'))
83 | } catch (error) {
84 | Sentry.captureException(error, {
85 | tags: {
86 | step: 'fetching comment',
87 | owner,
88 | repo,
89 | issue_number,
90 | }
91 | })
92 | console.error('error fetching PW comment')
93 | console.error(error)
94 | }
95 |
96 | if (wallaceComment) {
97 | console.log(`Updating comment ID ${wallaceComment.id}`)
98 | await octokit.issues.updateComment({
99 | owner,
100 | repo,
101 | issue_number,
102 | comment_id: wallaceComment.id,
103 | body: formattedBody,
104 | })
105 | .catch((error) => {
106 | Sentry.captureException(error, {
107 | tags: {
108 | step: 'updating comment',
109 | owner,
110 | repo,
111 | issue_number,
112 | }
113 | })
114 | core.warning(`Error ${error}: Failed to update comment to PR`)
115 | throw error
116 | })
117 | } else {
118 | await octokit.issues
119 | .createComment({
120 | owner,
121 | repo,
122 | issue_number,
123 | body: formattedBody,
124 | })
125 | .catch((error) => {
126 | Sentry.captureException(error, {
127 | tags: {
128 | step: 'posting comment',
129 | owner,
130 | repo,
131 | issue_number,
132 | }
133 | })
134 | core.warning(`Error ${error}: Failed to post new comment to PR`)
135 | throw error
136 | })
137 | }
138 | } catch (error) {
139 | Sentry.captureException(error, {
140 | tags: {
141 | step: 'general error'
142 | }
143 | })
144 | core.setFailed(error.message)
145 | }
146 | }
147 |
148 | run()
149 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: 100%;
3 | }
4 |
5 | body {
6 | background-color: aqua;
7 | }
8 |
9 | p {
10 | color: red;
11 | }
12 |
13 | code {
14 | font-family: monospace, monospace;
15 | }
16 |
17 | @font-face {
18 | src: url('somewhere.woff2');
19 | font-family: test;
20 | }
--------------------------------------------------------------------------------
/test/create-comment.test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava')
2 | const { createCommentMarkdown } = require('../src/create-comment')
3 |
4 | const diffFixture = [
5 | {
6 | title: 'Filesize (raw bytes)',
7 | aggregate: 'sum',
8 | key: 'stylesheet.size',
9 | diff: {
10 | oldValue: 9,
11 | newValue: 100,
12 | diff: { absolute: 91, relative: 10.11111111111111 },
13 | },
14 | },
15 | {
16 | title: 'Source Lines of Code',
17 | aggregate: 'sum',
18 | key: 'stylesheet.sourceLinesOfCode',
19 | diff: { oldValue: 1, newValue: 9, diff: { absolute: 8, relative: 8 } },
20 | },
21 | {
22 | title: '@font-faces',
23 | aggregate: 'sum',
24 | key: 'atrules.fontface.total',
25 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
26 | },
27 | {
28 | title: 'Unique @font-faces',
29 | aggregate: 'sum',
30 | key: 'atrules.fontface.totalUnique',
31 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
32 | },
33 | {
34 | title: '@media queries',
35 | aggregate: 'sum',
36 | key: 'atrules.media.total',
37 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
38 | },
39 | {
40 | title: 'Unique @media queries',
41 | aggregate: 'sum',
42 | key: 'atrules.media.totalUnique',
43 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
44 | },
45 | {
46 | title: '@media queries',
47 | aggregate: 'list',
48 | key: 'atrules.media.unique',
49 | diff: {
50 | diff: [
51 | { value: '(min-width: 30em)', added: true, removed: false, changed: true },
52 | { value: 'all', added: false, removed: false, changed: false },
53 | { value: 'only screen', added: false, removed: true, changed: true }
54 | ]
55 | }
56 | },
57 | {
58 | title: 'Rules',
59 | aggregate: 'sum',
60 | key: 'rules.total',
61 | diff: { oldValue: 1, newValue: 3, diff: { absolute: 2, relative: 2 } },
62 | },
63 | {
64 | title: 'Empty rules',
65 | aggregate: 'sum',
66 | key: 'rules.empty.total',
67 | diff: { oldValue: 1, newValue: 0, diff: { absolute: -1, relative: -1 } },
68 | },
69 | {
70 | title: 'Minimum size per rule',
71 | aggregate: 'min',
72 | key: 'rules.sizes.min',
73 | diff: { oldValue: 1, newValue: 2, diff: { absolute: 1, relative: 1 } },
74 | },
75 | {
76 | title: 'Maximum size per rule',
77 | aggregate: 'max',
78 | key: 'rules.sizes.max',
79 | diff: { oldValue: 1, newValue: 2, diff: { absolute: 1, relative: 1 } },
80 | },
81 | {
82 | title: 'Average size per rule',
83 | aggregate: 'average',
84 | key: 'rules.sizes.mean',
85 | diff: { oldValue: 1, newValue: 2, diff: { absolute: 1, relative: 1 } },
86 | },
87 | {
88 | title: 'Most common size per rule',
89 | aggregate: 'average',
90 | key: 'rules.sizes.mode',
91 | diff: { oldValue: 1, newValue: 2, diff: { absolute: 1, relative: 1 } },
92 | },
93 | {
94 | title: 'RuleSet sizes',
95 | aggregate: 'list',
96 | key: 'rules.sizes.unique',
97 | diff: {
98 | diff: [
99 | { value: '0', added: false, removed: true, changed: true },
100 | { value: '2', added: true, removed: false, changed: true },
101 | ],
102 | },
103 | },
104 | {
105 | title: 'Selectors per RuleSet',
106 | aggregate: 'list',
107 | key: 'rules.selectors.unique',
108 | diff: {
109 | diff: [
110 | { value: '0', added: false, removed: true, changed: true },
111 | { value: '1', added: true, removed: false, changed: true },
112 | ],
113 | },
114 | },
115 | {
116 | title: 'Minimum declarations per rule',
117 | aggregate: 'min',
118 | key: 'rules.declarations.min',
119 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
120 | },
121 | {
122 | title: 'Maximum declarations per rule',
123 | aggregate: 'max',
124 | key: 'rules.declarations.max',
125 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
126 | },
127 | {
128 | title: 'Average declarations per rule',
129 | aggregate: 'average',
130 | key: 'rules.declarations.mean',
131 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
132 | },
133 | {
134 | title: 'Most common declarations per rule',
135 | aggregate: 'average',
136 | key: 'rules.declarations.mode',
137 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
138 | },
139 | {
140 | title: 'Declarations per RuleSet',
141 | aggregate: 'list',
142 | key: 'rules.declarations.unique',
143 | diff: {
144 | diff: [
145 | { value: '0', added: false, removed: true, changed: true },
146 | { value: '1', added: true, removed: false, changed: true },
147 | ],
148 | },
149 | },
150 | {
151 | title: 'Selectors',
152 | aggregate: 'sum',
153 | key: 'selectors.total',
154 | diff: { oldValue: 1, newValue: 3, diff: { absolute: 2, relative: 2 } },
155 | },
156 | {
157 | title: 'Unique selectors',
158 | aggregate: 'sum',
159 | key: 'selectors.totalUnique',
160 | diff: { oldValue: 1, newValue: 3, diff: { absolute: 2, relative: 2 } },
161 | },
162 | {
163 | title: 'Total Selector Complexity',
164 | aggregate: 'sum',
165 | key: 'selectors.complexity.sum',
166 | diff: { oldValue: 1, newValue: 3, diff: { absolute: 2, relative: 2 } },
167 | },
168 | {
169 | title: 'Unique Selector Complexities',
170 | aggregate: 'list',
171 | key: 'selectors.complexity.unique',
172 | diff: {
173 | diff: [
174 | { value: '0', added: false, removed: true, changed: true },
175 | { value: '1', added: true, removed: false, changed: true },
176 | ],
177 | },
178 | },
179 | {
180 | title: 'ID selectors',
181 | aggregate: 'sum',
182 | key: 'selectors.id.total',
183 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
184 | },
185 | {
186 | title: 'Unique ID Selectors',
187 | aggregate: 'sum',
188 | key: 'selectors.id.totalUnique',
189 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
190 | },
191 | {
192 | title: 'Declarations',
193 | aggregate: 'sum',
194 | key: 'declarations.total',
195 | diff: { oldValue: 0, newValue: 4, diff: { absolute: 4, relative: 1 } },
196 | },
197 | {
198 | title: 'Unique declarations',
199 | aggregate: 'sum',
200 | key: 'declarations.totalUnique',
201 | diff: { oldValue: 0, newValue: 4, diff: { absolute: 4, relative: 1 } },
202 | },
203 | {
204 | title: 'Properties',
205 | aggregate: 'sum',
206 | key: 'properties.total',
207 | diff: { oldValue: 0, newValue: 4, diff: { absolute: 4, relative: 1 } },
208 | },
209 | {
210 | title: 'Unique properties',
211 | aggregate: 'sum',
212 | key: 'properties.totalUnique',
213 | diff: { oldValue: 0, newValue: 4, diff: { absolute: 4, relative: 1 } },
214 | },
215 | {
216 | title: 'Properties',
217 | aggregate: 'list',
218 | key: 'properties.unique',
219 | diff: {
220 | diff: [
221 | { value: 'color', added: true, removed: false, changed: true },
222 | { value: 'font-family', added: true, removed: false, changed: true },
223 | { value: 'font-size', added: true, removed: false, changed: true },
224 | { value: 'x', added: true, removed: false, changed: true },
225 | ],
226 | },
227 | },
228 | {
229 | title: 'Property complexity',
230 | aggregate: 'sum',
231 | key: 'properties.complexity.sum',
232 | diff: { oldValue: 0, newValue: 4, diff: { absolute: 4, relative: 1 } },
233 | },
234 | {
235 | title: 'Max. property complexity',
236 | aggregate: 'max',
237 | key: 'properties.complexity.max',
238 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
239 | },
240 | {
241 | title: 'Min. property complexity',
242 | aggregate: 'min',
243 | key: 'properties.complexity.min',
244 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
245 | },
246 | {
247 | title: 'Avg. property complexity',
248 | aggregate: 'average',
249 | key: 'properties.complexity.mean',
250 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
251 | },
252 | {
253 | title: 'Most common property complexity',
254 | aggregate: 'average',
255 | key: 'properties.complexity.mode',
256 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
257 | },
258 | {
259 | title: 'Font-sizes',
260 | aggregate: 'sum',
261 | key: 'values.fontSizes.total',
262 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
263 | },
264 | {
265 | title: 'Unique font-sizes',
266 | aggregate: 'sum',
267 | key: 'values.fontSizes.totalUnique',
268 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
269 | },
270 | {
271 | title: 'Font-sizes',
272 | aggregate: 'list',
273 | key: 'values.fontSizes.unique',
274 | diff: {
275 | diff: [{ value: '3em', added: true, removed: false, changed: true }],
276 | },
277 | },
278 | {
279 | title: 'Font-families',
280 | aggregate: 'sum',
281 | key: 'values.fontFamilies.total',
282 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
283 | },
284 | {
285 | title: 'Unique font-families',
286 | aggregate: 'sum',
287 | key: 'values.fontFamilies.totalUnique',
288 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
289 | },
290 | {
291 | title: 'Font-families',
292 | aggregate: 'list',
293 | key: 'values.fontFamilies.unique',
294 | diff: {
295 | diff: [{ value: '"x"', added: true, removed: false, changed: true }],
296 | },
297 | },
298 | {
299 | title: 'Colors',
300 | aggregate: 'sum',
301 | key: 'values.colors.total',
302 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
303 | },
304 | {
305 | title: 'Unique colors',
306 | aggregate: 'sum',
307 | key: 'values.colors.totalUnique',
308 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
309 | },
310 | {
311 | title: 'Colors',
312 | aggregate: 'list',
313 | key: 'values.colors.unique',
314 | diff: {
315 | diff: [{ value: 'red', added: true, removed: false, changed: true }],
316 | },
317 | },
318 | {
319 | title: 'Units',
320 | aggregate: 'sum',
321 | key: 'values.units.total',
322 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
323 | },
324 | {
325 | title: 'Unique units',
326 | aggregate: 'sum',
327 | key: 'values.units.totalUnique',
328 | diff: { oldValue: 0, newValue: 1, diff: { absolute: 1, relative: 1 } },
329 | },
330 | {
331 | title: 'Units',
332 | aggregate: 'list',
333 | key: 'values.units.unique',
334 | diff: {
335 | diff: [{ value: 'em', added: true, removed: false, changed: true }],
336 | },
337 | },
338 | ]
339 |
340 | let actual
341 |
342 | test.beforeEach(() => {
343 | actual = createCommentMarkdown({ changes: diffFixture })
344 | })
345 |
346 | test('it shows a table header', (t) => {
347 | t.true(
348 | actual.includes('| metric | current value | value after PR | difference |')
349 | )
350 | })
351 |
352 | test('it shows filesize diffs correctly', (t) => {
353 | const expected = '| Filesize (raw bytes) | 9 B | 100 B | +91 B (+1,011.11%) |'
354 | const line = actual
355 | .split('\n')
356 | .find((line) => line.startsWith('| Filesize (raw bytes)'))
357 |
358 | t.is(line, expected)
359 | })
360 |
361 | test('it shows total diffs correctly', (t) => {
362 | const lines = actual.split('\n')
363 | const rules = lines.find((line) => line.startsWith('| Rules |'))
364 | t.is(rules, '| Rules | 1 | 3 | +2 (+200%) |')
365 |
366 | const selectorComplexity = lines.find((line) =>
367 | line.startsWith('| Total Selector Complexity |')
368 | )
369 | t.is(selectorComplexity, '| Total Selector Complexity | 1 | 3 | +2 (+200%) |')
370 | })
371 |
372 | test('it shows complex array-like diffs correctly', (t) => {
373 | const lines = actual
374 | .split('\n')
375 | .filter((line) => line.startsWith('| @media queries |'))
376 | const media = lines[lines.length - 1]
377 | t.is(
378 | media,
379 | '| @media queries | all
only screen
| (min-width: 30em)
all
| |'
380 | )
381 | })
382 |
--------------------------------------------------------------------------------