diff --git a/package-lock.json b/package-lock.json index 2466411a..18d3dd56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,17 @@ "pngjs": "^7" }, "devDependencies": { + "@rollup/plugin-node-resolve": "^15", + "@web/dev-server": "^0.2", + "@web/rollup-plugin-html": "^2", "chai": "^4", + "deepmerge": "^4", "eslint": "^8", "eslint-config-brightspace": "^0.23", "lit": "^2", "mocha": "^10", + "page": "^1", + "rollup": "^3", "sinon": "^15" } }, @@ -576,7 +582,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -599,17 +604,25 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true, - "peer": true, "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", @@ -1369,6 +1382,41 @@ "node": ">=16.0.0" } }, + "node_modules/@web/rollup-plugin-html": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@web/rollup-plugin-html/-/rollup-plugin-html-2.0.0.tgz", + "integrity": "sha512-XaCaEufPQ+byMYNjRtJ+YYKqSWGo2ct9VVa6Bquxfd6VjKziYorNJtsDbVMkjWzIlK9PY7Y5yIsAAxYjTChOBA==", + "dev": true, + "dependencies": { + "@web/parse5-utils": "^2.0.0", + "glob": "^7.1.6", + "html-minifier-terser": "^7.1.0", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@web/rollup-plugin-html/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@web/test-runner": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/@web/test-runner/-/test-runner-0.16.1.tgz", @@ -1906,6 +1954,12 @@ "node": "*" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -1958,6 +2012,16 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -2186,6 +2250,27 @@ "devtools-protocol": "*" } }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -2363,6 +2448,15 @@ "node": ">=12.17" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2640,6 +2734,16 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2683,7 +2787,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "peer": true, "engines": { "node": ">=0.12" }, @@ -3853,6 +3956,27 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -4912,6 +5036,15 @@ "get-func-name": "^2.0.0" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5272,6 +5405,25 @@ "type-detect": "4.0.8" } }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5481,6 +5633,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/page": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/page/-/page-1.11.6.tgz", + "integrity": "sha512-P6e2JfzkBrPeFCIPplLP7vDDiU84RUUZMrWdsH4ZBGJ8OosnwFkcUkBHp1DTIjuipLliw9yQn/ZJsXZvarsO+g==", + "dev": true, + "dependencies": { + "path-to-regexp": "~1.2.1" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5516,6 +5687,16 @@ "node": ">= 0.8" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5570,9 +5751,9 @@ } }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.2.1.tgz", + "integrity": "sha512-DBw9IhWfevR2zCVwEZURTuQNseCvu/Q9f5ZgqMCK0Rh61bDa4uyjPAOy9b55yKiPT59zZn+7uYKxmWwsguInwg==", "dev": true, "dependencies": { "isarray": "0.0.1" @@ -5912,6 +6093,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6324,6 +6514,25 @@ "node": ">= 8" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -6585,6 +6794,30 @@ "node": ">=6" } }, + "node_modules/terser": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz", + "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6662,6 +6895,12 @@ "json5": "lib/cli.js" } }, + "node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "dev": true + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", diff --git a/package.json b/package.json index a5e14eec..f6131e70 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "repository": "https://github.com/BrightspaceUI/testing.git", "scripts": { "lint": "eslint . --ext .js", + "start": "web-dev-server --root-dir ./.vdiff --open ./report/", "test": "npm run lint && npm run test:server && npm run test:browser", "test:browser": "web-test-runner --files \"./test/browser/**/*.test.js\" --node-resolve --playwright", "test:server": "mocha ./test/server/**/*.test.js", @@ -15,11 +16,17 @@ "author": "D2L Corporation", "license": "Apache-2.0", "devDependencies": { + "@rollup/plugin-node-resolve": "^15", + "@web/dev-server": "^0.2", + "@web/rollup-plugin-html": "^2", "chai": "^4", + "deepmerge": "^4", "eslint": "^8", "eslint-config-brightspace": "^0.23", "lit": "^2", "mocha": "^10", + "page": "^1", + "rollup": "^3", "sinon": "^15" }, "exports": { diff --git a/src/server/report/app.js b/src/server/report/app.js new file mode 100644 index 00000000..0d5c0ec9 --- /dev/null +++ b/src/server/report/app.js @@ -0,0 +1,133 @@ +import './test-result.js'; +import { css, html, LitElement } from 'lit'; +import data from './data.js'; +import page from 'page'; + +class App extends LitElement { + static properties = { + _filterFile: { state: true }, + _filterTest: { state: true }, + _mode: { state: true }, + _overlay: { state: true } + }; + static styles = [css` + table { + border-collapse: collapse; + } + td, th { + border: 1px solid #cdd5dc; + padding: 10px; + } + thead th { + background-color: #f9fbff; + } + tbody th { + font-weight: normal; + text-align: left; + } + `]; + constructor() { + super(); + this._mode = 'sideBySide'; + this._overlay = true; + } + connectedCallback() { + super.connectedCallback(); + this._root = new URL(window.location.href).pathname; + page(this._root, (ctx) => { + const searchParams = new URLSearchParams(ctx.querystring); + if (searchParams.has('file')) { + this._filterFile = searchParams.get('file'); + if (searchParams.has('test')) { + this._filterTest = searchParams.get('test'); + } else { + this._filterTest = undefined; + } + } else { + this._filterFile = undefined; + this._filterTest = undefined; + } + }); + page(); + } + render() { + let view; + if (this._filterFile !== undefined && this._filterTest !== undefined) { + const fileData = data.files.find(f => f.name === this._filterFile); + if (!fileData) { + view = html`

File not found: ${this._filterFile}.

`; + } else { + const testData = fileData.tests.find(t => t.name === this._filterTest); + if (!testData) { + view = html`

Test not found: ${this._filterTest}.

`; + } else { + view = this._renderTest(fileData, testData); + } + } + } else { + view = data.files.map(f => this._renderFile(f)); + } + return html` +
+

Visual-diff Results

+
+
${view}
+ `; + } + _goHome() { + page(this._root); + } + _handleModeChange(e) { + this._mode = e.target.options[e.target.selectedIndex].value; + } + _handleOverlayChange(e) { + this._overlay = e.target.checked; + } + _renderFile(file) { + return html` +

${file.name}

+ + + + + ${data.browsers.map(b => html``)} + + + + ${file.tests.map(t => this._renderTestResultRow(file, t))} + +
Test${b.name}
+ `; + } + _renderTest(file, test) { + return html` +

${test.name} (${(test.results.length - test.numFailed)}/${test.results.length} passed)

+
+ + +
+ ${test.results.map(r => html``)} +
+ +
+ `; + } + _renderTestResultRow(file, test) { + const results = test.results.map(r => { + return html`${r.passed.toString()}`; + }); + return html` + + ${test.name} + ${results} + + `; + } +} +customElements.define('d2l-vdiff-report-app', App); diff --git a/src/server/report/index.html b/src/server/report/index.html new file mode 100644 index 00000000..c4c3f733 --- /dev/null +++ b/src/server/report/index.html @@ -0,0 +1,21 @@ + + + + + + Visual Diff Report + + + + + + + + + \ No newline at end of file diff --git a/src/server/report/test-result.js b/src/server/report/test-result.js new file mode 100644 index 00000000..d2277f70 --- /dev/null +++ b/src/server/report/test-result.js @@ -0,0 +1,110 @@ +import { css, html, LitElement, nothing } from 'lit'; +import data from './data.js'; + +class TestResult extends LitElement { + static properties = { + browser: { type: String }, + mode: { type: String }, + file: { type: String }, + showOverlay: { attribute: 'show-overlay', type: Boolean }, + test: { type: String }, + }; + static styles = [css` + .side-by-side { + flex-direction: row; + flex-wrap: nowrap; + display: flex; + gap: 10px; + } + .side-by-side > div { + flex: 0 1 50%; + } + img { + max-width: 100%; + } + .diff-container { + background: repeating-conic-gradient(#cccccc 0% 25%, #ffffff 0% 50%) 50% / 20px 20px; + background-position: 0 0; + border: 1px solid #cccccc; + display: inline-block; + line-height: 0; + } + .overlay-container { + position: relative; + } + .overlay { + background: hsla(0,0%,100%,.8); + position: absolute; + top: 0; + left: 0; + } + .no-changes { + border: 1px solid #cccccc; + padding: 20px; + } + `]; + render() { + + const { browserData, fileData, resultData, testData } = this._fetchData(); + if (!browserData || !fileData || !resultData || !testData) return nothing; + + return html` +

${resultData.name} v${browserData.version} (${resultData.duration}ms)

+ ${this._renderBody(resultData)} + `; + + } + _fetchData() { + + const fileData = data.files.find(f => f.name === this.file); + if (!fileData) return {}; + + const testData = fileData.tests.find(t => t.name === this.test); + if (!testData) return {}; + + const resultData = testData.results.find(r => r.name === this.browser); + if (!resultData) return {}; + + const browserData = data.browsers.find(b => b.name === this.browser); + if (!browserData) return {}; + + return { browserData, fileData, resultData, testData }; + + } + _renderBody(resultData) { + + if (!resultData.passed && resultData.info === undefined) { + return html` +

An error occurred that prevented a visual-diff snapshot from being taken:

+
${resultData.error}
+ `; + } + + const noChanges = html`
No changes
`; + const goldenImage = html``; + const newImage = html``; + + const overlay = (this.showOverlay && !resultData.passed) ? + html`
` : nothing; + + if (this.mode === 'sideBySide') { + const g = html`
${goldenImage}
`; + return html` +
+ ${ resultData.passed ? noChanges : g } +
${newImage}${overlay}
+
`; + } else { + return html` +
+
+ ${(this.mode === 'oneUpOriginal') ? goldenImage : newImage} + ${overlay} +
+
`; + } + + } +} + +customElements.define('d2l-vdiff-report-test-result', TestResult); diff --git a/src/server/rollup.config.js b/src/server/rollup.config.js new file mode 100644 index 00000000..4797d636 --- /dev/null +++ b/src/server/rollup.config.js @@ -0,0 +1,15 @@ +import { rollupPluginHTML as html } from '@web/rollup-plugin-html'; +import { join } from 'path'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import { PATHS } from './visual-diff-plugin.js'; + +export default { + input: join(PATHS.VDIFF_ROOT, './report/temp/index.html'), + output: { + dir: join(PATHS.VDIFF_ROOT, 'report') + }, + plugins: [ + html(), + nodeResolve() + ], +}; diff --git a/src/server/visual-diff-plugin.js b/src/server/visual-diff-plugin.js index 639ed288..07241ca3 100644 --- a/src/server/visual-diff-plugin.js +++ b/src/server/visual-diff-plugin.js @@ -1,13 +1,14 @@ import { access, constants, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'; import { basename, dirname, join } from 'node:path'; import { env } from 'node:process'; +import merge from 'deepmerge'; import pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; const isCI = !!env['CI']; const DEFAULT_MARGIN = 10; const DEFAULT_TOLERANCE = 0; // TODO: Support tolerance override? -const PATHS = { +export const PATHS = { FAIL: 'fail', GOLDEN: 'golden', PASS: 'pass', @@ -29,8 +30,7 @@ async function clearDir(updateGoldens, path) { } else { await Promise.all([ rm(join(path, PATHS.FAIL), { force: true, recursive: true }), - rm(join(path, PATHS.PASS), { force: true, recursive: true }), - rm(join(path, 'report.html'), { force: true }) + rm(join(path, PATHS.PASS), { force: true, recursive: true }) ]); } } @@ -53,8 +53,6 @@ async function clearDiffPaths(dir) { if (path.isDirectory()) { if (base === PATHS.PASS || base === PATHS.FAIL) await rm(full, { force: true, recursive: true }); else await clearDiffPaths(full); - } else { - if (base === 'report.html') await rm(full, { force: true }); } } } @@ -114,6 +112,21 @@ function extractTestPartsFromName(name) { }; } +const testInfoMap = new Map(); +export function getTestInfo(session, fullTitle) { + return testInfoMap.get(getTestInfoKey(session, fullTitle)); +} +function getTestInfoKey(session, fullTitle) { + return `${session.browser.name.toLowerCase()}|${session.testFile}|${fullTitle}`; +} +function setTestInfo(session, fullTitle, testInfo) { + const key = getTestInfoKey(session, fullTitle); + if (testInfoMap.has(key)) { + testInfo = merge(testInfoMap.get(key), testInfo); + } + testInfoMap.set(key, testInfo); +} + export function visualDiff({ updateGoldens = false, runSubset = false } = {}) { let currentRun = 0, rootDir; @@ -171,6 +184,16 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) { path: updateGoldens ? goldenFileName : screenshotFileName }); + const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1; + setTestInfo(session, payload.name, { + golden: { + path: goldenFileName.substring(rootLength) + }, + new: { + path: passFileName.substring(rootLength) + } + }); + if (updateGoldens) { return { pass: true }; } @@ -183,6 +206,17 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) { const screenshotImage = PNG.sync.read(await readFile(screenshotFileName)); const goldenImage = PNG.sync.read(await readFile(goldenFileName)); + setTestInfo(session, payload.name, { + golden: { + height: goldenImage.height, + width: goldenImage.width + }, + new: { + height: screenshotImage.height, + width: screenshotImage.width + } + }); + if (screenshotImage.width === goldenImage.width && screenshotImage.height === goldenImage.height) { const diff = new PNG({ width: screenshotImage.width, height: screenshotImage.height }); const pixelsDiff = pixelmatch( @@ -190,6 +224,12 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) { ); if (pixelsDiff !== 0) { + setTestInfo(session, payload.name, { + diff: `${screenshotFile.substring(rootLength)}-diff.png`, + new: { + path: `${screenshotFile.substring(rootLength)}.png` + } + }); await writeFile(`${screenshotFile}-diff.png`, PNG.sync.write(diff)); return { pass: false, message: `Image does not match golden. ${pixelsDiff} pixels are different.` }; } else { @@ -231,6 +271,17 @@ export function visualDiff({ updateGoldens = false, runSubset = false } = {}) { await writeFile(`${screenshotFile}-resized-golden.png`, PNG.sync.write(newGoldens[bestIndex].png)); await writeFile(`${screenshotFile}-diff.png`, PNG.sync.write(bestDiffImage)); + const rootLength = join(rootDir, PATHS.VDIFF_ROOT).length + 1; + setTestInfo(session, payload.name, { + diff: `${screenshotFile.substring(rootLength)}-diff.png`, + golden: { + path: `${screenshotFile.substring(rootLength)}-resized-golden.png` + }, + new: { + path: `${screenshotFile.substring(rootLength)}-resized-screenshot.png` + } + }); + return { pass: false, message: `Images are not the same size. When resized, ${pixelsDiff} pixels are different.` }; } diff --git a/src/server/visual-diff-reporter.js b/src/server/visual-diff-reporter.js new file mode 100644 index 00000000..dbf44bd3 --- /dev/null +++ b/src/server/visual-diff-reporter.js @@ -0,0 +1,111 @@ +import { cpSync, mkdirSync, rmSync, writeFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { getTestInfo, PATHS } from './visual-diff-plugin.js'; +import { execSync } from 'child_process'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function createData(rootDir, sessions) { + + const files = new Map(); + const browsers = new Map(); + + sessions.forEach(s => { + + const browserName = s.browser.name; + if (!browsers.has(browserName)) { + browsers.set(browserName, { + name: browserName, + numFailed: 0, + version: s.browser.browser.version().substring(0, s.browser.browser.version().indexOf('.')) + }); + } + const browserData = browsers.get(browserName); + + const fileName = s.testFile.substring(rootDir.length + 1); + if (!files.has(fileName)) { + files.set(fileName, { name: fileName, numFailed: 0, tests: new Map() }); + } + const fileData = files.get(fileName); + flattenResults(s, browserData, fileData); + + }); + + return { files, browsers }; + +} + +function flattenResults(session, browserData, fileData) { + + function collectTests(prefix, tests) { + tests.forEach(t => { + const testName = `${prefix}${t.name}`; + const testKey = testName.replaceAll(' > ', ' '); + if (!fileData.tests.has(testName)) { + fileData.tests.set(testName, { + name: testName, + numFailed: 0, + results: [] + }); + } + const testData = fileData.tests.get(testName); + if (!t.passed) { + browserData.numFailed++; + fileData.numFailed++; + testData.numFailed++; + } + testData.results.push({ + name: browserData.name, + duration: t.duration, + error: t.error?.message, + passed: t.passed, + info: getTestInfo(session, testKey) + }); + }); + } + + function collectSuite(prefix, suite) { + collectTests(prefix, suite.tests); + suite.suites.forEach(s => collectSuite(`${prefix}${s.name} > `, s)); + } + + if (session.testResults) { + collectSuite('', session.testResults); + } + +} + +export function visualDiffReporter({ reportResults = true } = {}) { + let rootDir; + return { + start({ config }) { + rootDir = config.rootDir; + rmSync(join(rootDir, PATHS.VDIFF_ROOT, 'report'), { force: true, recursive: true }); + }, + stop({ sessions }) { + + if (!reportResults) return; + + const data = createData(rootDir, sessions); + const json = JSON.stringify(data, (_key, val) => { + if (val instanceof Map) return [...val.values()].sort((a, b) => a.name.localeCompare(b.name)); + return val; + }, '\t'); + + const inputDir = join(__dirname, 'report'); + const reportDir = join(rootDir, PATHS.VDIFF_ROOT, 'report'); + const tempDir = join(reportDir, 'temp'); + + mkdirSync(reportDir); + + cpSync(inputDir, tempDir, { force: true, recursive: true }); + writeFileSync(join(tempDir, 'data.js'), `export default ${json};`); + + execSync(`rollup -c ${join(__dirname, './rollup.config.js')}`); + + rmSync(tempDir, { recursive: true }); + + } + }; +} diff --git a/src/server/wtr-config.js b/src/server/wtr-config.js index 24c70fce..83b85689 100644 --- a/src/server/wtr-config.js +++ b/src/server/wtr-config.js @@ -3,6 +3,7 @@ import { defaultReporter } from '@web/test-runner'; import { headedMode } from './headed-mode-plugin.js'; import { playwrightLauncher } from '@web/test-runner-playwright'; import { visualDiff } from './visual-diff-plugin.js'; +import { visualDiffReporter } from './visual-diff-reporter.js'; const optionDefinitions = [ // @web/test-runner options @@ -89,7 +90,7 @@ export class WTRConfig { body { background-color: #ffffff; color: var(--d2l-color-ferrite, #202122); - font-family: 'Lato'; + font-family: 'Lato', sans-serif; letter-spacing: 0.01rem; font-size: 0.95rem; font-weight: 400; @@ -184,7 +185,7 @@ export class WTRConfig { if (vdiff) { config.reporters ??= [ defaultReporter() ]; - //config.reporters.push(visualDiffReporter()); + config.reporters.push(visualDiffReporter()); config.plugins ??= []; config.plugins.push(visualDiff({ updateGoldens: golden, runSubset: !!(filter || grep) }));