Merge "Screenshot test support"
diff --git a/.gitignore b/.gitignore
index 623369b..53bc9f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,7 @@
 /plugins/*
 /polygerrit-ui/coverage/
 /polygerrit-ui/app/plugins/*
+/polygerrit-ui/screenshots/Chrome/failed/
 !/plugins/.eslintignore
 !/plugins/.eslintrc.js
 !/plugins/.prettierrc.js
diff --git a/package.json b/package.json
index 1dfee7c..cdaf400 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,8 @@
     "start": "run-p -rl compile:watch start:server",
     "start:server": "web-dev-server",
     "test": "yarn --cwd=polygerrit-ui test",
+    "test:screenshot": "yarn --cwd=polygerrit-ui test:screenshot",
+    "test:screenshot-update": "yarn --cwd=polygerrit-ui test:screenshot-update",
     "test:coverage": "yarn --cwd=polygerrit-ui test:coverage",
     "test:watch": "yarn --cwd=polygerrit-ui test:watch",
     "test:single": "yarn --cwd=polygerrit-ui test:single",
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts
new file mode 100644
index 0000000..f80f48b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import '../../shared/gr-date-formatter/gr-date-formatter';
+import {fixture, html} from '@open-wc/testing';
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {FileInfo, PARENT, RevisionPatchSetNum} from '../../../api/rest-api';
+import {normalize} from '../../../models/change/files-model';
+import {PatchRange} from '../../../types/common';
+import {DiffPreferencesInfo} from '../../../api/diff';
+import {NormalizedFileInfo, GrFileList} from './gr-file-list';
+import './gr-file-list';
+
+suite('gr-file-list screenshot tests', () => {
+  let element: GrFileList;
+
+  function createFiles(
+    count: number,
+    fileInfo: FileInfo
+  ): NormalizedFileInfo[] {
+    return Array.from(Array(count).keys()).map(index =>
+      normalize(fileInfo, `/file${index}`)
+    );
+  }
+
+  setup(async () => {
+    const patchRange: PatchRange = {
+      basePatchNum: PARENT,
+      patchNum: 2 as RevisionPatchSetNum,
+    };
+    const diffPrefs: DiffPreferencesInfo = {
+      context: 10,
+      tab_size: 8,
+      font_size: 12,
+      line_length: 100,
+      ignore_whitespace: 'IGNORE_NONE',
+    };
+    element = await fixture(
+      html`<gr-file-list
+        .patchRange=${patchRange}
+        .diffPrefs=${diffPrefs}
+      ></gr-file-list>`
+    );
+  });
+
+  test('screenshot', async () => {
+    element.files = [
+      ...createFiles(3, {lines_inserted: 9}),
+      ...createFiles(2, {lines_deleted: 14}),
+    ];
+    await element.updateComplete;
+
+    await visualDiff(element, 'gr-file-list');
+  });
+});
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 6f6fe73..306747b 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -38,6 +38,7 @@
   Provider,
 } from '../models/dependency';
 import * as sinon from 'sinon';
+import '../styles/themes/app-theme.ts';
 
 declare global {
   interface Window {
@@ -52,8 +53,7 @@
   const log = _testOnly_defaultResinReportHandler;
   log(isViolation, fmt, ...args);
   if (isViolation) {
-    // This will cause the test to fail if there is a data binding
-    // violation.
+    // This will cause the test to fail if there is a data binding violation.
     throw new Error('polymer-resin violation: ' + fmt + JSON.stringify(args));
   }
 });
@@ -132,7 +132,7 @@
 // Very simple function to catch unexpected elements in documents body.
 // It can't catch everything, but in most cases it is enough.
 function checkChildAllowed(element: Element) {
-  const allowedTags = ['SCRIPT', 'IRON-A11Y-ANNOUNCER'];
+  const allowedTags = ['SCRIPT', 'IRON-A11Y-ANNOUNCER', 'LINK'];
   if (allowedTags.includes(element.tagName)) {
     return;
   }
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index b8fef7d..1287d0c 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -11,6 +11,7 @@
     "@open-wc/testing": "^3.1.6",
     "@web/dev-server-esbuild": "^0.3.2",
     "@web/test-runner": "^0.14.0",
+    "@web/test-runner-visual-regression": "^0.6.6",
     "accessibility-developer-tools": "^2.12.0",
     "karma": "^6.3.20",
     "karma-chrome-launcher": "^3.1.1",
@@ -22,6 +23,8 @@
   },
   "scripts": {
     "test": "web-test-runner",
+    "test:screenshot": "web-test-runner --run-screenshots",
+    "test:screenshot-update": "web-test-runner --update-screenshots --files",
     "test:coverage": "web-test-runner --coverage",
     "test:watch": "web-test-runner --watch",
     "test:single": "web-test-runner --watch --files",
diff --git a/polygerrit-ui/screenshots/Chrome/baseline/gr-file-list.png b/polygerrit-ui/screenshots/Chrome/baseline/gr-file-list.png
new file mode 100644
index 0000000..4d0cbed
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chrome/baseline/gr-file-list.png
Binary files differ
diff --git a/polygerrit-ui/web-test-runner.config.mjs b/polygerrit-ui/web-test-runner.config.mjs
index 44b5969..552e609 100644
--- a/polygerrit-ui/web-test-runner.config.mjs
+++ b/polygerrit-ui/web-test-runner.config.mjs
@@ -1,24 +1,61 @@
 import { esbuildPlugin } from "@web/dev-server-esbuild";
 import { defaultReporter, summaryReporter } from "@web/test-runner";
+import { visualRegressionPlugin } from "@web/test-runner-visual-regression/plugin";
 
 /** @type {import('@web/test-runner').TestRunnerConfig} */
 const config = {
-  files: ["app/**/*_test.{ts,js}", "!**/node_modules/**/*"],
+  files: [
+    "app/**/*_test.{ts,js}",
+    "!**/node_modules/**/*",
+    ...(process.argv.includes("--run-screenshots")
+      ? []
+      : ["!app/**/*_screenshot_test.{ts,js}"]),
+  ],
   port: 9876,
   nodeResolve: true,
-  testFramework: {
-    config: {
-      ui: "tdd",
-      timeout: 5000,
-    },
-  },
+  testFramework: { config: { ui: "tdd", timeout: 5000 } },
   plugins: [
     esbuildPlugin({
       ts: true,
       target: "es2020",
       tsconfig: "app/tsconfig.json",
     }),
+    visualRegressionPlugin({
+      diffOptions: {
+        threshold: 0.8,
+      },
+      update: process.argv.includes("--update-screenshots"),
+    }),
   ],
+  // serve from gerrit root directory so that we can serve fonts from
+  // /lib/fonts/, see middleware.
+  rootDir: "..",
   reporters: [defaultReporter(), summaryReporter()],
+  middleware: [
+    // Fonts are in /lib/fonts/, but css tries to load from
+    // /polygerrit-ui/app/fonts/. In production this works because our build
+    // copies them over, see /polygerrit-ui/BUILD
+    async (context, next) => {
+      if (context.url.startsWith("/polygerrit-ui/app/fonts/")) {
+        context.url = context.url.replace("/polygerrit-ui/app/", "/lib/");
+      }
+      await next();
+    },
+  ],
+  testRunnerHtml: (testFramework) => `
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <link rel="stylesheet" href="polygerrit-ui/app/styles/main.css">
+        <link rel="stylesheet" href="polygerrit-ui/app/styles/fonts.css">
+        <link
+          rel="stylesheet"
+          href="polygerrit-ui/app/styles/material-icons.css">
+      </head>
+      <body>
+        <script type="module" src="${testFramework}"></script>
+      </body>
+    </html>
+  `,
 };
 export default config;
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 68b6fff..ca6943d 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -1526,6 +1526,13 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
   integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
 
+"@types/mkdirp@^1.0.1":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.2.tgz#8d0bad7aa793abe551860be1f7ae7f3198c16666"
+  integrity sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/mocha@^8.2.0":
   version "8.2.3"
   resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
@@ -1551,6 +1558,20 @@
   resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.0.tgz#02d6ff38975d684bdec96204494baf9f29f0e17f"
   integrity sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==
 
+"@types/pixelmatch@^5.2.2":
+  version "5.2.4"
+  resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.4.tgz#ca145cc5ede1388c71c68edf2d1f5190e5ddd0f6"
+  integrity sha512-HDaSHIAv9kwpMN7zlmwfTv6gax0PiporJOipcrGsVNF3Ba+kryOZc0Pio5pn6NhisgWr7TaajlPEKTbTAypIBQ==
+  dependencies:
+    "@types/node" "*"
+
+"@types/pngjs@^6.0.0":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072"
+  integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/qs@*":
   version "6.9.7"
   resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@@ -1780,6 +1801,14 @@
     "@web/test-runner-core" "^0.10.27"
     mkdirp "^1.0.4"
 
+"@web/test-runner-commands@^0.6.4":
+  version "0.6.5"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.6.5.tgz#69a2a06b52fd9d329f9cf1e172cd8fb1d5ffc521"
+  integrity sha512-W+wLg10jEAJY9N6tNWqG1daKmAzxGmTbO/H9fFfcgOgdxdn+hHiR4r2/x1iylKbFLujHUQlnjNQeu2d6eDPFqg==
+  dependencies:
+    "@web/test-runner-core" "^0.10.27"
+    mkdirp "^1.0.4"
+
 "@web/test-runner-core@^0.10.20":
   version "0.10.22"
   resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.10.22.tgz#34bb67d12a79b01dc79c816f3d76f3419ef50eaf"
@@ -1862,6 +1891,20 @@
     "@types/mocha" "^8.2.0"
     "@web/test-runner-core" "^0.10.20"
 
+"@web/test-runner-visual-regression@^0.6.6":
+  version "0.6.6"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-visual-regression/-/test-runner-visual-regression-0.6.6.tgz#4a4dc734f360cba66a005e07b4a1c0a9ef956444"
+  integrity sha512-010J3zE6z2v7eLLey/l5cYa9/WhPsgzZb3Z6K5nn4Mn5W5LGPs/f+XG3N6+Tx8Q1/RvDqLdFvRs/T6c4ul4dgQ==
+  dependencies:
+    "@types/mkdirp" "^1.0.1"
+    "@types/pixelmatch" "^5.2.2"
+    "@types/pngjs" "^6.0.0"
+    "@web/test-runner-commands" "^0.6.4"
+    "@web/test-runner-core" "^0.10.20"
+    mkdirp "^1.0.4"
+    pixelmatch "^5.2.1"
+    pngjs "^6.0.0"
+
 "@web/test-runner@^0.14.0":
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.14.0.tgz#fc7b206f3fdc5a1d774cfc8f60159a574d30b185"
@@ -4569,6 +4612,13 @@
   resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
   integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
 
+pixelmatch@^5.2.1:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a"
+  integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==
+  dependencies:
+    pngjs "^6.0.0"
+
 pkg-dir@4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -4576,6 +4626,11 @@
   dependencies:
     find-up "^4.0.0"
 
+pngjs@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
+  integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
+
 polyfills-loader@^1.7.4:
   version "1.7.6"
   resolved "https://registry.yarnpkg.com/polyfills-loader/-/polyfills-loader-1.7.6.tgz#5cff98bfc9689cf10e44bdd32f498cfeb4374c51"