From c21310886a5cd6db3f6ddd2997cd3cd005981fac Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 12:06:59 -0700 Subject: [PATCH 01/53] docs(infrastructure): Add "revert offline screenshots" to `README.md` (#3087) --- test/screenshot/README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/screenshot/README.md b/test/screenshot/README.md index 7920683a909..b3b39c16764 100644 --- a/test/screenshot/README.md +++ b/test/screenshot/README.md @@ -223,20 +223,32 @@ For example: $ echo '.mdc-button:not(:disabled){color:red}' >> packages/mdc-button/mdc-button.scss ``` -6. Rerun the tests locally: +6. Rerun the tests locally until you're satisfied with how they look: ```bash $ npm run screenshot:test -- --url=mdc-button --retries=0 --offline 30 screenshots changed! + Diff report: http://localhost:9000/advorak/2018/07/15/04_11_46_560/report/report.html + $ git add test/screenshot/golden.json + $ git commit -m 'Update golden.json with offline screenshots' + ``` + +7. Once you're happy with your changes, revert the offline-generated golden images (because they won't match CBT): + + ```bash + $ git fetch + $ git checkout origin/master -- test/screenshot/golden.json + $ git add test/screenshot/golden.json + $ git commit -m 'Revert offline changes to golden.json' ``` -7. Run the tests remotely and create a PR: +8. Run the tests remotely on CBT and create a PR: ```bash - $ npm run screenshot:test -- --url=mdc-button --retries=0 + $ npm run screenshot:test -- --url=mdc-button $ npm run screenshot:approve -- --all --report=https://.../report.json $ git add test/screenshot/golden.json - $ git commit -m 'feat(button): Fancy' + $ git commit -m 'feat(button): Fancy variant' $ git push -u origin ``` From a2b2782d5620e7be9782243eb6d82b19dd351045 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 12:25:26 -0700 Subject: [PATCH 02/53] chore(infrastructure): Fix local dev server (`npm start` command) (#3089) The `npm run screenshot:serve` command was recently broken by a refactoring. It would immediately exit instead of waiting to be killed by the user. That is now fixed. --- test/screenshot/lib/controller.js | 20 +++++--------------- test/screenshot/run.js | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/test/screenshot/lib/controller.js b/test/screenshot/lib/controller.js index 0d3c226d4c3..5dc20631360 100644 --- a/test/screenshot/lib/controller.js +++ b/test/screenshot/lib/controller.js @@ -208,7 +208,10 @@ class Controller { * @return {!Promise} */ async getTestExitCode(reportData) { - const isOnline = await this.cli_.isOnline(); + // Don't fail Travis builds when screenshots change. The diffs are reported in GitHub instead. + if (process.env.TRAVIS === 'true') { + return ExitCode.OK; + } // TODO(acdvorak): Store this directly in the proto so we don't have to recalculate it all over the place const numChanges = @@ -216,20 +219,7 @@ class Controller { reportData.screenshots.added_screenshot_list.length + reportData.screenshots.removed_screenshot_list.length; - if (numChanges === 0) { - return ExitCode.OK; - } - - if (isOnline) { - if (process.env.TRAVIS === 'true') { - return ExitCode.OK; - } - return ExitCode.CHANGES_FOUND; - } - - // Allow the report HTTP server to keep running by waiting for a promise that never resolves. - console.log('\nPress Ctrl-C to kill the report server'); - await new Promise(() => {}); + return numChanges > 0 ? ExitCode.CHANGES_FOUND : ExitCode.OK; } /** diff --git a/test/screenshot/run.js b/test/screenshot/run.js index d161cdd4ead..b82cfbab3e5 100644 --- a/test/screenshot/run.js +++ b/test/screenshot/run.js @@ -60,15 +60,17 @@ async function run() { console.log('Offline mode!'); } - cmd() - .then( - (exitCode = 0) => { + cmd().then( + (exitCode = 0) => { + if (exitCode !== 0) { process.exit(exitCode); - }, - (err) => { - console.error(err); - process.exit(ExitCode.UNKNOWN_ERROR); - }); + } + }, + (err) => { + console.error(err); + process.exit(ExitCode.UNKNOWN_ERROR); + } + ); } else { console.error(`Error: Unknown command: '${cli.command}'`); process.exit(ExitCode.UNSUPPORTED_CLI_COMMAND); From d9cc9b4054c1a42d7674ec15c178e6854252af2a Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 12:40:28 -0700 Subject: [PATCH 03/53] docs: Update list of officially supported browsers (#3082) - Remove "Opera". We don't test in it, and it uses the same rendering engine as Chrome (Blink) - Reorganize list into a map of browsers to OSes (I find it easier to read) --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eadba73973f..4fab9b966c7 100644 --- a/README.md +++ b/README.md @@ -109,10 +109,8 @@ This will produce a Material Design ripple on the button! We officially support the last two versions of every major browser. Specifically, we test on the following browsers: -- Chrome -- Safari -- Firefox -- IE 11/Edge -- Opera -- Mobile Safari -- Chrome on Android +- **Chrome** on Android, Windows, macOS, and Linux +- **Firefox** on Windows, macOS, and Linux +- **Safari** on iOS and macOS +- **Edge** on Windows +- **IE 11** on Windows From 262fc965bd8133f2d4bdb083a5133dd96d4b03cf Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Mon, 16 Jul 2018 15:56:35 -0400 Subject: [PATCH 04/53] docs(shape): Add explicit mdc-shape import to readme (#3088) Fixes #3081. --- packages/mdc-shape/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mdc-shape/README.md b/packages/mdc-shape/README.md index d596d54a9bb..4f1ac7f5001 100644 --- a/packages/mdc-shape/README.md +++ b/packages/mdc-shape/README.md @@ -61,6 +61,7 @@ unelevated component. ### Styles ```scss +@import "@material/shape/mdc-shape"; // The base shape styles need to be imported once in the page or application @import "@material/shape/mixins"; .my-shape-container { @@ -72,11 +73,10 @@ unelevated component. ### Outlined Angled Corners -Outlined angled corners involve the same markup and styles as above, with the addition of including a mixin for outline: +Outlined angled corners involve the same markup and styles/imports as above, with the addition of including a mixin for +outline: ```scss -@import "@material/shape/mixins"; - .my-shape-container { @include mdc-shape-angled-corner(#fff, 10px); @include mdc-shape-angled-corner-outline(2px, blue); From 971eb299f1a8b1ceae73324cd1a9daae30e69abd Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 13:44:23 -0700 Subject: [PATCH 05/53] chore(infrastructure): Post-merge Travis runs should fail w/ diffs (#3092) PRs have a separate "status check" for screenshot diffs in the GitHub UI, so we don't want to fail the Travis job if a PR has diffs. However, once the PR has been merged into `master`, we absolutely _should_ fail the Travis job if there are any diffs. --- .travis.yml | 4 ++++ test/screenshot/lib/controller.js | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c05c4bda00..1413857ac41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: node_js branches: + # Only run Travis on: + # A) Commits made directly to the `master` branch (i.e., merged PRs); and + # B) PRs that will eventually be merged into `master`. + # This prevents excessive resource usage and CI slowness. only: - master matrix: diff --git a/test/screenshot/lib/controller.js b/test/screenshot/lib/controller.js index 5dc20631360..98b41ee75b6 100644 --- a/test/screenshot/lib/controller.js +++ b/test/screenshot/lib/controller.js @@ -208,8 +208,9 @@ class Controller { * @return {!Promise} */ async getTestExitCode(reportData) { - // Don't fail Travis builds when screenshots change. The diffs are reported in GitHub instead. - if (process.env.TRAVIS === 'true') { + // Pull requests display screenshot diffs as a separate "status check" in the GitHub UI, so we don't want to mark + // the Travis run as "failed". + if (Number(process.env.TRAVIS_PULL_REQUEST)) { return ExitCode.OK; } @@ -247,6 +248,7 @@ class Controller { } /** + * @param {string} title * @param {!Array} screenshots * @private */ From e09d71586aa86def0d76ab7b2d430c7c5e88830c Mon Sep 17 00:00:00 2001 From: Abhinay Omkar Date: Mon, 16 Jul 2018 17:22:05 -0400 Subject: [PATCH 06/53] chore: Publish --- lerna.json | 2 +- packages/material-components-web/package.json | 54 +++++++++---------- packages/mdc-button/package.json | 6 +-- packages/mdc-card/package.json | 4 +- packages/mdc-checkbox/package.json | 6 +-- packages/mdc-chips/package.json | 8 +-- packages/mdc-dialog/package.json | 6 +-- packages/mdc-drawer/package.json | 4 +- packages/mdc-fab/package.json | 6 +-- packages/mdc-floating-label/package.json | 4 +- packages/mdc-form-field/package.json | 6 +-- packages/mdc-grid-list/package.json | 4 +- packages/mdc-icon-button/package.json | 4 +- packages/mdc-icon-toggle/package.json | 4 +- packages/mdc-image-list/package.json | 4 +- packages/mdc-list/package.json | 6 +-- packages/mdc-menu/package.json | 4 +- packages/mdc-notched-outline/package.json | 2 +- packages/mdc-radio/package.json | 6 +-- packages/mdc-ripple/package.json | 2 +- packages/mdc-select/package.json | 10 ++-- packages/mdc-selection-control/package.json | 4 +- packages/mdc-snackbar/package.json | 4 +- packages/mdc-tab/package.json | 6 +-- packages/mdc-tabs/package.json | 6 +-- packages/mdc-textfield/package.json | 10 ++-- packages/mdc-toolbar/package.json | 6 +-- packages/mdc-top-app-bar/package.json | 6 +-- packages/mdc-typography/package.json | 2 +- 29 files changed, 98 insertions(+), 98 deletions(-) diff --git a/lerna.json b/lerna.json index 3ed52edcdf0..61b8d1d90c7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "2.0.0-beta.36", - "version": "0.37.0", + "version": "0.37.1", "commands": { "publish": { "ignore": [ diff --git a/packages/material-components-web/package.json b/packages/material-components-web/package.json index 7d4b79eac54..6834031aa92 100644 --- a/packages/material-components-web/package.json +++ b/packages/material-components-web/package.json @@ -1,7 +1,7 @@ { "name": "material-components-web", "description": "Modular and customizable Material Design UI components for the web", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -16,40 +16,40 @@ "@material/animation": "^0.34.0", "@material/auto-init": "^0.35.0", "@material/base": "^0.35.0", - "@material/button": "^0.37.0", - "@material/card": "^0.37.0", - "@material/checkbox": "^0.37.0", - "@material/chips": "^0.37.0", - "@material/dialog": "^0.37.0", - "@material/drawer": "^0.36.1", + "@material/button": "^0.37.1", + "@material/card": "^0.37.1", + "@material/checkbox": "^0.37.1", + "@material/chips": "^0.37.1", + "@material/dialog": "^0.37.1", + "@material/drawer": "^0.37.1", "@material/elevation": "^0.36.1", - "@material/fab": "^0.37.0", - "@material/floating-label": "^0.36.0", - "@material/form-field": "^0.37.0", - "@material/grid-list": "^0.36.0", - "@material/icon-button": "^0.37.0", - "@material/icon-toggle": "^0.37.0", - "@material/image-list": "^0.35.0", + "@material/fab": "^0.37.1", + "@material/floating-label": "^0.37.1", + "@material/form-field": "^0.37.1", + "@material/grid-list": "^0.37.1", + "@material/icon-button": "^0.37.1", + "@material/icon-toggle": "^0.37.1", + "@material/image-list": "^0.37.1", "@material/layout-grid": "^0.34.0", "@material/line-ripple": "^0.35.0", "@material/linear-progress": "^0.35.0", - "@material/list": "^0.37.0", - "@material/menu": "^0.36.1", - "@material/notched-outline": "^0.35.0", - "@material/radio": "^0.37.0", - "@material/ripple": "^0.37.0", + "@material/list": "^0.37.1", + "@material/menu": "^0.37.1", + "@material/notched-outline": "^0.37.1", + "@material/radio": "^0.37.1", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", - "@material/select": "^0.37.0", - "@material/selection-control": "^0.37.0", + "@material/select": "^0.37.1", + "@material/selection-control": "^0.37.1", "@material/shape": "^0.35.0", "@material/slider": "^0.36.0", - "@material/snackbar": "^0.36.0", + "@material/snackbar": "^0.37.1", "@material/switch": "^0.36.1", - "@material/tabs": "^0.37.0", - "@material/textfield": "^0.37.0", + "@material/tabs": "^0.37.1", + "@material/textfield": "^0.37.1", "@material/theme": "^0.35.0", - "@material/toolbar": "^0.37.0", - "@material/top-app-bar": "^0.37.0", - "@material/typography": "^0.35.0" + "@material/toolbar": "^0.37.1", + "@material/top-app-bar": "^0.37.1", + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-button/package.json b/packages/mdc-button/package.json index b6d28404d3b..580d595a70f 100644 --- a/packages/mdc-button/package.json +++ b/packages/mdc-button/package.json @@ -1,7 +1,7 @@ { "name": "@material/button", "description": "The Material Components for the web button component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache 2.0", "keywords": [ "material components", @@ -14,9 +14,9 @@ }, "dependencies": { "@material/elevation": "^0.36.1", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-card/package.json b/packages/mdc-card/package.json index 36449ef72f2..1385f7a754b 100644 --- a/packages/mdc-card/package.json +++ b/packages/mdc-card/package.json @@ -1,6 +1,6 @@ { "name": "@material/card", - "version": "0.37.0", + "version": "0.37.1", "description": "The Material Components for the web card component", "license": "Apache-2.0", "keywords": [ @@ -14,7 +14,7 @@ }, "dependencies": { "@material/elevation": "^0.36.1", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0" } diff --git a/packages/mdc-checkbox/package.json b/packages/mdc-checkbox/package.json index cb6b77405f7..351a303eb32 100644 --- a/packages/mdc-checkbox/package.json +++ b/packages/mdc-checkbox/package.json @@ -1,7 +1,7 @@ { "name": "@material/checkbox", "description": "The Material Components for the web checkbox component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -16,9 +16,9 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", - "@material/selection-control": "^0.37.0", + "@material/selection-control": "^0.37.1", "@material/theme": "^0.35.0" } } diff --git a/packages/mdc-chips/package.json b/packages/mdc-chips/package.json index 7f82eb76221..56601c28dc4 100644 --- a/packages/mdc-chips/package.json +++ b/packages/mdc-chips/package.json @@ -1,7 +1,7 @@ { "name": "@material/chips", "description": "The Material Components for the Web chips component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache 2.0", "keywords": [ "material components", @@ -18,8 +18,8 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/checkbox": "^0.37.0", - "@material/ripple": "^0.37.0", - "@material/typography": "^0.35.0" + "@material/checkbox": "^0.37.1", + "@material/ripple": "^0.37.1", + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-dialog/package.json b/packages/mdc-dialog/package.json index 6c66fc5cca5..ef2aa68067f 100644 --- a/packages/mdc-dialog/package.json +++ b/packages/mdc-dialog/package.json @@ -1,6 +1,6 @@ { "name": "@material/dialog", - "version": "0.37.0", + "version": "0.37.1", "description": "The Material Components Web dialog component", "license": "Apache-2.0", "keywords": [ @@ -18,10 +18,10 @@ "@material/animation": "^0.34.0", "@material/base": "^0.35.0", "@material/elevation": "^0.36.1", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0", + "@material/typography": "^0.37.1", "focus-trap": "^2.3.0" }, "publishConfig": { diff --git a/packages/mdc-drawer/package.json b/packages/mdc-drawer/package.json index 62cdf8f9717..296636b9bf9 100644 --- a/packages/mdc-drawer/package.json +++ b/packages/mdc-drawer/package.json @@ -1,6 +1,6 @@ { "name": "@material/drawer", - "version": "0.36.1", + "version": "0.37.1", "description": "The Material Components Web drawer component", "license": "Apache-2.0", "keywords": [ @@ -20,6 +20,6 @@ "@material/elevation": "^0.36.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-fab/package.json b/packages/mdc-fab/package.json index a727096a1b5..1eb1a9e5bfe 100644 --- a/packages/mdc-fab/package.json +++ b/packages/mdc-fab/package.json @@ -1,7 +1,7 @@ { "name": "@material/fab", "description": "The Material Components for the web floating action button component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -16,9 +16,9 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/elevation": "^0.36.1", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-floating-label/package.json b/packages/mdc-floating-label/package.json index ccec73e1f94..6dd8d8dd68e 100644 --- a/packages/mdc-floating-label/package.json +++ b/packages/mdc-floating-label/package.json @@ -1,7 +1,7 @@ { "name": "@material/floating-label", "description": "The Material Components for the web floating-label component", - "version": "0.36.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -21,6 +21,6 @@ "@material/base": "^0.35.0", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-form-field/package.json b/packages/mdc-form-field/package.json index f2625bc9ac3..4b7127ad529 100644 --- a/packages/mdc-form-field/package.json +++ b/packages/mdc-form-field/package.json @@ -1,7 +1,7 @@ { "name": "@material/form-field", "description": "Material Components for the web wrapper for laying out form fields and labels next to one another", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -16,8 +16,8 @@ "dependencies": { "@material/base": "^0.35.0", "@material/rtl": "^0.36.0", - "@material/selection-control": "^0.37.0", + "@material/selection-control": "^0.37.1", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-grid-list/package.json b/packages/mdc-grid-list/package.json index c009205c7c4..602da5815d7 100644 --- a/packages/mdc-grid-list/package.json +++ b/packages/mdc-grid-list/package.json @@ -1,6 +1,6 @@ { "name": "@material/grid-list", - "version": "0.36.0", + "version": "0.37.1", "description": "The Material Components for the web grid list component", "license": "Apache-2.0", "repository": { @@ -16,7 +16,7 @@ "@material/base": "^0.35.0", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-icon-button/package.json b/packages/mdc-icon-button/package.json index 9969bc899e6..a252e062699 100644 --- a/packages/mdc-icon-button/package.json +++ b/packages/mdc-icon-button/package.json @@ -1,7 +1,7 @@ { "name": "@material/icon-button", "description": "The Material Components for the web icon button component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache 2.0", "keywords": [ "material components", @@ -16,7 +16,7 @@ }, "dependencies": { "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/theme": "^0.35.0" }, "publishConfig": { diff --git a/packages/mdc-icon-toggle/package.json b/packages/mdc-icon-toggle/package.json index dc43288564a..2bfa1e1a50d 100644 --- a/packages/mdc-icon-toggle/package.json +++ b/packages/mdc-icon-toggle/package.json @@ -1,7 +1,7 @@ { "name": "@material/icon-toggle", "description": "The Material Components for the web icon toggle component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -16,7 +16,7 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/theme": "^0.35.0" } } diff --git a/packages/mdc-image-list/package.json b/packages/mdc-image-list/package.json index 3231567c394..18e5811494c 100644 --- a/packages/mdc-image-list/package.json +++ b/packages/mdc-image-list/package.json @@ -1,6 +1,6 @@ { "name": "@material/image-list", - "version": "0.35.0", + "version": "0.37.1", "description": "The Material Components for the web image list component", "license": "Apache-2.0", "repository": { @@ -14,7 +14,7 @@ ], "dependencies": { "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-list/package.json b/packages/mdc-list/package.json index f02172f4eef..50de312b9f9 100644 --- a/packages/mdc-list/package.json +++ b/packages/mdc-list/package.json @@ -1,7 +1,7 @@ { "name": "@material/list", "description": "The Material Components for the web list component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -14,9 +14,9 @@ }, "dependencies": { "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-menu/package.json b/packages/mdc-menu/package.json index 33967b1b6af..854d81641d7 100644 --- a/packages/mdc-menu/package.json +++ b/packages/mdc-menu/package.json @@ -1,6 +1,6 @@ { "name": "@material/menu", - "version": "0.36.1", + "version": "0.37.1", "description": "The Material Components for the web menu component", "license": "Apache-2.0", "keywords": [ @@ -18,6 +18,6 @@ "@material/base": "^0.35.0", "@material/elevation": "^0.36.1", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-notched-outline/package.json b/packages/mdc-notched-outline/package.json index 073871a9a5e..311efe632c4 100644 --- a/packages/mdc-notched-outline/package.json +++ b/packages/mdc-notched-outline/package.json @@ -1,7 +1,7 @@ { "name": "@material/notched-outline", "description": "The Material Components for the web notched-outline component", - "version": "0.35.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", diff --git a/packages/mdc-radio/package.json b/packages/mdc-radio/package.json index e47760b5701..c1ce70239ea 100644 --- a/packages/mdc-radio/package.json +++ b/packages/mdc-radio/package.json @@ -1,7 +1,7 @@ { "name": "@material/radio", "description": "The Material Components for the web radio component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -16,8 +16,8 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", - "@material/selection-control": "^0.37.0", + "@material/ripple": "^0.37.1", + "@material/selection-control": "^0.37.1", "@material/theme": "^0.35.0" } } diff --git a/packages/mdc-ripple/package.json b/packages/mdc-ripple/package.json index 6c5ad4b2416..5f8c252811b 100644 --- a/packages/mdc-ripple/package.json +++ b/packages/mdc-ripple/package.json @@ -1,7 +1,7 @@ { "name": "@material/ripple", "description": "The Material Components for the web Ink Ripple effect for web element interactions", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", diff --git a/packages/mdc-select/package.json b/packages/mdc-select/package.json index ca6ee673bb6..1e26beb5d9c 100644 --- a/packages/mdc-select/package.json +++ b/packages/mdc-select/package.json @@ -1,7 +1,7 @@ { "name": "@material/select", "description": "The Material Components web select (text field drop-down) component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -17,12 +17,12 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/floating-label": "^0.36.0", + "@material/floating-label": "^0.37.1", "@material/line-ripple": "^0.35.0", - "@material/notched-outline": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/notched-outline": "^0.37.1", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-selection-control/package.json b/packages/mdc-selection-control/package.json index 99ff95ce6c1..8efa677b6af 100644 --- a/packages/mdc-selection-control/package.json +++ b/packages/mdc-selection-control/package.json @@ -1,7 +1,7 @@ { "name": "@material/selection-control", "description": "The set of base classes for Material selection controls", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "main": "index.js", "repository": { @@ -9,7 +9,7 @@ "url": "https://github.com/material-components/material-components-web.git" }, "dependencies": { - "@material/ripple": "^0.37.0" + "@material/ripple": "^0.37.1" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-snackbar/package.json b/packages/mdc-snackbar/package.json index f6bc565abd7..2723725b3fc 100644 --- a/packages/mdc-snackbar/package.json +++ b/packages/mdc-snackbar/package.json @@ -1,7 +1,7 @@ { "name": "@material/snackbar", "description": "The Material Components for the web snackbar component", - "version": "0.36.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -18,6 +18,6 @@ "@material/base": "^0.35.0", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-tab/package.json b/packages/mdc-tab/package.json index 60321f48b83..1caf3f5594d 100644 --- a/packages/mdc-tab/package.json +++ b/packages/mdc-tab/package.json @@ -1,7 +1,7 @@ { "name": "@material/tab", "description": "The Material Components for the web tab component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "private": true, "keywords": [ @@ -16,10 +16,10 @@ }, "dependencies": { "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.30.0", "@material/theme": "^0.30.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-tabs/package.json b/packages/mdc-tabs/package.json index 305e6b1a7ae..556c5e72d51 100644 --- a/packages/mdc-tabs/package.json +++ b/packages/mdc-tabs/package.json @@ -1,6 +1,6 @@ { "name": "@material/tabs", - "version": "0.37.0", + "version": "0.37.1", "description": "The Material Components for the web tabs component", "license": "Apache-2.0", "repository": { @@ -18,9 +18,9 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-textfield/package.json b/packages/mdc-textfield/package.json index ced319890e3..7daf454510d 100644 --- a/packages/mdc-textfield/package.json +++ b/packages/mdc-textfield/package.json @@ -1,7 +1,7 @@ { "name": "@material/textfield", "description": "The Material Components for the web text field component", - "version": "0.37.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", @@ -17,12 +17,12 @@ "dependencies": { "@material/animation": "^0.34.0", "@material/base": "^0.35.0", - "@material/floating-label": "^0.36.0", + "@material/floating-label": "^0.37.1", "@material/line-ripple": "^0.35.0", - "@material/notched-outline": "^0.35.0", - "@material/ripple": "^0.37.0", + "@material/notched-outline": "^0.37.1", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" } } diff --git a/packages/mdc-toolbar/package.json b/packages/mdc-toolbar/package.json index 9dfcc92aeb4..7c24a4c5520 100644 --- a/packages/mdc-toolbar/package.json +++ b/packages/mdc-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@material/toolbar", - "version": "0.37.0", + "version": "0.37.1", "description": "The Material Components for the web toolbar component", "license": "Apache-2.0", "repository": { @@ -15,10 +15,10 @@ "dependencies": { "@material/base": "^0.35.0", "@material/elevation": "^0.36.1", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-top-app-bar/package.json b/packages/mdc-top-app-bar/package.json index 0b9fdf5dbf5..449e8365048 100644 --- a/packages/mdc-top-app-bar/package.json +++ b/packages/mdc-top-app-bar/package.json @@ -1,6 +1,6 @@ { "name": "@material/top-app-bar", - "version": "0.37.0", + "version": "0.37.1", "description": "The Material Components for the web top app bar component", "license": "Apache-2.0", "repository": { @@ -18,10 +18,10 @@ "@material/animation": "^0.34.0", "@material/base": "^0.35.0", "@material/elevation": "^0.36.1", - "@material/ripple": "^0.37.0", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", "@material/theme": "^0.35.0", - "@material/typography": "^0.35.0" + "@material/typography": "^0.37.1" }, "publishConfig": { "access": "public" diff --git a/packages/mdc-typography/package.json b/packages/mdc-typography/package.json index fddb579248b..2d5af6089c4 100644 --- a/packages/mdc-typography/package.json +++ b/packages/mdc-typography/package.json @@ -1,7 +1,7 @@ { "name": "@material/typography", "description": "Typography classes, mixins, and variables for Material Components for the web", - "version": "0.35.0", + "version": "0.37.1", "license": "Apache-2.0", "keywords": [ "material components", From c1fa93ad278ab4451df8669c640dcfc978b3ea68 Mon Sep 17 00:00:00 2001 From: Abhinay Omkar Date: Mon, 16 Jul 2018 17:24:25 -0400 Subject: [PATCH 07/53] docs: Update CHANGELOG.md --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e741615c2..d84ea0a126d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + +## [0.37.1](https://github.com/material-components/material-components-web/compare/v0.37.0...v0.37.1) (2018-07-16) + + +### Bug Fixes + +* hot-patching closure annotations. ([#3024](https://github.com/material-components/material-components-web/issues/3024)) ([d5b95ab](https://github.com/material-components/material-components-web/commit/d5b95ab)) +* **button:** Remove dense/stroked line-height tweaks to improve alignment ([#3028](https://github.com/material-components/material-components-web/issues/3028)) ([8b5f595](https://github.com/material-components/material-components-web/commit/8b5f595)) +* **notched-outline:** Remove unused dependency from scss ([#3044](https://github.com/material-components/material-components-web/issues/3044)) ([85ecf11](https://github.com/material-components/material-components-web/commit/85ecf11)) +* **typography:** Update variable reference to work for newer versions of ruby-sass ([#3047](https://github.com/material-components/material-components-web/issues/3047)) ([0dfad9a](https://github.com/material-components/material-components-web/commit/0dfad9a)) + + + # [0.37.0](https://github.com/material-components/material-components-web/compare/v0.36.0...v0.37.0) (2018-07-02) From cd1f9724f930e452bf29628192b8b6ef475abfd4 Mon Sep 17 00:00:00 2001 From: Will Ernest <34519388+williamernest@users.noreply.github.com> Date: Mon, 16 Jul 2018 14:45:49 -0700 Subject: [PATCH 08/53] feat(list): Add single selection (#2970) Adds a single selection feature to the list with accessibility support. --- demos/list.html | 174 ++++++++++++++++- demos/list.scss | 10 +- packages/mdc-list/README.md | 95 ++++++++- packages/mdc-list/adapter.js | 51 ++++- packages/mdc-list/constants.js | 4 +- packages/mdc-list/foundation.js | 101 +++++++++- packages/mdc-list/index.js | 85 ++++++++- packages/mdc-list/mdc-list.scss | 24 ++- test/unit/mdc-list/foundation.test.js | 265 +++++++++++++++++++++++++- test/unit/mdc-list/mdc-list.test.js | 180 ++++++++++++++++- 10 files changed, 929 insertions(+), 60 deletions(-) diff --git a/demos/list.html b/demos/list.html index 6b4e520cdbe..819e3d7b8e9 100644 --- a/demos/list.html +++ b/demos/list.html @@ -39,7 +39,7 @@
    -
  • +
  • @@ -101,7 +101,7 @@ aria-labelledby="toggle-rtl-label" />
    -
    @@ -279,6 +279,72 @@

    Graphic Example - Icon with Text

+
+

Leading Checkbox

+
    +
  • + +
    + +
    + + + +
    +
    +
    +
    + +
  • +
  • + +
    + +
    + + + +
    +
    +
    +
    + +
  • +
  • + +
    + +
    + + + +
    +
    +
    +
    + +
  • +
+

Avatar List

    Metadata (Dense)
+
+

Trailing Checkbox

+
    +
  • + + +
    + +
    + + + +
    +
    +
    +
    +
  • +
  • + + +
    + +
    + + + +
    +
    +
    +
    +
  • +
  • + + +
    + +
    + + + +
    +
    +
    +
    +
  • +
+

Avatar + Metadata

    Example - Interactive List diff --git a/demos/list.scss b/demos/list.scss index 28b177722cd..fbae881eb8e 100644 --- a/demos/list.scss +++ b/demos/list.scss @@ -68,7 +68,7 @@ a.material-icons { } #demo-wrapper .mdc-list, #demo-wrapper .mdc-list-group { - border: 1px solid rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, .1); } #demo-wrapper .mdc-list-group .mdc-list { @@ -76,18 +76,18 @@ a.material-icons { } #demo-wrapper h2 { - font-size: 1.5em; - margin-bottom: 0.8em; + margin-bottom: .8em; margin-left: 24px; + font-size: 1.5em; } #demo-wrapper h3 { - margin-bottom: 0.8em; + margin-bottom: .8em; } .hero .mdc-list { @include mdc-elevation(4); - background-color: white; min-width: 320px; + background-color: white; } diff --git a/packages/mdc-list/README.md b/packages/mdc-list/README.md index 04f08d9d699..0c34bf80106 100644 --- a/packages/mdc-list/README.md +++ b/packages/mdc-list/README.md @@ -135,6 +135,44 @@ OR
``` +### Single Selection List + +MDC List can handle selecting/deselecting list elements based on click or keyboard action. When enabled, the `space` and `enter` keys (or `click` event) will trigger an +single list item to become selected or deselected. + +```html +
    +
  • Single-line item
  • +
  • Single-line item
  • +
  • Single-line item
  • +
+``` + +```js +var listEle = document.getElementById('my-list'); +var list = new mdc.list.MDCList(listEle); +list.singleSelection = true; +``` + +#### Pre-selected list item + +When rendering the list with a pre-selected list item, the list item that needs to be selected should contain +the `mdc-list-item--selected` class and `aria-selected="true"` attribute before creating the list. + +```html +
    +
  • Single-line item
  • +
  • Single-line item
  • +
  • Single-line item
  • +
+``` + +```js +var listEle = document.getElementById('my-list'); +var list = new mdc.list.MDCList(listEle); +list.singleSelection = true; +``` + ## Style Customization ### CSS Classes @@ -163,7 +201,7 @@ CSS Class | Description > NOTE: the difference between selected and activated states: -* *Selected* state should be implemented on the `.list-item` when it is likely to change soon. Eg., selecting one or more photos to share in Google Photos. +* *Selected* state should be implemented on the `.mdc-list-item` when it is likely to change soon. Eg., selecting one or more photos to share in Google Photos. * Multiple items can be selected at the same time when using the *selected* state. * *Activated* state is similar to selected state, however should only be implemented once within a specific list. * *Activated* state is more permanent than selected state, and will **NOT** change soon relative to the lifetime of the page. @@ -188,9 +226,10 @@ within the list component. You should not add `tabindex` to any of the `li` elem As the user navigates through the list, any `button` or `a` elements within the list will receive `tabindex="-1"` when the list item is not focused. When the list item receives focus, the child `button` and `a` elements will -receive `tabIndex="0"`. This allows for the user to tab through list items elements and then tab to the +receive `tabIndex="0"`. This allows for the user to tab through list item elements and then tab to the first element after the list. The `Arrow`, `Home`, and `End` keys should be used for navigating internal list elements. -The MDCList will perform the following actions for each key press +If `singleSelection=true`, the list will allow the user to use the `Space` or `Enter` keys to select or deselect +a list item. The MDCList will perform the following actions for each key press Key | Action --- | --- @@ -200,11 +239,50 @@ Key | Action `ArrowRight` | When the list is in a horizontal orientation (default), it will cause the next list item to receive focus. `Home` | Will cause the first list item in the list to receive focus. `End` | Will cause the last list item in the list to receive focus. +`Space` | Will cause the currently focused list item to become selected/deselected if `singleSelection=true`. +`Enter` | Will cause the currently focused list item to become selected/deselected if `singleSelection=true`. ## Usage within Web Frameworks If you are using a JavaScript framework, such as React or Angular, you can create a List for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). +### Considerations for Advanced Approach + +The `MDCListFoundation` expects the HTML to be setup a certain way before being used. This setup is a part of the `layout()` and `singleSelection()` functions within the `index.js`. + +#### Setup in `layout()` + +The default component requires that every list item receives a `tabindex` value so that it can receive focus +(`li` elements cannot receive focus at all without a `tabindex` value). Any element not already containing a +`tabindex` attribute will receive `tabindex=-1`. The first list item should have `tabindex="0"` so that the +user can find the first element using the `tab` key, but subsequent `tab` keys strokes will cause focus to +skip over the entire list. If the list items contain sub-elements that are focusable (`button` or `a` elements), +these should also receive `tabIndex="-1"`. + +```html +
    +
  • Single-line item
  • +
  • Single-line item
  • +
  • Single-line item
  • +
+``` + +#### Setup in `singleSelection()` + +When implementing a component that will use the single selection variant, the HTML should be modified to include +the `aria-selected` attribute, the `mdc-list-item--selected` class should be added, and the `tabindex` of the selected +element should be `0`. The first list item should have the `tabindex` updated to `-1`. The foundation method +`setSelectedIndex()` should be called with the initially selected element immediately after the foundation is +instantiated. + +```html +
    +
  • Single-line item
  • +
  • Single-line item
  • +
  • Single-line item
  • +
+``` + ### `MDCListAdapter` Method Signature | Description @@ -212,8 +290,12 @@ Method Signature | Description `getListItemCount() => Number` | Returns the total number of list items (elements with `mdc-list-item` class) that are direct children of the `root_` element. `getFocusedElementIndex() => Number` | Returns the `index` value of the currently focused element. `getListItemIndex(ele: Element) => Number` | Returns the `index` value of the provided `ele` element. -`focusItemAtIndex(ndx: Number) => void` | Focuses the list item at the `ndx` value specified. -`setTabIndexForListItemChildren(ndx: Number, value: Number) => void` | Sets the `tabindex` attribute to `value` for each child `button` and `a` element in the list item at the `ndx` specified. +`setAttributeForElementIndex(index: Number, attr: String, value: String) => void` | Sets the `attr` attribute to `value` for the list item at `index`. +`addClassForElementIndex(index: Number, className: String) => void` | Adds the `className` class to the list item at `index`. +`removeClassForElementIndex(index: Number, className: String) => void` | Removes the `className` class to the list item at `index`. +`focusItemAtIndex(index: Number) => void` | Focuses the list item at the `index` value specified. +`isElementFocusable(ele: Element) => boolean` | Returns true if `ele` contains a focusable child element. +`setTabIndexForListItemChildren(index: Number, value: Number) => void` | Sets the `tabindex` attribute to `value` for each child `button` and `a` element in the list item at the `index` specified. ### `MDCListFoundation` @@ -221,9 +303,12 @@ Method Signature | Description --- | --- `setWrapFocus(value: Boolean) => void` | Sets the list to allow the up arrow on the first element to focus the last element of the list and vice versa. `setVerticalOrientation(value: Boolean) => void` | Sets the list to an orientation causing the keys used for navigation to change. `true` results in the Up/Down arrow keys being used. `false` results in the Left/Right arrow keys being used. +`setSingleSelection(value: Boolean) => void` | Sets the list to be a selection list. Enables the `enter` and `space` keys for selecting/deselecting a list item. +`setSelectedIndex(index: Number) => void` | Toggles the `selected` state of the list item at index `index`. `handleFocusIn(evt: Event) => void` | Handles the changing of `tabindex` to `0` for all `button` and `a` elements when a list item receives focus. `handleFocusOut(evt: Event) => void` | Handles the changing of `tabindex` to `-1` for all `button` and `a` elements when a list item loses focus. `handleKeydown(evt: Event) => void` | Handles determining if a focus action should occur when a key event is triggered. +`handleClick(evt: Event) => void` | Handles toggling the selected/deselected state for a list item when clicked. This method is only used by the single selection list. `focusNextElement(index: Number) => void` | Handles focusing the next element using the current `index`. `focusPrevElement(index: Number) => void` | Handles focusing the previous element using the current `index`. `focusFirstElement() => void` | Handles focusing the first element in a list. diff --git a/packages/mdc-list/adapter.js b/packages/mdc-list/adapter.js index dfe82fa5895..243d9031300 100644 --- a/packages/mdc-list/adapter.js +++ b/packages/mdc-list/adapter.js @@ -31,29 +31,66 @@ * @record */ class MDCListAdapter { - /** @return {Number} */ + /** @return {number} */ getListItemCount() {} /** - * @return {Number} */ + * @return {number} */ getFocusedElementIndex() {} /** @param {Element} node */ getListItemIndex(node) {} + /** + * @param {number} index + * @param {string} attribute + * @param {string} value + */ + setAttributeForElementIndex(index, attribute, value) {} + + /** + * @param {number} index + * @param {string} attribute + */ + removeAttributeForElementIndex(index, attribute) {} + + /** + * @param {number} index + * @param {string} className + */ + addClassForElementIndex(index, className) {} + + /** + * @param {number} index + * @param {string} className + */ + removeClassForElementIndex(index, className) {} + /** * Focuses list item at the index specified. - * @param {Number} ndx + * @param {number} index + */ + focusItemAtIndex(index) {} + + /** + * Checks if the provided element is a focusable sub-element. + * @param {Element} ele + */ + isElementFocusable(ele) {} + + /** + * Checks if the provided element is contains the mdc-list-item class. + * @param {Element} ele */ - focusItemAtIndex(ndx) {} + isListItem(ele) {} /** * Sets the tabindex to the value specified for all button/a element children of * the list item at the index specified. - * @param {Number} listItemIndex - * @param {Number} tabIndexValue + * @param {number} listItemIndex + * @param {number} tabIndexValue */ setTabIndexForListItemChildren(listItemIndex, tabIndexValue) {} } -export {MDCListAdapter}; +export default MDCListAdapter; diff --git a/packages/mdc-list/constants.js b/packages/mdc-list/constants.js index 2952ccf590b..536a4bedd0f 100644 --- a/packages/mdc-list/constants.js +++ b/packages/mdc-list/constants.js @@ -18,14 +18,16 @@ /** @enum {string} */ const cssClasses = { LIST_ITEM_CLASS: 'mdc-list-item', + LIST_ITEM_SELECTED_CLASS: 'mdc-list-item--selected', }; /** @enum {string} */ const strings = { ARIA_ORIENTATION: 'aria-orientation', ARIA_ORIENTATION_VERTICAL: 'vertical', + ARIA_SELECTED: 'aria-selected', FOCUSABLE_CHILD_ELEMENTS: 'button:not(:disabled), a', - ITEMS_SELECTOR: '.mdc-list-item', + ENABLED_ITEMS_SELECTOR: '.mdc-list-item:not(.mdc-list-item--disabled)', }; export {strings, cssClasses}; diff --git a/packages/mdc-list/foundation.js b/packages/mdc-list/foundation.js index 641b2e401cc..5771679cced 100644 --- a/packages/mdc-list/foundation.js +++ b/packages/mdc-list/foundation.js @@ -16,25 +16,39 @@ */ import MDCFoundation from '@material/base/foundation'; +import MDCListAdapter from './adapter'; import {strings, cssClasses} from './constants'; const ELEMENTS_KEY_ALLOWED_IN = ['input', 'button', 'textarea', 'select']; class MDCListFoundation extends MDCFoundation { + /** @return enum {string} */ static get strings() { return strings; } + /** @return enum {string} */ static get cssClasses() { return cssClasses; } + /** + * {@see MDCListAdapter} for typing information on parameters and return + * types. + * @return {!MDCListAdapter} + */ static get defaultAdapter() { - return /** {MDCListAdapter */ ({ + return /** @type {!MDCListAdapter} */ ({ getListItemCount: () => {}, getFocusedElementIndex: () => {}, getListItemIndex: () => {}, + setAttributeForElementIndex: () => {}, + removeAttributeForElementIndex: () => {}, + addClassForElementIndex: () => {}, + removeClassForElementIndex: () => {}, focusItemAtIndex: () => {}, + isElementFocusable: () => {}, + isListItem: () => {}, setTabIndexForListItemChildren: () => {}, }); } @@ -45,6 +59,10 @@ class MDCListFoundation extends MDCFoundation { this.wrapFocus_ = false; /** {boolean} */ this.isVertical_ = true; + /** {boolean} */ + this.isSingleSelectionList_ = false; + /** {number} */ + this.selectedIndex_ = -1; } /** @@ -63,6 +81,48 @@ class MDCListFoundation extends MDCFoundation { this.isVertical_ = value; } + /** + * Sets the isSingleSelectionList_ private variable. + * @param {boolean} value + */ + setSingleSelection(value) { + this.isSingleSelectionList_ = value; + } + + /** @param {number} index */ + setSelectedIndex(index) { + if (index === this.selectedIndex_) { + this.adapter_.removeAttributeForElementIndex(this.selectedIndex_, strings.ARIA_SELECTED); + this.adapter_.removeClassForElementIndex(this.selectedIndex_, cssClasses.LIST_ITEM_SELECTED_CLASS); + + // Used to reset the first element to tabindex=0 when deselecting a list item. + // If already on the first list item, leave tabindex at 0. + if (this.selectedIndex_ >= 0) { + this.adapter_.setAttributeForElementIndex(this.selectedIndex_, 'tabindex', -1); + this.adapter_.setAttributeForElementIndex(0, 'tabindex', 0); + } + this.selectedIndex_ = -1; + return; + } + + if (this.selectedIndex_ >= 0) { + this.adapter_.removeAttributeForElementIndex(this.selectedIndex_, strings.ARIA_SELECTED); + this.adapter_.removeClassForElementIndex(this.selectedIndex_, cssClasses.LIST_ITEM_SELECTED_CLASS); + this.adapter_.setAttributeForElementIndex(this.selectedIndex_, 'tabindex', -1); + } + + if (index >= 0 && this.adapter_.getListItemCount() > index) { + this.selectedIndex_ = index; + this.adapter_.setAttributeForElementIndex(this.selectedIndex_, strings.ARIA_SELECTED, true); + this.adapter_.addClassForElementIndex(this.selectedIndex_, cssClasses.LIST_ITEM_SELECTED_CLASS); + this.adapter_.setAttributeForElementIndex(this.selectedIndex_, 'tabindex', 0); + + if (this.selectedIndex_ !== 0) { + this.adapter_.setAttributeForElementIndex(0, 'tabindex', -1); + } + } + } + /** * Focus in handler for the list items. * @param evt @@ -71,7 +131,11 @@ class MDCListFoundation extends MDCFoundation { const listItem = this.getListItem_(evt.target); if (!listItem) return; - this.adapter_.setTabIndexForListItemChildren(this.adapter_.getListItemIndex(listItem), 0); + const listItemIndex = this.adapter_.getListItemIndex(listItem); + + if (listItemIndex >= 0) { + this.adapter_.setTabIndexForListItemChildren(listItemIndex, 0); + } } /** @@ -81,8 +145,11 @@ class MDCListFoundation extends MDCFoundation { handleFocusOut(evt) { const listItem = this.getListItem_(evt.target); if (!listItem) return; + const listItemIndex = this.adapter_.getListItemIndex(listItem); - this.adapter_.setTabIndexForListItemChildren(this.adapter_.getListItemIndex(listItem), -1); + if (listItemIndex >= 0) { + this.adapter_.setTabIndexForListItemChildren(listItemIndex, -1); + } } /** @@ -96,6 +163,9 @@ class MDCListFoundation extends MDCFoundation { const arrowDown = evt.key === 'ArrowDown' || evt.keyCode === 40; const isHome = evt.key === 'Home' || evt.keyCode === 36; const isEnd = evt.key === 'End' || evt.keyCode === 35; + const isEnter = evt.key === 'Enter' || evt.keyCode === 13; + const isSpace = evt.key === 'Space' || evt.keyCode === 32; + let currentIndex = this.adapter_.getFocusedElementIndex(); if (currentIndex === -1) { @@ -120,9 +190,26 @@ class MDCListFoundation extends MDCFoundation { } else if (isEnd) { this.preventDefaultEvent_(evt); this.focusLastElement(); + } else if (this.isSingleSelectionList_ && (isEnter || isSpace)) { + this.preventDefaultEvent_(evt); + // Check if the space key was pressed on the list item or a child element. + if (this.adapter_.isListItem(evt.target)) { + this.setSelectedIndex(currentIndex); + } } } + /** + * Click handler for the list. + */ + handleClick() { + const currentIndex = this.adapter_.getFocusedElementIndex(); + + if (currentIndex === -1) return; + + this.setSelectedIndex(currentIndex); + } + /** * Ensures that preventDefault is only called if the containing element doesn't * consume the event, and it will cause an unintended scroll. @@ -138,7 +225,7 @@ class MDCListFoundation extends MDCFoundation { /** * Focuses the next element on the list. - * @param {Number} index + * @param {number} index */ focusNextElement(index) { const count = this.adapter_.getListItemCount(); @@ -156,7 +243,7 @@ class MDCListFoundation extends MDCFoundation { /** * Focuses the previous element on the list. - * @param {Number} index + * @param {number} index */ focusPrevElement(index) { let prevIndex = index - 1; @@ -191,7 +278,7 @@ class MDCListFoundation extends MDCFoundation { * @private */ getListItem_(target) { - while (!target.classList.contains(cssClasses.LIST_ITEM_CLASS)) { + while (!this.adapter_.isListItem(target)) { if (!target.parentElement) return null; target = target.parentElement; } @@ -199,4 +286,4 @@ class MDCListFoundation extends MDCFoundation { } } -export {MDCListFoundation}; +export default MDCListFoundation; diff --git a/packages/mdc-list/index.js b/packages/mdc-list/index.js index f27a251fba4..948d93cd41a 100644 --- a/packages/mdc-list/index.js +++ b/packages/mdc-list/index.js @@ -16,19 +16,22 @@ */ import MDCComponent from '@material/base/component'; -import {MDCListFoundation} from './foundation'; -import {strings} from './constants'; +import MDCListFoundation from './foundation'; +import MDCListAdapter from './adapter'; +import {cssClasses, strings} from './constants'; /** * @extends MDCComponent */ -export class MDCList extends MDCComponent { +class MDCList extends MDCComponent { /** @param {...?} args */ constructor(...args) { super(...args); /** @private {!Function} */ this.handleKeydown_; /** @private {!Function} */ + this.handleClick_; + /** @private {!Function} */ this.focusInEventListener_; /** @private {!Function} */ this.focusOutEventListener_; @@ -44,12 +47,14 @@ export class MDCList extends MDCComponent { destroy() { this.root_.removeEventListener('keydown', this.handleKeydown_); + this.root_.removeEventListener('click', this.handleClick_); this.root_.removeEventListener('focusin', this.focusInEventListener_); this.root_.removeEventListener('focusout', this.focusOutEventListener_); } initialSyncWithDOM() { this.handleKeydown_ = this.foundation_.handleKeydown.bind(this.foundation_); + this.handleClick_ = this.foundation_.handleClick.bind(this.foundation_); this.focusInEventListener_ = this.foundation_.handleFocusIn.bind(this.foundation_); this.focusOutEventListener_ = this.foundation_.handleFocusOut.bind(this.foundation_); this.root_.addEventListener('keydown', this.handleKeydown_); @@ -80,8 +85,7 @@ export class MDCList extends MDCComponent { /** @return Array*/ get listElements_() { - return [].slice.call(this.root_.querySelectorAll(strings.ITEMS_SELECTOR)) - .filter((ele) => ele.parentElement === this.root_); + return [].slice.call(this.root_.querySelectorAll(strings.ENABLED_ITEMS_SELECTOR)); } /** @param {boolean} value */ @@ -89,18 +93,79 @@ export class MDCList extends MDCComponent { this.foundation_.setWrapFocus(value); } + /** @param {boolean} isSingleSelectionList */ + set singleSelection(isSingleSelectionList) { + if (isSingleSelectionList) { + this.root_.addEventListener('click', this.handleClick_); + } else { + this.root_.removeEventListener('click', this.handleClick_); + } + + this.foundation_.setSingleSelection(isSingleSelectionList); + const selectedElement = this.root_.querySelector('.mdc-list-item--selected'); + + if (selectedElement) { + this.selectedIndex = this.listElements_.indexOf(selectedElement); + } + } + + /** @param {number} index */ + set selectedIndex(index) { + this.foundation_.setSelectedIndex(index); + } + /** @return {!MDCListFoundation} */ getDefaultFoundation() { - return new MDCListFoundation(/** @type {!MDCListAdapter} */{ + return new MDCListFoundation(/** @type {!MDCListAdapter} */ (Object.assign({ getListItemCount: () => this.listElements_.length, getFocusedElementIndex: () => this.listElements_.indexOf(document.activeElement), getListItemIndex: (node) => this.listElements_.indexOf(node), - focusItemAtIndex: (ndx) => this.listElements_[ndx].focus(), + setAttributeForElementIndex: (index, attr, value) => { + const element = this.listElements_[index]; + if (element) { + element.setAttribute(attr, value); + } + }, + removeAttributeForElementIndex: (index, attr) => { + const element = this.listElements_[index]; + if (element) { + element.removeAttribute(attr); + } + }, + addClassForElementIndex: (index, className) => { + const element = this.listElements_[index]; + if (element) { + element.classList.add(className); + } + }, + removeClassForElementIndex: (index, className) => { + const element = this.listElements_[index]; + if (element) { + element.classList.remove(className); + } + }, + isListItem: (target) => target.classList.contains(cssClasses.LIST_ITEM_CLASS), + focusItemAtIndex: (index) => { + const element = this.listElements_[index]; + if (element) { + element.focus(); + } + }, + isElementFocusable: (ele) => { + if (!ele) return false; + let matches = Element.prototype.matches; + if (!matches) { // IE uses a different name for the same functionality + matches = Element.prototype.msMatchesSelector; + } + return matches.call(ele, strings.FOCUSABLE_CHILD_ELEMENTS); + }, setTabIndexForListItemChildren: (listItemIndex, tabIndexValue) => { - const listItemChildren = [].slice.call(this.listElements_[listItemIndex] - .querySelectorAll(strings.FOCUSABLE_CHILD_ELEMENTS)); + const element = this.listElements_[listItemIndex]; + const listItemChildren = [].slice.call(element.querySelectorAll(strings.FOCUSABLE_CHILD_ELEMENTS)); listItemChildren.forEach((ele) => ele.setAttribute('tabindex', tabIndexValue)); }, - }); + }))); } } + +export {MDCList, MDCListFoundation}; diff --git a/packages/mdc-list/mdc-list.scss b/packages/mdc-list/mdc-list.scss index 7898323fc72..72e73f263be 100644 --- a/packages/mdc-list/mdc-list.scss +++ b/packages/mdc-list/mdc-list.scss @@ -77,6 +77,10 @@ @include mdc-list-item-graphic-ink-color(primary); } +.mdc-list-item--disabled { + @include mdc-list-item-primary-text-ink-color(text-disabled-on-background); +} + .mdc-list-item__graphic { @include mdc-list-graphic-size_(24px); @@ -127,16 +131,6 @@ border-radius: 50%; } -// List items should support states by default, but it should be possible to opt out. -// Direct child combinator is necessary for non-interactive modifier on parent to not match this selector. -:not(.mdc-list--non-interactive) > .mdc-list-item { - @include mdc-ripple-surface; - @include mdc-ripple-radius-bounded; - @include mdc-states; - @include mdc-states-activated(primary); - @include mdc-states-selected(primary); -} - .mdc-list--two-line .mdc-list-item { height: 72px; } @@ -152,6 +146,16 @@ .mdc-list--avatar-list.mdc-list--dense .mdc-list-item__graphic { @include mdc-list-graphic-size_(36px); } + +// List items should support states by default, but it should be possible to opt out. +// Direct child combinator is necessary for non-interactive modifier on parent to not match this selector. +:not(.mdc-list--non-interactive) > :not(.mdc-list-item--disabled).mdc-list-item { + @include mdc-ripple-surface; + @include mdc-ripple-radius-bounded; + @include mdc-states; + @include mdc-states-activated(primary); + @include mdc-states-selected(primary); +} // postcss-bem-linter: end // Override anchor tag styles for the use-case of a list being used for navigation diff --git a/test/unit/mdc-list/foundation.test.js b/test/unit/mdc-list/foundation.test.js index 73de79c7453..454fff911c1 100644 --- a/test/unit/mdc-list/foundation.test.js +++ b/test/unit/mdc-list/foundation.test.js @@ -21,7 +21,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import {MDCListFoundation} from '../../../packages/mdc-list/foundation'; +import MDCListFoundation from '../../../packages/mdc-list/foundation'; import {strings, cssClasses} from '../../../packages/mdc-list/constants'; suite('MDCListFoundation'); @@ -36,8 +36,9 @@ test('exports cssClasses', () => { test('defaultAdapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCListFoundation, [ - 'getListItemCount', 'getFocusedElementIndex', 'getListItemIndex', - 'focusItemAtIndex', 'setTabIndexForListItemChildren', + 'getListItemCount', 'getFocusedElementIndex', 'getListItemIndex', 'setAttributeForElementIndex', + 'removeAttributeForElementIndex', 'addClassForElementIndex', 'removeClassForElementIndex', + 'focusItemAtIndex', 'isElementFocusable', 'isListItem', 'setTabIndexForListItemChildren', ]); }); @@ -63,6 +64,7 @@ test('#handleFocusIn switches list item button/a elements to tabindex=0', () => const event = {target}; td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleFocusIn(event); td.verify(mockAdapter.setTabIndexForListItemChildren(1, 0)); @@ -74,6 +76,7 @@ test('#handleFocusOut switches list item button/a elements to tabindex=-1', () = const event = {target}; td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleFocusOut(event); td.verify(mockAdapter.setTabIndexForListItemChildren(1, -1)); @@ -86,6 +89,7 @@ test('#handleFocusIn switches list item button/a elements to tabindex=0 when tar const event = {target}; td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleFocusIn(event); td.verify(mockAdapter.setTabIndexForListItemChildren(1, 0)); @@ -98,6 +102,7 @@ test('#handleFocusOut switches list item button/a elements to tabindex=-1 when t const event = {target}; td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleFocusOut(event); td.verify(mockAdapter.setTabIndexForListItemChildren(1, -1)); @@ -125,6 +130,33 @@ test('#handleFocusOut does nothing if mdc-list-item is not on element or ancesto td.verify(mockAdapter.setTabIndexForListItemChildren(td.matchers.anything(), td.matchers.anything()), {times: 0}); }); +test('#handleFocusIn does nothing if list item is from nested list', () => { + const {foundation, mockAdapter} = setupTest(); + const parentElement = {classList: ['mdc-list-item']}; + const target = {classList: [], parentElement}; + const event = {target}; + + td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(-1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(false, true); + foundation.handleFocusIn(event); + + td.verify(mockAdapter.setTabIndexForListItemChildren(td.matchers.anything(), td.matchers.anything()), {times: 0}); +}); + + +test('#handleFocusOut does nothing if list item is from nested list', () => { + const {foundation, mockAdapter} = setupTest(); + const parentElement = {classList: ['mdc-list-item']}; + const target = {classList: [], parentElement}; + const event = {target}; + + td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(-1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(false, true); + foundation.handleFocusOut(event); + + td.verify(mockAdapter.setTabIndexForListItemChildren(td.matchers.anything(), td.matchers.anything()), {times: 0}); +}); + test('#handleKeydown does nothing if the key is not used for navigation', () => { const {foundation, mockAdapter} = setupTest(); const preventDefault = td.func('preventDefault'); @@ -162,6 +194,7 @@ test('#handleKeydown navigation key on an empty list does nothing', () => { td.when(mockAdapter.getFocusedElementIndex()).thenReturn(-1); td.when(mockAdapter.getListItemCount()).thenReturn(0); td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(-1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleKeydown(event); td.verify(mockAdapter.focusItemAtIndex(td.matchers.anything()), {times: 0}); @@ -335,6 +368,7 @@ test('#handleKeydown End key on empty list does nothing', () => { td.when(mockAdapter.getFocusedElementIndex()).thenReturn(-1); td.when(mockAdapter.getListItemCount()).thenReturn(0); td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(-1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleKeydown(event); td.verify(mockAdapter.focusItemAtIndex(td.matchers.anything()), {times: 0}); @@ -367,6 +401,8 @@ test('#handleKeydown finds the first ancestor with mdc-list-item', () => { td.when(mockAdapter.getFocusedElementIndex()).thenReturn(-1); td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(1); td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(false); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); foundation.handleKeydown(event); td.verify(mockAdapter.focusItemAtIndex(0), {times: 1}); @@ -383,7 +419,230 @@ test('#handleKeydown does not find ancestor with mdc-list-item so returns early' td.when(mockAdapter.getFocusedElementIndex()).thenReturn(-1); td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(-1); td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(false); foundation.handleKeydown(event); td.verify(preventDefault(), {times: 0}); }); + +test('#handleKeydown space key causes preventDefault to be called on the event when singleSelection=true', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 1}); +}); + +test('#handleKeydown enter key causes preventDefault to be called on the event when singleSelection=true', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Enter', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 1}); +}); + +test('#handleKeydown space key does not cause preventDefault to be called if singleSelection=false', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 0}); +}); + +test('#handleKeydown enter key does not cause preventDefault to be called if singleSelection=false', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Enter', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 0}); +}); + +test('#handleKeydown space key is triggered when singleSelection is true selects the list item', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(target)).thenReturn(true); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 1}); + td.verify(mockAdapter.setAttributeForElementIndex(0, strings.ARIA_SELECTED, true), {times: 1}); +}); + +test('#handleKeydown space key is triggered 2x when singleSelection is true un-selects the list item', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(target)).thenReturn(true); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 2}); + td.verify(mockAdapter.removeAttributeForElementIndex(0, strings.ARIA_SELECTED), {times: 1}); +}); + +test('#handleKeydown space key is triggered when singleSelection is true on second ' + + 'element updates first element tabindex', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(1); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(target)).thenReturn(true); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 1}); + td.verify(mockAdapter.setAttributeForElementIndex(1, 'tabindex', 0), {times: 1}); +}); + +test('#handleKeydown space key is triggered 2x when singleSelection is true on second ' + + 'element updates first element tabindex', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(1); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(target)).thenReturn(true); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + foundation.handleKeydown(event); + + td.verify(preventDefault(), {times: 2}); + td.verify(mockAdapter.setAttributeForElementIndex(0, 'tabindex', 0), {times: 1}); + td.verify(mockAdapter.setAttributeForElementIndex(1, 'tabindex', -1), {times: 1}); +}); + +test('#handleKeydown space key is triggered and focused is moved to a different element', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {key: 'Space', target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(1); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(target)).thenReturn(true); + foundation.setSingleSelection(true); + foundation.handleKeydown(event); + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(2); + foundation.handleKeydown(event); + + td.verify(mockAdapter.setAttributeForElementIndex(1, 'tabindex', -1), {times: 1}); + td.verify(mockAdapter.setAttributeForElementIndex(2, 'tabindex', 0), {times: 1}); +}); + +test('#handleClick when singleSelection=true on a list item should cause the list item to be selected', () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: ['mdc-list-item']}; + const event = {target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(1); + td.when(mockAdapter.getListItemCount()).thenReturn(3); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(true); + foundation.handleClick(event); + + td.verify(mockAdapter.setAttributeForElementIndex(1, 'tabindex', 0), {times: 1}); +}); + +test('#handleClick when singleSelection=true on a button subelement should not cause the list item to be selected', + () => { + const {foundation, mockAdapter} = setupTest(); + const parentElement = {classList: ['mdc-list-item']}; + const preventDefault = td.func('preventDefault'); + const target = {classList: [], parentElement}; + const event = {target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(-1); + td.when(mockAdapter.isElementFocusable(td.matchers.anything())).thenReturn(true); + td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(1); + td.when(mockAdapter.isListItem(td.matchers.anything())).thenReturn(false); + foundation.setSingleSelection(true); + foundation.handleClick(event); + + td.verify(mockAdapter.setAttributeForElementIndex(1, 'tabindex', 0), {times: 0}); + }); + +test('#handleClick when singleSelection=true on an element not in a list item should be ignored', + () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: []}; + const event = {target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(-1); + td.when(mockAdapter.isElementFocusable(td.matchers.anything())).thenReturn(true); + td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(-1); + foundation.setSingleSelection(true); + foundation.handleClick(event); + + td.verify(mockAdapter.setAttributeForElementIndex(1, 'tabindex', 0), {times: 0}); + }); + +test('#handleClick when singleSelection=true on the first element when already selected', + () => { + const {foundation, mockAdapter} = setupTest(); + const preventDefault = td.func('preventDefault'); + const target = {classList: []}; + const event = {target, preventDefault}; + + td.when(mockAdapter.getFocusedElementIndex()).thenReturn(0); + td.when(mockAdapter.isElementFocusable(td.matchers.anything())).thenReturn(true); + td.when(mockAdapter.getListItemIndex(td.matchers.anything())).thenReturn(0); + foundation.setSingleSelection(true); + foundation.handleClick(event); + foundation.handleClick(event); + + td.verify(mockAdapter.setAttributeForElementIndex(0, 'tabindex', 0), {times: 0}); + }); + +test('#focusFirstElement is called when the list is empty does not focus an element', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.getListItemCount()).thenReturn(-1); + foundation.focusFirstElement(); + + td.verify(mockAdapter.focusItemAtIndex(td.matchers.anything()), {times: 0}); +}); + +test('#focusLastElement is called when the list is empty does not focus an element', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.getListItemCount()).thenReturn(-1); + foundation.focusLastElement(); + + td.verify(mockAdapter.focusItemAtIndex(td.matchers.anything()), {times: 0}); +}); diff --git a/test/unit/mdc-list/mdc-list.test.js b/test/unit/mdc-list/mdc-list.test.js index 58f72298fc2..4ede0dda176 100644 --- a/test/unit/mdc-list/mdc-list.test.js +++ b/test/unit/mdc-list/mdc-list.test.js @@ -19,17 +19,23 @@ import {assert} from 'chai'; import td from 'testdouble'; import bel from 'bel'; -import {MDCList} from '../../../packages/mdc-list'; -import {MDCListFoundation} from '../../../packages/mdc-list/foundation'; +import {MDCList, MDCListFoundation} from '../../../packages/mdc-list'; +import domEvents from 'dom-events'; function getFixture() { return bel`
    -
  • Fruit -
  • -
  • Pasta -
  • -
  • Pizza
  • +
  • + Fruit + +
  • +
  • + Pasta + +
  • +
  • + Pizza +
`; } @@ -72,6 +78,94 @@ test('#adapter.getListItemIndex returns the index of the element specified', () document.body.removeChild(root); }); +test('#adapter.setAttributeForElementIndex does nothing if the element at index does not exist', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const func = () => { + component.getDefaultFoundation().adapter_.setAttributeForElementIndex(5, 'foo', 'bar'); + }; + assert.doesNotThrow(func); + document.body.removeChild(root); +}); + +test('#adapter.setAttributeForElementIndex sets the attribute for the list element at index specified', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const selectedNode = root.querySelectorAll('.mdc-list-item')[1]; + component.getDefaultFoundation().adapter_.setAttributeForElementIndex(1, 'foo', 'bar'); + assert.equal('bar', selectedNode.getAttribute('foo')); + document.body.removeChild(root); +}); + +test('#adapter.removeAttributeForElementIndex does nothing if the element at index does not exist', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const func = () => { + component.getDefaultFoundation().adapter_.removeAttributeForElementIndex(5, 'foo'); + }; + assert.doesNotThrow(func); + document.body.removeChild(root); +}); + +test('#adapter.removeAttributeForElementIndex sets the attribute for the list element at index specified', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const selectedNode = root.querySelectorAll('.mdc-list-item')[1]; + component.getDefaultFoundation().adapter_.setAttributeForElementIndex(1, 'foo', 'bar'); + component.getDefaultFoundation().adapter_.removeAttributeForElementIndex(1, 'foo'); + assert.isFalse(selectedNode.hasAttribute('foo')); + document.body.removeChild(root); +}); + +test('#adapter.addClassForElementIndex does nothing if the element at index does not exist', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const func = () => { + component.getDefaultFoundation().adapter_.addClassForElementIndex(5, 'foo'); + }; + assert.doesNotThrow(func); + document.body.removeChild(root); +}); + +test('#adapter.addClassForElementIndex adds the class to the list element at index specified', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const selectedNode = root.querySelectorAll('.mdc-list-item')[1]; + component.getDefaultFoundation().adapter_.addClassForElementIndex(1, 'foo'); + assert.isTrue(selectedNode.classList.contains('foo')); + document.body.removeChild(root); +}); + +test('#adapter.removeClassForElementIndex does nothing if the element at index does not exist', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const func = () => { + component.getDefaultFoundation().adapter_.removeClassForElementIndex(5, 'foo'); + }; + assert.doesNotThrow(func); + document.body.removeChild(root); +}); + +test('#adapter.removeClassForElementIndex removes the class from the list element at index specified', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const selectedNode = root.querySelectorAll('.mdc-list-item')[1]; + selectedNode.classList.add('foo'); + component.getDefaultFoundation().adapter_.removeClassForElementIndex(1, 'foo'); + assert.isFalse(selectedNode.classList.contains('foo')); + document.body.removeChild(root); +}); + +test('#adapter.focusItemAtIndex does not throw an error if element at index is undefined/null', () => { + const {root, component} = setupTest(); + document.body.appendChild(root); + const func = () => { + component.getDefaultFoundation().adapter_.focusItemAtIndex(5); + }; + assert.doesNotThrow(func); + document.body.removeChild(root); +}); + test('#adapter.focusItemAtIndex focuses the list item at the index specified', () => { const {root, component} = setupTest(); document.body.appendChild(root); @@ -82,14 +176,46 @@ test('#adapter.focusItemAtIndex focuses the list item at the index specified', ( document.body.removeChild(root); }); +test('adapter#isListItem returns true if the element is a list item', () => { + const {root, component} = setupTest(true); + const item1 = root.querySelectorAll('.mdc-list-item')[0]; + assert.isTrue(component.getDefaultFoundation().adapter_.isListItem(item1)); +}); + +test('adapter#isListItem returns false if the element is a not a list item', () => { + const {root, component} = setupTest(true); + const item1 = root.querySelectorAll('.mdc-list-item button')[0]; + assert.isFalse(component.getDefaultFoundation().adapter_.isListItem(item1)); +}); + +test('adapter#isElementFocusable returns true if the element is a focusable list item sub-element', () => { + const {root, component} = setupTest(true); + const item1 = root.querySelectorAll('.mdc-list-item button')[0]; + assert.isTrue(component.getDefaultFoundation().adapter_.isElementFocusable(item1)); +}); + +test('adapter#isElementFocusable returns false if the element is not a focusable list item sub-element', + () => { + const {root, component} = setupTest(true); + const item1 = root.querySelectorAll('.mdc-list-item')[2]; + assert.isFalse(component.getDefaultFoundation().adapter_.isElementFocusable(item1)); + }); + +test('adapter#isElementFocusable returns false if the element is null/undefined', + () => { + const {component} = setupTest(true); + assert.isFalse(component.getDefaultFoundation().adapter_.isElementFocusable()); + }); + test('#adapter.setTabIndexForListItemChildren sets the child button/a elements of index', () => { const {root, component} = setupTest(); document.body.appendChild(root); const listItemIndex = 1; - const listItem = root.querySelectorAll('.mdclist-item')[listItemIndex]; + const listItem = root.querySelectorAll('.mdc-list-item')[listItemIndex]; component.getDefaultFoundation().adapter_.setTabIndexForListItemChildren(listItemIndex, 0); - assert.equal(1, root.querySelectorAll('button[tabIndex="0"]').length); - assert.equal(listItem, root.querySelectorAll('button[tabIndex="0"]').parentElement); + + assert.equal(1, root.querySelectorAll('button[tabindex="0"]').length); + assert.equal(listItem, root.querySelectorAll('button[tabindex="0"]')[0].parentElement); document.body.removeChild(root); }); @@ -115,6 +241,40 @@ test('wrapFocus calls setWrapFocus on foundation', () => { td.verify(mockFoundation.setWrapFocus(true), {times: 1}); }); +test('singleSelection true sets the selectedIndex if a list item has the --selected class', () => { + const {root, component, mockFoundation} = setupTest(); + root.querySelector('.mdc-list-item').classList.add(MDCListFoundation.cssClasses.LIST_ITEM_SELECTED_CLASS); + component.singleSelection = true; + td.verify(mockFoundation.setSelectedIndex(0), {times: 1}); +}); + +test('singleSelection true sets the click handler from the root element', () => { + const {root, component, mockFoundation} = setupTest(); + component.singleSelection = true; + domEvents.emit(root, 'click'); + td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 1}); +}); + +test('singleSelection false removes the click handler from the root element', () => { + const {root, component, mockFoundation} = setupTest(); + component.singleSelection = true; + component.singleSelection = false; + domEvents.emit(root, 'click'); + td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 0}); +}); + +test('singleSelection calls foundation setSingleSelection with the provided value', () => { + const {component, mockFoundation} = setupTest(); + component.singleSelection = true; + td.verify(mockFoundation.setSingleSelection(true), {times: 1}); +}); + +test('selectedIndex calls setSelectedIndex on foundation', () => { + const {component, mockFoundation} = setupTest(); + component.selectedIndex = 1; + td.verify(mockFoundation.setSelectedIndex(1), {times: 1}); +}); + test('keydown handler is added to root element', () => { const {root, mockFoundation} = setupTest(); const event = document.createEvent('KeyboardEvent'); From fbbf58aefbf3214cd71863817cab8d1120372e3c Mon Sep 17 00:00:00 2001 From: Patty RoDee Date: Mon, 16 Jul 2018 16:01:26 -0700 Subject: [PATCH 09/53] fix(infrastructure): Rework goog.module positioning (#3098) --- scripts/rewrite-decl-statements-for-closure-test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/rewrite-decl-statements-for-closure-test.js b/scripts/rewrite-decl-statements-for-closure-test.js index 897b6f16bb1..898df912443 100644 --- a/scripts/rewrite-decl-statements-for-closure-test.js +++ b/scripts/rewrite-decl-statements-for-closure-test.js @@ -211,7 +211,12 @@ function transform(srcFile, rootDir) { } // Specify goog.module after the @license comment and append newline at the end of the file. - const pos = outputCode.indexOf(' */') + 3; + // First, get the first occurence of a multiline comment terminator with 0 or more preceding whitespace characters. + const result = /\s*\*\//.exec(outputCode); + // Then, get the index of that first matching character set plus the length of the matching characters, plus one + // extra character for more space. We now have the position at which we need to inject the "goog.module(...)" + // declaration and can assemble the module-declared code. Yay! + const pos = result.index + result[0].length + 1; outputCode = outputCode.substr(0, pos) + '\ngoog.module(\'' + packageStr + '\');\n' + outputCode.substr(pos); fs.writeFileSync(srcFile, outputCode, 'utf8'); logProgress(`[rewrite] ${srcFile}`); From 0000d5f99c84ae695acf9f692b1ecf0fd7ff6433 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 16:25:18 -0700 Subject: [PATCH 10/53] chore(infrastructure): Display snapshot diff base in CBT UI (#3097) ### What it does - Sets the "test name" and "build name" fields of CBT Selenium requests - Includes the commit author's email address, commit hash, branch name, and PR # - Makes it easier to figure out who triggered a CBT run ### Example output #### Before: ![image](https://user-images.githubusercontent.com/409245/42786252-50e40a2a-890a-11e8-95a7-d1525fe21fa2.png) #### After: ![image](https://user-images.githubusercontent.com/409245/42787800-08068470-8911-11e8-908c-52aee10cb2fa.png) --- test/screenshot/lib/cbt-api.js | 57 ++++++++++++++++++++++++++-- test/screenshot/lib/cli.js | 34 +++++++++++++---- test/screenshot/lib/git-repo.js | 17 +++++++++ test/screenshot/lib/github-api.js | 14 +++---- test/screenshot/lib/golden-io.js | 2 +- test/screenshot/lib/report-writer.js | 2 +- test/screenshot/proto/mdc.pb.js | 54 ++++++++++++++++++++++++++ test/screenshot/proto/mdc.proto | 6 +++ 8 files changed, 165 insertions(+), 21 deletions(-) diff --git a/test/screenshot/lib/cbt-api.js b/test/screenshot/lib/cbt-api.js index f6bb15c2d36..271c646e356 100644 --- a/test/screenshot/lib/cbt-api.js +++ b/test/screenshot/lib/cbt-api.js @@ -25,6 +25,8 @@ const {FormFactorType, OsVendorType, BrowserVendorType, BrowserVersionType} = Us const {CbtAccount, CbtActiveTestCounts, CbtConcurrencyStats} = cbtProto; const {RawCapabilities} = seleniumProto; +const Cli = require('./cli'); + const MDC_CBT_USERNAME = process.env.MDC_CBT_USERNAME; const MDC_CBT_AUTHKEY = process.env.MDC_CBT_AUTHKEY; const REST_API_BASE_URL = 'https://crossbrowsertesting.com/api/v3'; @@ -36,6 +38,12 @@ let allBrowsersPromise; class CbtApi { constructor() { + /** + * @type {!Cli} + * @private + */ + this.cli_ = new Cli(); + this.validateEnvVars_(); } @@ -125,12 +133,12 @@ https://crossbrowsertesting.com/account /** @type {{device: !cbt.proto.CbtDevice, browser: !cbt.proto.CbtBrowser}} */ const matchingCbtUserAgent = await this.getMatchingCbtUserAgent_(userAgent); + const {cbtBuildName, cbtTestName} = await this.getCbtTestNameAndBuildNameForReport_(meta); + /** @type {!selenium.proto.RawCapabilities} */ const defaultCaps = { - // TODO(acdvorak): Implement - name: undefined, - // TODO(acdvorak): Figure out why this value is an empty string - build: meta.snapshot_diff_base.input_string, + name: `${cbtTestName} - `, + build: cbtBuildName, // TODO(acdvorak): Expose these as CLI flags record_video: true, @@ -287,6 +295,47 @@ https://crossbrowsertesting.com/account return matchingCbtUserAgents[0]; } + /** + * @param {!mdc.proto.ReportMeta} meta + * @return {{cbtBuildName: string, cbtTestName: string}} + * @private + */ + async getCbtTestNameAndBuildNameForReport_(meta) { + /** @type {?mdc.proto.GitRevision} */ + const travisGitRev = await this.cli_.getTravisGitRevision(); + if (travisGitRev) { + return this.getCbtTestNameAndBuildNameForGitRev_(travisGitRev); + } + + const snapshotDiffBase = meta.snapshot_diff_base; + const snapshotGitRev = snapshotDiffBase.git_revision; + if (snapshotGitRev) { + return this.getCbtTestNameAndBuildNameForGitRev_(snapshotGitRev); + } + + const serialized = JSON.stringify(meta, null, 2); + throw new Error(`Unable to generate CBT test/build name for metadata:\n${serialized}`); + } + + /** + * @param {!mdc.proto.GitRevision} gitRev + * @return {{cbtBuildName: string, cbtTestName: string}} + * @private + */ + getCbtTestNameAndBuildNameForGitRev_(gitRev) { + const nameParts = [ + gitRev.author.email, + gitRev.commit ? gitRev.commit.substr(0, 7) : null, + gitRev.branch ? gitRev.branch : null, + gitRev.tag ? gitRev.tag : null, + gitRev.pr_number ? `PR #${gitRev.pr_number}` : null, + ].filter((part) => part); + return { + cbtTestName: nameParts.slice(0, -1).join(' - '), + cbtBuildName: nameParts.slice(-1)[0], + }; + } + /** * @param {string} method * @param {string} endpoint diff --git a/test/screenshot/lib/cli.js b/test/screenshot/lib/cli.js index d5e2a316954..a7ac4ea58d7 100644 --- a/test/screenshot/lib/cli.js +++ b/test/screenshot/lib/cli.js @@ -483,7 +483,7 @@ that you know are going to have diffs. */ async parseGoldenDiffBase() { /** @type {?mdc.proto.GitRevision} */ - const travisGitRevision = await this.getTravisGitRevision_(); + const travisGitRevision = await this.getTravisGitRevision(); if (travisGitRevision) { return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, @@ -568,9 +568,8 @@ that you know are going to have diffs. /** * @return {?Promise} - * @private */ - async getTravisGitRevision_() { + async getTravisGitRevision() { const travisBranch = process.env.TRAVIS_BRANCH; const travisTag = process.env.TRAVIS_TAG; const travisPrNumber = Number(process.env.TRAVIS_PULL_REQUEST); @@ -578,29 +577,38 @@ that you know are going to have diffs. const travisPrSha = process.env.TRAVIS_PULL_REQUEST_SHA; if (travisPrNumber) { + const commit = await this.gitRepo_.getFullCommitHash(travisPrSha); + const author = await this.gitRepo_.getCommitAuthor(commit); return GitRevision.create({ type: GitRevision.Type.TRAVIS_PR, golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, - commit: await this.gitRepo_.getFullCommitHash(travisPrSha), + commit, + author, branch: travisPrBranch || travisBranch, pr_number: travisPrNumber, }); } if (travisTag) { + const commit = await this.gitRepo_.getFullCommitHash(travisTag); + const author = await this.gitRepo_.getCommitAuthor(commit); return GitRevision.create({ type: GitRevision.Type.REMOTE_TAG, golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, - commit: await this.gitRepo_.getFullCommitHash(travisTag), + commit, + author, tag: travisTag, }); } if (travisBranch) { + const commit = await this.gitRepo_.getFullCommitHash(travisBranch); + const author = await this.gitRepo_.getCommitAuthor(commit); return GitRevision.create({ type: GitRevision.Type.LOCAL_BRANCH, golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, - commit: await this.gitRepo_.getFullCommitHash(travisBranch), + commit, + author, branch: travisBranch, }); } @@ -641,14 +649,17 @@ that you know are going to have diffs. * @return {!mdc.proto.DiffBase} * @private */ - createCommitDiffBase_(commit, goldenJsonFilePath) { + async createCommitDiffBase_(commit, goldenJsonFilePath) { + const author = await this.gitRepo_.getCommitAuthor(commit); + return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, git_revision: GitRevision.create({ type: GitRevision.Type.COMMIT, input_string: `${commit}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, - commit: commit, + commit, + author, }), }); } @@ -664,6 +675,7 @@ that you know are going to have diffs. const remote = allRemoteNames.find((curRemoteName) => remoteRef.startsWith(curRemoteName + '/')); const branch = remoteRef.substr(remote.length + 1); // add 1 for forward-slash separator const commit = await this.gitRepo_.getFullCommitHash(remoteRef); + const author = await this.gitRepo_.getCommitAuthor(commit); return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, @@ -672,6 +684,7 @@ that you know are going to have diffs. input_string: `${remoteRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, + author, remote, branch, }), @@ -686,6 +699,7 @@ that you know are going to have diffs. */ async createRemoteTagDiffBase_(tagRef, goldenJsonFilePath) { const commit = await this.gitRepo_.getFullCommitHash(tagRef); + const author = await this.gitRepo_.getCommitAuthor(commit); return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, @@ -694,6 +708,7 @@ that you know are going to have diffs. input_string: `${tagRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, + author, remote: 'origin', tag: tagRef, }), @@ -708,6 +723,8 @@ that you know are going to have diffs. */ async createLocalBranchDiffBase_(branch, goldenJsonFilePath) { const commit = await this.gitRepo_.getFullCommitHash(branch); + const author = await this.gitRepo_.getCommitAuthor(commit); + return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, git_revision: GitRevision.create({ @@ -715,6 +732,7 @@ that you know are going to have diffs. input_string: `${branch}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, + author, branch, }), }); diff --git a/test/screenshot/lib/git-repo.js b/test/screenshot/lib/git-repo.js index 072331e9981..8fe97882f25 100644 --- a/test/screenshot/lib/git-repo.js +++ b/test/screenshot/lib/git-repo.js @@ -18,6 +18,9 @@ const simpleGit = require('simple-git/promise'); +const mdcProto = require('../proto/mdc.pb').mdc.proto; +const {User} = mdcProto; + class GitRepo { constructor(workingDirPath = undefined) { /** @@ -130,6 +133,20 @@ class GitRepo { return this.repo_.checkIgnore(filePaths); } + /** + * @param {string=} commit + * @return {!Promise} + */ + async getCommitAuthor(commit = undefined) { + /** @type {!Array} */ + const logEntries = await this.getLog([commit]); + const logEntry = logEntries[0]; + return User.create({ + name: logEntry.author_name, + email: logEntry.author_email, + }); + } + /** * @param {string} cmd * @param {!Array=} argList diff --git a/test/screenshot/lib/github-api.js b/test/screenshot/lib/github-api.js index 33567af491c..d623b736ef4 100644 --- a/test/screenshot/lib/github-api.js +++ b/test/screenshot/lib/github-api.js @@ -14,13 +14,13 @@ * limitations under the License. */ -const octocat = require('@octokit/rest'); +const octokit = require('@octokit/rest'); const GitRepo = require('./git-repo'); class GitHubApi { constructor() { this.gitRepo_ = new GitRepo(); - this.octocat_ = octocat(); + this.octokit_ = octokit(); this.authenticate_(); } @@ -37,7 +37,7 @@ class GitHubApi { return; } - this.octocat_.authenticate({ + this.octokit_.authenticate({ type: 'oauth', token: token, }); @@ -58,7 +58,7 @@ class GitHubApi { /** * @param {!mdc.proto.ReportData} reportData - * @return {Promise} + * @return {!Promise<*>} */ async setPullRequestStatus(reportData) { const meta = reportData.meta; @@ -119,7 +119,7 @@ class GitHubApi { * @private */ async createStatus_({state, targetUrl, description = undefined}) { - return await this.octocat_.repos.createStatus({ + return await this.octokit_.repos.createStatus({ owner: 'material-components', repo: 'material-components-web', sha: await this.gitRepo_.getFullCommitHash(process.env.TRAVIS_PULL_REQUEST_SHA), @@ -137,7 +137,7 @@ class GitHubApi { async getPullRequestNumber(branch = undefined) { branch = branch || await this.gitRepo_.getBranchName(); - const allPRs = await this.octocat_.pullRequests.getAll({ + const allPRs = await this.octokit_.pullRequests.getAll({ owner: 'material-components', repo: 'material-components-web', per_page: 100, @@ -155,7 +155,7 @@ class GitHubApi { */ async getPullRequestFiles(prNumber) { /** @type {!github.proto.PullRequestFileResponse} */ - const fileResponse = await this.octocat_.pullRequests.getFiles({ + const fileResponse = await this.octokit_.pullRequests.getFiles({ owner: 'material-components', repo: 'material-components-web', number: prNumber, diff --git a/test/screenshot/lib/golden-io.js b/test/screenshot/lib/golden-io.js index 0dc8dfe371e..2c2a4a2dec0 100644 --- a/test/screenshot/lib/golden-io.js +++ b/test/screenshot/lib/golden-io.js @@ -98,7 +98,7 @@ class GoldenIo { const serialized = JSON.stringify({parsedDiffBase, meta}, null, 2); throw new Error( - `Unable to parse '--diff-base=${rawDiffBase}': Expected a URL, local file path, or git ref. ${serialized}` + `Unable to parse '--diff-base=${rawDiffBase}': Expected a URL, local file path, or git ref.\n${serialized}` ); } diff --git a/test/screenshot/lib/report-writer.js b/test/screenshot/lib/report-writer.js index 19f38abf406..378d9ace7df 100644 --- a/test/screenshot/lib/report-writer.js +++ b/test/screenshot/lib/report-writer.js @@ -460,7 +460,7 @@ ${prMarkup} } const serialized = JSON.stringify({diffBase, meta}, null, 2); - throw new Error(`Unable to generate markup for invalid diff source: ${serialized}`); + throw new Error(`Unable to generate markup for invalid diff source:\n${serialized}`); } /** diff --git a/test/screenshot/proto/mdc.pb.js b/test/screenshot/proto/mdc.pb.js index ef8e8605762..1f1e2cffbaa 100644 --- a/test/screenshot/proto/mdc.pb.js +++ b/test/screenshot/proto/mdc.pb.js @@ -1427,6 +1427,8 @@ $root.mdc = (function() { * @property {string|null} [tag] GitRevision tag * @property {number|null} [pr_number] GitRevision pr_number * @property {Array.|null} [pr_file_paths] GitRevision pr_file_paths + * @property {mdc.proto.IUser|null} [author] GitRevision author + * @property {mdc.proto.IUser|null} [committer] GitRevision committer */ /** @@ -1509,6 +1511,22 @@ $root.mdc = (function() { */ GitRevision.prototype.pr_file_paths = $util.emptyArray; + /** + * GitRevision author. + * @member {mdc.proto.IUser|null|undefined} author + * @memberof mdc.proto.GitRevision + * @instance + */ + GitRevision.prototype.author = null; + + /** + * GitRevision committer. + * @member {mdc.proto.IUser|null|undefined} committer + * @memberof mdc.proto.GitRevision + * @instance + */ + GitRevision.prototype.committer = null; + /** * Creates a new GitRevision instance using the specified properties. * @function create @@ -1550,6 +1568,10 @@ $root.mdc = (function() { if (message.pr_file_paths != null && message.pr_file_paths.length) for (var i = 0; i < message.pr_file_paths.length; ++i) writer.uint32(/* id 8, wireType 2 =*/66).string(message.pr_file_paths[i]); + if (message.author != null && message.hasOwnProperty("author")) + $root.mdc.proto.User.encode(message.author, writer.uint32(/* id 9, wireType 2 =*/74).fork()).ldelim(); + if (message.committer != null && message.hasOwnProperty("committer")) + $root.mdc.proto.User.encode(message.committer, writer.uint32(/* id 10, wireType 2 =*/82).fork()).ldelim(); return writer; }; @@ -1610,6 +1632,12 @@ $root.mdc = (function() { message.pr_file_paths = []; message.pr_file_paths.push(reader.string()); break; + case 9: + message.author = $root.mdc.proto.User.decode(reader, reader.uint32()); + break; + case 10: + message.committer = $root.mdc.proto.User.decode(reader, reader.uint32()); + break; default: reader.skipType(tag & 7); break; @@ -1682,6 +1710,16 @@ $root.mdc = (function() { if (!$util.isString(message.pr_file_paths[i])) return "pr_file_paths: string[] expected"; } + if (message.author != null && message.hasOwnProperty("author")) { + var error = $root.mdc.proto.User.verify(message.author); + if (error) + return "author." + error; + } + if (message.committer != null && message.hasOwnProperty("committer")) { + var error = $root.mdc.proto.User.verify(message.committer); + if (error) + return "committer." + error; + } return null; }; @@ -1742,6 +1780,16 @@ $root.mdc = (function() { for (var i = 0; i < object.pr_file_paths.length; ++i) message.pr_file_paths[i] = String(object.pr_file_paths[i]); } + if (object.author != null) { + if (typeof object.author !== "object") + throw TypeError(".mdc.proto.GitRevision.author: object expected"); + message.author = $root.mdc.proto.User.fromObject(object.author); + } + if (object.committer != null) { + if (typeof object.committer !== "object") + throw TypeError(".mdc.proto.GitRevision.committer: object expected"); + message.committer = $root.mdc.proto.User.fromObject(object.committer); + } return message; }; @@ -1768,6 +1816,8 @@ $root.mdc = (function() { object.branch = ""; object.tag = ""; object.pr_number = 0; + object.author = null; + object.committer = null; } if (message.type != null && message.hasOwnProperty("type")) object.type = options.enums === String ? $root.mdc.proto.GitRevision.Type[message.type] : message.type; @@ -1788,6 +1838,10 @@ $root.mdc = (function() { for (var j = 0; j < message.pr_file_paths.length; ++j) object.pr_file_paths[j] = message.pr_file_paths[j]; } + if (message.author != null && message.hasOwnProperty("author")) + object.author = $root.mdc.proto.User.toObject(message.author, options); + if (message.committer != null && message.hasOwnProperty("committer")) + object.committer = $root.mdc.proto.User.toObject(message.committer, options); return object; }; diff --git a/test/screenshot/proto/mdc.proto b/test/screenshot/proto/mdc.proto index be35736f21a..b06d502ba3c 100644 --- a/test/screenshot/proto/mdc.proto +++ b/test/screenshot/proto/mdc.proto @@ -93,6 +93,12 @@ message GitRevision { string tag = 6; uint32 pr_number = 7; repeated string pr_file_paths = 8; + + // "author" is the person who created the PR; "committer" is the person who created the commit. + // E.g., if Alice creates a PR and Bob clicks "Update branch" in the GitHub UI, then + // author = "Alice" and committer = "Bob". + User author = 9; + User committer = 10; } message User { From 52f0bc5847b77ea3497791fb13a825027506a0d2 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 17:26:47 -0700 Subject: [PATCH 11/53] chore(infrastructure): Skip screenshot tests on external PRs (#3100) Avoids spurious Travis test failures. External PRs are a security risk because they contain untrusted code. Therefore, Travis does not pass env vars to them, which means we don't have access to CBT/GCS credentials. We might as well check for this condition and exit early instead of waiting 2 minutes to throw an error. --- test/screenshot/commands/test.js | 11 ++++++++++- test/screenshot/commands/travis.sh | 13 +++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/test/screenshot/commands/test.js b/test/screenshot/commands/test.js index 71f26c56777..d8c76d83c59 100644 --- a/test/screenshot/commands/test.js +++ b/test/screenshot/commands/test.js @@ -19,9 +19,17 @@ const BuildCommand = require('./build'); const Controller = require('../lib/controller'); const GitHubApi = require('../lib/github-api'); +const {ExitCode} = require('../lib/constants'); module.exports = { async runAsync() { + const travisPrSlug = process.env.TRAVIS_PULL_REQUEST_SLUG; + if (travisPrSlug && !travisPrSlug.startsWith('material-components/')) { + console.log('Screenshot tests are not supported on external PRs.'); + console.log('Skipping screenshot tests.'); + return ExitCode.OK; + } + await BuildCommand.runAsync(); const controller = new Controller(); const gitHubApi = new GitHubApi(); @@ -31,7 +39,8 @@ module.exports = { const {isTestable, prNumber} = controller.checkIsTestable(reportData); if (!isTestable) { - console.log(`PR #${prNumber} does not contain any testable source file changes.\nSkipping screenshot tests.`); + console.log(`PR #${prNumber} does not contain any testable source file changes.`); + console.log('Skipping screenshot tests.'); return; } diff --git a/test/screenshot/commands/travis.sh b/test/screenshot/commands/travis.sh index cd49eee9df6..5163dcb5105 100755 --- a/test/screenshot/commands/travis.sh +++ b/test/screenshot/commands/travis.sh @@ -1,5 +1,13 @@ #!/usr/bin/env bash +function exit_if_external_pr() { + if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then + echo 'Screenshot tests are not supported on external PRs.' + echo 'Skipping screenshot tests.' + exit + fi +} + function extract_api_credentials() { openssl aes-256-cbc -K $encrypted_eead2343bb54_key -iv $encrypted_eead2343bb54_iv \ -in test/screenshot/auth/travis.tar.enc -out test/screenshot/auth/travis.tar -d @@ -16,7 +24,7 @@ function extract_api_credentials() { } function install_google_cloud_sdk() { - if [ ! -d $HOME/google-cloud-sdk ]; then + if [[ ! -d $HOME/google-cloud-sdk ]]; then curl -o /tmp/gcp-sdk.bash https://sdk.cloud.google.com chmod +x /tmp/gcp-sdk.bash /tmp/gcp-sdk.bash --disable-prompts @@ -30,13 +38,14 @@ function install_google_cloud_sdk() { gcloud components install gsutil which gsutil 2>&1 > /dev/null - if [ $? != 0 ]; then + if [[ $? != 0 ]]; then pip install --upgrade pip pip install gsutil fi } if [ "$TEST_SUITE" == 'screenshot' ]; then + exit_if_external_pr extract_api_credentials install_google_cloud_sdk fi From d7f677201bb2981efcdba625c0c1c9438da57f41 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 16 Jul 2018 17:52:36 -0700 Subject: [PATCH 12/53] =?UTF-8?q?Update=20css-loader=20to=20the=20latest?= =?UTF-8?q?=20version=20=F0=9F=9A=80=20(#3037)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1176 +++++---------------------------------------- package.json | 2 +- 2 files changed, 130 insertions(+), 1048 deletions(-) diff --git a/package-lock.json b/package-lock.json index e607b72b5e0..88546a3ebfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -569,12 +569,6 @@ "repeat-string": "^1.5.2" } }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -2264,36 +2258,6 @@ "map-obj": "^1.0.0" } }, - "caniuse-api": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", - "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", - "dev": true, - "requires": { - "browserslist": "^1.3.6", - "caniuse-db": "^1.0.30000529", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "dev": true, - "requires": { - "caniuse-db": "^1.0.30000639", - "electron-to-chromium": "^1.2.7" - } - } - } - }, - "caniuse-db": { - "version": "1.0.30000676", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000676.tgz", - "integrity": "sha1-gupXgjdjfI/zSiisqt43O2JMTqg=", - "dev": true - }, "canvas-prebuilt": { "version": "1.6.5-prerelease.1", "resolved": "https://registry.npmjs.org/canvas-prebuilt/-/canvas-prebuilt-1.6.5-prerelease.1.tgz", @@ -2437,15 +2401,6 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, - "clap": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.1.3.tgz", - "integrity": "sha1-s7026T3Uy/s5WjwmiWNSRFJlwFs=", - "dev": true, - "requires": { - "chalk": "^1.1.3" - } - }, "clean-stack": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", @@ -2640,15 +2595,6 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, - "coa": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.2.tgz", - "integrity": "sha1-K6n+w7SqQ9eknX5sNWHpIGG2vOw=", - "dev": true, - "requires": { - "q": "^1.1.2" - } - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2745,17 +2691,6 @@ "integrity": "sha1-S5BvZw5aljqHt2sOFolkM0G2Ajw=", "dev": true }, - "color": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", - "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", - "dev": true, - "requires": { - "clone": "^1.0.2", - "color-convert": "^1.3.0", - "color-string": "^0.3.0" - } - }, "color-convert": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", @@ -2771,26 +2706,6 @@ "integrity": "sha1-XIq3K2S9IhXWF66VWeuxSEdc+Y0=", "dev": true }, - "color-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", - "dev": true, - "requires": { - "color-name": "^1.0.0" - } - }, - "colormin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", - "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", - "dev": true, - "requires": { - "color": "^0.11.0", - "css-color-names": "0.0.4", - "has": "^1.0.1" - } - }, "colors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", @@ -4397,12 +4312,6 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, "css-font-size-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", @@ -4443,37 +4352,94 @@ } }, "css-loader": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.4.tgz", - "integrity": "sha1-bPNXkZLONV6LONX0Ldeh8uyJjQ8=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz", + "integrity": "sha512-tMXlTYf3mIMt3b0dDCOQFJiVvxbocJ5Ho577WiGPYPZcqVEO218L2iU22pDXzkTZCLDE+9AmGSUkWxeh/nZReA==", "dev": true, "requires": { - "babel-code-frame": "^6.11.0", + "babel-code-frame": "^6.26.0", "css-selector-tokenizer": "^0.7.0", - "cssnano": ">=2.6.1 <4", "icss-utils": "^2.1.0", "loader-utils": "^1.0.2", "lodash.camelcase": "^4.3.0", - "object-assign": "^4.0.1", - "postcss": "^5.0.6", - "postcss-modules-extract-imports": "^1.0.0", - "postcss-modules-local-by-default": "^1.0.1", - "postcss-modules-scope": "^1.0.0", - "postcss-modules-values": "^1.1.0", + "postcss": "^6.0.23", + "postcss-modules-extract-imports": "^1.2.0", + "postcss-modules-local-by-default": "^1.2.0", + "postcss-modules-scope": "^1.1.0", + "postcss-modules-values": "^1.3.0", "postcss-value-parser": "^3.3.0", - "source-list-map": "^0.1.7" + "source-list-map": "^2.0.0" }, "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "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 + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" } } } @@ -4514,94 +4480,6 @@ "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", "dev": true }, - "cssnano": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", - "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", - "dev": true, - "requires": { - "autoprefixer": "^6.3.1", - "decamelize": "^1.1.2", - "defined": "^1.0.0", - "has": "^1.0.1", - "object-assign": "^4.0.1", - "postcss": "^5.0.14", - "postcss-calc": "^5.2.0", - "postcss-colormin": "^2.1.8", - "postcss-convert-values": "^2.3.4", - "postcss-discard-comments": "^2.0.4", - "postcss-discard-duplicates": "^2.0.1", - "postcss-discard-empty": "^2.0.1", - "postcss-discard-overridden": "^0.1.1", - "postcss-discard-unused": "^2.2.1", - "postcss-filter-plugins": "^2.0.0", - "postcss-merge-idents": "^2.1.5", - "postcss-merge-longhand": "^2.0.1", - "postcss-merge-rules": "^2.0.3", - "postcss-minify-font-values": "^1.0.2", - "postcss-minify-gradients": "^1.0.1", - "postcss-minify-params": "^1.0.4", - "postcss-minify-selectors": "^2.0.4", - "postcss-normalize-charset": "^1.1.0", - "postcss-normalize-url": "^3.0.7", - "postcss-ordered-values": "^2.1.0", - "postcss-reduce-idents": "^2.2.2", - "postcss-reduce-initial": "^1.0.0", - "postcss-reduce-transforms": "^1.0.3", - "postcss-svgo": "^2.1.1", - "postcss-unique-selectors": "^2.0.2", - "postcss-value-parser": "^3.2.3", - "postcss-zindex": "^2.0.1" - }, - "dependencies": { - "autoprefixer": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", - "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", - "dev": true, - "requires": { - "browserslist": "^1.7.6", - "caniuse-db": "^1.0.30000634", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^5.2.16", - "postcss-value-parser": "^3.2.3" - } - }, - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "dev": true, - "requires": { - "caniuse-db": "^1.0.30000639", - "electron-to-chromium": "^1.2.7" - } - }, - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "csso": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", - "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", - "dev": true, - "requires": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - } - }, "cssom": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", @@ -5210,12 +5088,6 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, - "electron-to-chromium": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.13.tgz", - "integrity": "sha1-GzperObgh7teJXoQCwy/6Bsokfw=", - "dev": true - }, "elliptic": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", @@ -6277,12 +6149,6 @@ } } }, - "flatten": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", - "dev": true - }, "follow-redirects": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.0.0.tgz", @@ -8618,12 +8484,6 @@ "wbuf": "^1.1.0" } }, - "html-comment-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", - "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", - "dev": true - }, "html-entities": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", @@ -9127,12 +8987,6 @@ "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", "dev": true }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, "is-alphabetical": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.1.tgz", @@ -9486,15 +9340,6 @@ "integrity": "sha1-i1IMhfrnolM4LUsCZS4EVXbhO7g=", "dev": true }, - "is-svg": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", - "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", - "dev": true, - "requires": { - "html-comment-regex": "^1.1.0" - } - }, "is-symbol": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", @@ -10804,12 +10649,6 @@ "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", "dev": true }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, "lodash.mergewith": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", @@ -10841,12 +10680,6 @@ "lodash._reinterpolate": "~3.0.0" } }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -11109,12 +10942,6 @@ "yallist": "^2.0.0" } }, - "macaddress": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", - "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=", - "dev": true - }, "macos-release": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-1.1.0.tgz", @@ -11219,12 +11046,6 @@ "integrity": "sha1-Sz3ToTPRUYuO8NvHCb8qG0gkvIw=", "dev": true }, - "math-expression-evaluator": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", - "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", - "dev": true - }, "mathml-tag-names": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz", @@ -12059,18 +11880,6 @@ "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", "dev": true }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, "npm-run-all": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.1.tgz", @@ -12888,46 +12697,30 @@ } } }, - "postcss-calc": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", - "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "postcss-html": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.12.0.tgz", + "integrity": "sha512-KxKUpj7AY7nlCbLcTOYxdfJnGE7QFAfU2n95ADj1Q90RM/pOLdz8k3n4avOyRFs7MDQHcRzJQWM1dehCwJxisQ==", "dev": true, "requires": { - "postcss": "^5.0.2", - "postcss-message-helpers": "^2.0.0", - "reduce-css-calc": "^1.2.6" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } + "htmlparser2": "^3.9.2", + "remark": "^8.0.0", + "unist-util-find-all-after": "^1.0.1" } }, - "postcss-colormin": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", - "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "postcss-less": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.3.tgz", + "integrity": "sha512-WS0wsQxRm+kmN8wEYAGZ3t4lnoNfoyx9EJZrhiPR1K0lMHR0UNWnz52Ya5QRXChHtY75Ef+kDc05FpnBujebgw==", "dev": true, "requires": { - "colormin": "^1.0.5", - "postcss": "^5.0.13", - "postcss-value-parser": "^3.2.3" + "postcss": "^5.2.16" }, "dependencies": { "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", "dev": true, "requires": { "chalk": "^1.1.3", @@ -12938,450 +12731,60 @@ } } }, - "postcss-convert-values": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", - "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", "dev": true, "requires": { - "postcss": "^5.0.11", - "postcss-value-parser": "^3.1.2" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } + "cosmiconfig": "^2.1.0", + "object-assign": "^4.1.0", + "postcss-load-options": "^1.2.0", + "postcss-load-plugins": "^2.3.0" } }, - "postcss-discard-comments": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", - "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", "dev": true, "requires": { - "postcss": "^5.0.14" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } + "cosmiconfig": "^2.1.0", + "object-assign": "^4.1.0" } }, - "postcss-discard-duplicates": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", - "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", - "dev": true, - "requires": { - "postcss": "^5.0.4" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-discard-empty": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", - "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", - "dev": true, - "requires": { - "postcss": "^5.0.14" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-discard-overridden": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", - "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", - "dev": true, - "requires": { - "postcss": "^5.0.16" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-discard-unused": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", - "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", - "dev": true, - "requires": { - "postcss": "^5.0.14", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-filter-plugins": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", - "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", - "dev": true, - "requires": { - "postcss": "^5.0.4", - "uniqid": "^4.0.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-html": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.12.0.tgz", - "integrity": "sha512-KxKUpj7AY7nlCbLcTOYxdfJnGE7QFAfU2n95ADj1Q90RM/pOLdz8k3n4avOyRFs7MDQHcRzJQWM1dehCwJxisQ==", - "dev": true, - "requires": { - "htmlparser2": "^3.9.2", - "remark": "^8.0.0", - "unist-util-find-all-after": "^1.0.1" - } - }, - "postcss-less": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.3.tgz", - "integrity": "sha512-WS0wsQxRm+kmN8wEYAGZ3t4lnoNfoyx9EJZrhiPR1K0lMHR0UNWnz52Ya5QRXChHtY75Ef+kDc05FpnBujebgw==", - "dev": true, - "requires": { - "postcss": "^5.2.16" - }, - "dependencies": { - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-load-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", - "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", - "dev": true, - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0", - "postcss-load-options": "^1.2.0", - "postcss-load-plugins": "^2.3.0" - } - }, - "postcss-load-options": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", - "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", - "dev": true, - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0" - } - }, - "postcss-load-plugins": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", - "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", "dev": true, "requires": { "cosmiconfig": "^2.1.1", "object-assign": "^4.1.0" } }, - "postcss-loader": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.5.tgz", - "integrity": "sha1-wZ0+i4PrGsMW9WIe9MDvWz0bizo=", - "dev": true, - "requires": { - "loader-utils": "^1.x", - "postcss": "^6.x", - "postcss-load-config": "^1.x", - "schema-utils": "^0.x" - } - }, - "postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", - "dev": true - }, - "postcss-merge-idents": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", - "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", - "dev": true, - "requires": { - "has": "^1.0.1", - "postcss": "^5.0.10", - "postcss-value-parser": "^3.1.1" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-merge-longhand": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", - "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", - "dev": true, - "requires": { - "postcss": "^5.0.4" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-merge-rules": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", - "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", - "dev": true, - "requires": { - "browserslist": "^1.5.2", - "caniuse-api": "^1.5.2", - "postcss": "^5.0.4", - "postcss-selector-parser": "^2.2.2", - "vendors": "^1.0.0" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "dev": true, - "requires": { - "caniuse-db": "^1.0.30000639", - "electron-to-chromium": "^1.2.7" - } - }, - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-message-helpers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", - "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", - "dev": true - }, - "postcss-minify-font-values": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", - "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "postcss": "^5.0.4", - "postcss-value-parser": "^3.0.2" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-minify-gradients": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", - "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", - "dev": true, - "requires": { - "postcss": "^5.0.12", - "postcss-value-parser": "^3.3.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-minify-params": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", - "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.1", - "postcss": "^5.0.2", - "postcss-value-parser": "^3.0.2", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-minify-selectors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", - "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "postcss-loader": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.5.tgz", + "integrity": "sha1-wZ0+i4PrGsMW9WIe9MDvWz0bizo=", "dev": true, "requires": { - "alphanum-sort": "^1.0.2", - "has": "^1.0.1", - "postcss": "^5.0.14", - "postcss-selector-parser": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } + "loader-utils": "^1.x", + "postcss": "^6.x", + "postcss-load-config": "^1.x", + "schema-utils": "^0.x" } }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, "postcss-modules-extract-imports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", - "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz", + "integrity": "sha1-ZhQOzs447wa/DT41XWm/WdFB6oU=", "dev": true, "requires": { "postcss": "^6.0.1" @@ -13417,151 +12820,6 @@ "postcss": "^6.0.1" } }, - "postcss-normalize-charset": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", - "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", - "dev": true, - "requires": { - "postcss": "^5.0.5" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-normalize-url": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", - "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^1.4.0", - "postcss": "^5.0.14", - "postcss-value-parser": "^3.2.3" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-ordered-values": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", - "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", - "dev": true, - "requires": { - "postcss": "^5.0.4", - "postcss-value-parser": "^3.0.1" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-reduce-idents": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", - "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", - "dev": true, - "requires": { - "postcss": "^5.0.4", - "postcss-value-parser": "^3.0.2" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-reduce-initial": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", - "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", - "dev": true, - "requires": { - "postcss": "^5.0.4" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-reduce-transforms": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", - "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", - "dev": true, - "requires": { - "has": "^1.0.1", - "postcss": "^5.0.8", - "postcss-value-parser": "^3.0.1" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, "postcss-reporter": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-5.0.0.tgz", @@ -13824,17 +13082,6 @@ } } }, - "postcss-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", - "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", - "dev": true, - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, "postcss-sorting": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-3.1.0.tgz", @@ -13910,88 +13157,12 @@ } } }, - "postcss-svgo": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", - "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", - "dev": true, - "requires": { - "is-svg": "^2.0.0", - "postcss": "^5.0.14", - "postcss-value-parser": "^3.2.3", - "svgo": "^0.7.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, - "postcss-unique-selectors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", - "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.1", - "postcss": "^5.0.4", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, "postcss-value-parser": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", "dev": true }, - "postcss-zindex": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", - "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", - "dev": true, - "requires": { - "has": "^1.0.1", - "postcss": "^5.0.4", - "uniqs": "^2.0.0" - }, - "dependencies": { - "postcss": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", - "integrity": "sha1-z09Ze4ZNZcikkrLqvp1wbIecOIs=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - } - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -14258,16 +13429,6 @@ "lodash": "4.17.4" } }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -14532,26 +13693,6 @@ "dev": true, "optional": true }, - "reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", - "dev": true, - "requires": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" - } - }, - "reduce-function-call": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", - "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", - "dev": true, - "requires": { - "balanced-match": "^0.4.2" - } - }, "regenerate": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", @@ -15861,19 +15002,10 @@ "socks": "~1.1.5" } }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, "source-list-map": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", - "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", "dev": true }, "source-map": { @@ -17281,29 +16413,6 @@ "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", "dev": true }, - "svgo": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", - "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", - "dev": true, - "requires": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.3.1", - "js-yaml": "~3.7.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" - }, - "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - } - } - }, "syntax-error": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", @@ -17853,21 +16962,6 @@ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", "dev": true }, - "uniqid": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", - "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", - "dev": true, - "requires": { - "macaddress": "^0.2.8" - } - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, "unique-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", @@ -18157,12 +17251,6 @@ "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=", "dev": true }, - "vendors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", - "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=", - "dev": true - }, "verror": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", @@ -18752,12 +17840,6 @@ "dev": true, "optional": true }, - "whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", - "dev": true - }, "which": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", diff --git a/package.json b/package.json index d86665c3f93..13a61d6bee0 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "conventional-commits-parser": "^3.0.0", "cp-file": "^6.0.0", "cross-env": "^5.0.0", - "css-loader": "^0.28.0", + "css-loader": "^1.0.0", "cssom": "^0.3.2", "cz-conventional-changelog": "^2.0.0", "del": "^3.0.0", From 4a87a2cf9220bdb31011087dc1d0f0b375dc3e0d Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 19:01:51 -0700 Subject: [PATCH 13/53] chore(infrastructure): Fail Travis screenshot test jobs for external PRs (#3101) --- test/screenshot/commands/test.js | 15 +++++++++------ test/screenshot/commands/travis.sh | 16 ++++++++++++---- test/screenshot/lib/constants.js | 1 + 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/test/screenshot/commands/test.js b/test/screenshot/commands/test.js index d8c76d83c59..7b8c0045518 100644 --- a/test/screenshot/commands/test.js +++ b/test/screenshot/commands/test.js @@ -19,15 +19,18 @@ const BuildCommand = require('./build'); const Controller = require('../lib/controller'); const GitHubApi = require('../lib/github-api'); +const Logger = require('../lib/logger'); const {ExitCode} = require('../lib/constants'); module.exports = { async runAsync() { + const logger = new Logger(__filename); + const travisPrSlug = process.env.TRAVIS_PULL_REQUEST_SLUG; if (travisPrSlug && !travisPrSlug.startsWith('material-components/')) { - console.log('Screenshot tests are not supported on external PRs.'); - console.log('Skipping screenshot tests.'); - return ExitCode.OK; + logger.error('Screenshot tests are not supported on external PRs.'); + logger.error('Skipping screenshot tests.'); + return ExitCode.UNSUPPORTED_EXTERNAL_PR; } await BuildCommand.runAsync(); @@ -39,9 +42,9 @@ module.exports = { const {isTestable, prNumber} = controller.checkIsTestable(reportData); if (!isTestable) { - console.log(`PR #${prNumber} does not contain any testable source file changes.`); - console.log('Skipping screenshot tests.'); - return; + logger.warn(`PR #${prNumber} does not contain any testable source file changes.`); + logger.warn('Skipping screenshot tests.'); + return ExitCode.OK; } await gitHubApi.setPullRequestStatus(reportData); diff --git a/test/screenshot/commands/travis.sh b/test/screenshot/commands/travis.sh index 5163dcb5105..5a6a9d1825f 100755 --- a/test/screenshot/commands/travis.sh +++ b/test/screenshot/commands/travis.sh @@ -1,10 +1,15 @@ #!/usr/bin/env bash +function print_error() { + echo -e "\033[31m\033[1m$@\033[0m" +} + function exit_if_external_pr() { if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then - echo 'Screenshot tests are not supported on external PRs.' - echo 'Skipping screenshot tests.' - exit + echo + print_error 'Screenshot tests are not supported on external PRs.' + print_error 'Skipping screenshot tests.' + exit 19 fi } @@ -44,8 +49,11 @@ function install_google_cloud_sdk() { fi } -if [ "$TEST_SUITE" == 'screenshot' ]; then +if [[ "$TEST_SUITE" == 'screenshot' ]] || [[ "$TEST_SUITE" == 'unit' ]]; then exit_if_external_pr +fi + +if [[ "$TEST_SUITE" == 'screenshot' ]]; then extract_api_credentials install_google_cloud_sdk fi diff --git a/test/screenshot/lib/constants.js b/test/screenshot/lib/constants.js index 1e59d15557a..ab45a42f63d 100644 --- a/test/screenshot/lib/constants.js +++ b/test/screenshot/lib/constants.js @@ -63,5 +63,6 @@ module.exports = { MISSING_ENV_VAR: 16, UNHANDLED_PROMISE_REJECTION: 17, CHANGES_FOUND: 18, + UNSUPPORTED_EXTERNAL_PR: 19, }, }; From e1192df72d46221901509a1c95c3efe71036a1c0 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 20:12:13 -0700 Subject: [PATCH 14/53] chore(infrastructure): More accurate error messages for external PRs on Travis (#3102) --- test/screenshot/commands/build.js | 3 +++ test/screenshot/commands/test.js | 10 +--------- test/screenshot/commands/travis.sh | 9 ++++++--- test/screenshot/lib/logger.js | 15 ++++++++++----- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/test/screenshot/commands/build.js b/test/screenshot/commands/build.js index 0d0ccc8acb1..ca04978e59d 100644 --- a/test/screenshot/commands/build.js +++ b/test/screenshot/commands/build.js @@ -26,6 +26,9 @@ const processManager = new ProcessManager(); module.exports = { async runAsync() { + // Travis sometimes forgets to emit this + logger.foldEnd('install.npm'); + const webpackArgs = []; const shouldBuild = await this.shouldBuild_(); const shouldWatch = await this.shouldWatch_(); diff --git a/test/screenshot/commands/test.js b/test/screenshot/commands/test.js index 7b8c0045518..8511b509c2d 100644 --- a/test/screenshot/commands/test.js +++ b/test/screenshot/commands/test.js @@ -24,18 +24,10 @@ const {ExitCode} = require('../lib/constants'); module.exports = { async runAsync() { - const logger = new Logger(__filename); - - const travisPrSlug = process.env.TRAVIS_PULL_REQUEST_SLUG; - if (travisPrSlug && !travisPrSlug.startsWith('material-components/')) { - logger.error('Screenshot tests are not supported on external PRs.'); - logger.error('Skipping screenshot tests.'); - return ExitCode.UNSUPPORTED_EXTERNAL_PR; - } - await BuildCommand.runAsync(); const controller = new Controller(); const gitHubApi = new GitHubApi(); + const logger = new Logger(__filename); /** @type {!mdc.proto.ReportData} */ const reportData = await controller.initForCapture(); diff --git a/test/screenshot/commands/travis.sh b/test/screenshot/commands/travis.sh index 5a6a9d1825f..d5d1d977804 100755 --- a/test/screenshot/commands/travis.sh +++ b/test/screenshot/commands/travis.sh @@ -6,9 +6,8 @@ function print_error() { function exit_if_external_pr() { if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then - echo - print_error 'Screenshot tests are not supported on external PRs.' - print_error 'Skipping screenshot tests.' + print_error "Error: $TEST_SUITE tests are not supported on external PRs." + print_error "Skipping $TEST_SUITE tests." exit 19 fi } @@ -49,6 +48,10 @@ function install_google_cloud_sdk() { fi } +echo +echo "TEST_SUITE='$TEST_SUITE'" +echo + if [[ "$TEST_SUITE" == 'screenshot' ]] || [[ "$TEST_SUITE" == 'unit' ]]; then exit_if_external_pr fi diff --git a/test/screenshot/lib/logger.js b/test/screenshot/lib/logger.js index e54b3c09a38..3d2bd5e506d 100644 --- a/test/screenshot/lib/logger.js +++ b/test/screenshot/lib/logger.js @@ -129,8 +129,8 @@ class Logger { console.log(''); if (this.isTravisJob_()) { // See https://github.com/travis-ci/docs-travis-ci-com/issues/949#issuecomment-276755003 - console.log(`travis_fold:start:${foldId}\n${colorMessage}`); - console.log(`travis_time:start:${hash}`); + process.stdout.write(`\e[0Ktravis_fold:start:${foldId}\n${colorMessage}\n`); + process.stdout.write(`\e[0Ktravis_time:start:${hash}\n`); } else { console.log(colorMessage); } @@ -151,8 +151,13 @@ class Logger { const durationNanos = finishNanos - startNanos; // See https://github.com/travis-ci/docs-travis-ci-com/issues/949#issuecomment-276755003 - console.log(`travis_fold:end:${foldId}`); - console.log(`travis_time:end:${hash}:start=${startNanos},finish=${finishNanos},duration=${durationNanos}`); + process.stdout.write(`\e[0Ktravis_fold:end:${foldId}\n`); + + if (durationNanos) { + process.stdout.write( + `travis_time:end:${hash}:start=${startNanos},finish=${finishNanos},duration=${durationNanos}\n` + ); + } } getFoldHash_(foldId) { @@ -165,7 +170,7 @@ class Logger { * @param {!Array<*>} args */ log(...args) { - console.log(`[log][${this.id_}]`, ...args); + console.log(...args); } /** From 1e6e6ee20c4fbdb4ca8107594949830043a63a23 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 16 Jul 2018 21:41:00 -0700 Subject: [PATCH 15/53] chore(infrastructure): Mark CBT Selenium tests as pass/fail (#3104) ### What it does - Tests that have N=0 diffs will be marked "pass" in the CBT UI - Tests that have N>0 diffs will be marked as "fail" in the CBT UI - Fixes collapsible fold sections in Travis job logs - Adds timing information to fold sections in Travis job logs ### Example output #### CBT Selenium UI: ![image](https://user-images.githubusercontent.com/409245/42796712-7f77f046-8940-11e8-8c45-704c9d8d00f0.png) #### Travis CI job log: ![image](https://user-images.githubusercontent.com/409245/42796657-42322b0c-8940-11e8-8548-9b038e1d4cec.png) --- test/screenshot/commands/travis.sh | 5 +-- test/screenshot/lib/cbt-api.js | 12 ++++++ test/screenshot/lib/duration.js | 7 ++++ test/screenshot/lib/logger.js | 65 ++++++++++++++++++----------- test/screenshot/lib/selenium-api.js | 29 +++++++++++-- 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/test/screenshot/commands/travis.sh b/test/screenshot/commands/travis.sh index d5d1d977804..8f4729d57e4 100755 --- a/test/screenshot/commands/travis.sh +++ b/test/screenshot/commands/travis.sh @@ -6,6 +6,7 @@ function print_error() { function exit_if_external_pr() { if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then + echo print_error "Error: $TEST_SUITE tests are not supported on external PRs." print_error "Skipping $TEST_SUITE tests." exit 19 @@ -48,10 +49,6 @@ function install_google_cloud_sdk() { fi } -echo -echo "TEST_SUITE='$TEST_SUITE'" -echo - if [[ "$TEST_SUITE" == 'screenshot' ]] || [[ "$TEST_SUITE" == 'unit' ]]; then exit_if_external_pr fi diff --git a/test/screenshot/lib/cbt-api.js b/test/screenshot/lib/cbt-api.js index 271c646e356..5972f2e719c 100644 --- a/test/screenshot/lib/cbt-api.js +++ b/test/screenshot/lib/cbt-api.js @@ -122,6 +122,18 @@ https://crossbrowsertesting.com/account return allBrowsersPromise; } + /** + * @param {string} seleniumSessionId + * @param {!Array} changedScreenshots + * @return {!Promise} + */ + async setTestScore({seleniumSessionId, changedScreenshots}) { + await this.sendRequest_('PUT', `/selenium/${seleniumSessionId}`, { + action: 'set_score', + score: changedScreenshots.length === 0 ? 'pass' : 'fail', + }); + } + /** * @param {!mdc.proto.ReportMeta} meta * @param {!mdc.proto.UserAgent} userAgent diff --git a/test/screenshot/lib/duration.js b/test/screenshot/lib/duration.js index 5ca579906b3..0ae66ad1242 100644 --- a/test/screenshot/lib/duration.js +++ b/test/screenshot/lib/duration.js @@ -83,6 +83,13 @@ class Duration { return this.ms_; } + /** + * @return {number} + */ + toNanos() { + return this.ms_ * 1000 * 1000; + } + /** * TODO(acdvorak): Create `toHumanLong` method that outputs "4d 23h 5m 11s" (or w/e) * @param {number=} numDecimalDigits diff --git a/test/screenshot/lib/logger.js b/test/screenshot/lib/logger.js index 3d2bd5e506d..b4589d95820 100644 --- a/test/screenshot/lib/logger.js +++ b/test/screenshot/lib/logger.js @@ -20,6 +20,8 @@ const colors = require('colors/safe'); const crypto = require('crypto'); const path = require('path'); +const Duration = require('./duration'); + /** * @typedef {(function(string):string|{ * enable: !CliColor, @@ -86,7 +88,7 @@ class Logger { * @type {!Map} * @private */ - this.foldStartTimes_ = new Map(); + this.foldStartTimesMs_ = new Map(); } /** @@ -121,20 +123,26 @@ class Logger { * @param {string} shortMessage */ foldStart(foldId, shortMessage) { - const hash = this.getFoldHash_(foldId); + const timerId = this.getFoldTimerId_(foldId); const colorMessage = colors.bold.yellow(shortMessage); - this.foldStartTimes_.set(foldId, Date.now()); + this.foldStartTimesMs_.set(foldId, Date.now()); - console.log(''); - if (this.isTravisJob_()) { - // See https://github.com/travis-ci/docs-travis-ci-com/issues/949#issuecomment-276755003 - process.stdout.write(`\e[0Ktravis_fold:start:${foldId}\n${colorMessage}\n`); - process.stdout.write(`\e[0Ktravis_time:start:${hash}\n`); - } else { + if (!this.isTravisJob_()) { + console.log(''); console.log(colorMessage); + console.log(colors.reset('')); + return; } + + // Undocumented Travis CI job logging features. See: + // https://github.com/travis-ci/docs-travis-ci-com/issues/949#issuecomment-276755003 + // https://github.com/rspec/rspec-support/blob/5a1c6756a9d8322fc18639b982e00196f452974d/script/travis_functions.sh console.log(''); + console.log(`travis_fold:start:${foldId}`); + console.log(`travis_time:start:${timerId}`); + console.log(colorMessage); + console.log(colors.reset('')); } /** @@ -145,27 +153,23 @@ class Logger { return; } - const hash = this.getFoldHash_(foldId); - const startNanos = this.foldStartTimes_.get(foldId) * 1000; - const finishNanos = Date.now() * 1000; - const durationNanos = finishNanos - startNanos; + // Undocumented Travis CI job logging feature. See: + // https://github.com/travis-ci/docs-travis-ci-com/issues/949#issuecomment-276755003 + console.log(`travis_fold:end:${foldId}`); - // See https://github.com/travis-ci/docs-travis-ci-com/issues/949#issuecomment-276755003 - process.stdout.write(`\e[0Ktravis_fold:end:${foldId}\n`); + const startMs = this.foldStartTimesMs_.get(foldId); + if (startMs) { + const timerId = this.getFoldTimerId_(foldId); + const startNanos = Duration.millis(startMs).toNanos(); + const finishNanos = Duration.millis(Date.now()).toNanos(); + const durationNanos = finishNanos - startNanos; - if (durationNanos) { - process.stdout.write( - `travis_time:end:${hash}:start=${startNanos},finish=${finishNanos},duration=${durationNanos}\n` - ); + // Undocumented Travis CI job logging feature. See: + // https://github.com/rspec/rspec-support/blob/5a1c6756a9d8322fc18639b982e00196f452974d/script/travis_functions.sh + console.log(`travis_time:end:${timerId}:start=${startNanos},finish=${finishNanos},duration=${durationNanos}`); } } - getFoldHash_(foldId) { - const sha1Sum = crypto.createHash('sha1'); - sha1Sum.update(foldId); - return sha1Sum.digest('hex').substr(0, 8); - } - /** * @param {!Array<*>} args */ @@ -201,6 +205,17 @@ class Logger { isTravisJob_() { return process.env.TRAVIS === 'true'; } + + /** + * @param {string} foldId + * @return {string} + * @private + */ + getFoldTimerId_(foldId) { + const sha1Sum = crypto.createHash('sha1'); + sha1Sum.update(foldId); + return sha1Sum.digest('hex').substr(0, 8); + } } module.exports = Logger; diff --git a/test/screenshot/lib/selenium-api.js b/test/screenshot/lib/selenium-api.js index 03a4b785e96..9ddf05e1404 100644 --- a/test/screenshot/lib/selenium-api.js +++ b/test/screenshot/lib/selenium-api.js @@ -120,6 +120,11 @@ class SeleniumApi { /** @type {!IWebDriver} */ const driver = await this.createWebDriver_({reportData, userAgent}); + /** @type {!Session} */ + const session = await driver.getSession(); + const seleniumSessionId = session.getId(); + let changedScreenshots; + const logResult = (verb) => { /* eslint-disable camelcase */ const {os_name, os_version, browser_name, browser_version} = userAgent.navigator; @@ -128,7 +133,7 @@ class SeleniumApi { }; try { - await this.driveBrowser_({reportData, userAgent, driver}); + changedScreenshots = (await this.driveBrowser_({reportData, userAgent, driver})).changedScreenshots; logResult('Finished'); } catch (err) { logResult('Failed'); @@ -137,6 +142,11 @@ class SeleniumApi { logResult('Quitting'); await driver.quit(); } + + await this.cbtApi_.setTestScore({ + seleniumSessionId, + changedScreenshots, + }); } /** @@ -316,7 +326,10 @@ class SeleniumApi { * @param {!mdc.proto.ReportData} reportData * @param {!mdc.proto.UserAgent} userAgent * @param {!IWebDriver} driver - * @return {!Promise} + * @return {Promise<{ + * changedScreenshots: !Array, + * unchangedScreenshots: !Array, + * }>} * @private */ async driveBrowser_({reportData, userAgent, driver}) { @@ -332,6 +345,9 @@ class SeleniumApi { const meta = reportData.meta; + /** @type {!Array} */ const changedScreenshots = []; + /** @type {!Array} */ const unchangedScreenshots = []; + /** @type {!Array} */ const screenshotQueue = reportData.screenshots.runnable_screenshot_browser_map[userAgent.alias].screenshots; @@ -345,11 +361,16 @@ class SeleniumApi { screenshot.diff_image_file = diffImageResult.diff_image_file; if (diffImageResult.has_changed) { - reportData.screenshots.changed_screenshot_list.push(screenshot); + changedScreenshots.push(screenshot); } else { - reportData.screenshots.unchanged_screenshot_list.push(screenshot); + unchangedScreenshots.push(screenshot); } } + + reportData.screenshots.changed_screenshot_list.push(...changedScreenshots); + reportData.screenshots.unchanged_screenshot_list.push(...unchangedScreenshots); + + return {changedScreenshots, unchangedScreenshots}; } /** From a50dd2aabf32c355d82db61f0019827329b4b8b2 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 01:15:10 -0700 Subject: [PATCH 16/53] chore(infrastructure): Move screenshot test packages to `spec/` dir (#3105) ### What it does - Moves test case files from `test/screenshot/mdc-foo/` to `test/screenshot/spec/mdc-foo/` - This will make the directories easier to navigate when we have tests for a large number of components - Centralizes all local filesystem read/write operations into `LocalStorage` class and removes direct usages of `fs` and `mkdirp` from all other files - This ensures that `mkdirp` is automatically called before writing every file (to avoid stupid errors) - Handles encoding/decoding of string and binary formats ### Example output * Diff report: https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/report/report.html --- test/screenshot/golden.json | 288 +++++++++--------- test/screenshot/lib/controller.js | 10 +- test/screenshot/lib/file-cache.js | 16 +- test/screenshot/lib/github-api.js | 9 +- test/screenshot/lib/golden-file.js | 6 +- test/screenshot/lib/golden-io.js | 14 +- test/screenshot/lib/image-cropper.js | 2 - test/screenshot/lib/image-differ.js | 19 +- test/screenshot/lib/local-storage.js | 38 ++- test/screenshot/lib/logger.js | 8 +- test/screenshot/lib/report-builder.js | 12 +- test/screenshot/lib/selenium-api.js | 12 +- test/screenshot/{ => spec}/fixture.js | 0 test/screenshot/{ => spec}/fixture.scss | 0 .../classes/baseline-button-with-icons.html | 8 +- .../baseline-button-without-icons.html | 8 +- .../classes/baseline-link-with-icons.html | 8 +- .../classes/baseline-link-without-icons.html | 8 +- .../classes/dense-button-with-icons.html | 8 +- .../classes/dense-button-without-icons.html | 8 +- .../classes/dense-link-with-icons.html | 8 +- .../classes/dense-link-without-icons.html | 8 +- .../{ => spec}/mdc-button/custom.scss | 0 .../{ => spec}/mdc-button/fixture.scss | 0 .../mixins/container-fill-color.html | 10 +- .../mdc-button/mixins/corner-radius.html | 10 +- .../mdc-button/mixins/filled-accessible.html | 10 +- .../mixins/horizontal-padding-baseline.html | 10 +- .../mixins/horizontal-padding-dense.html | 10 +- .../mdc-button/mixins/icon-color.html | 10 +- .../mdc-button/mixins/ink-color.html | 10 +- .../mdc-button/mixins/stroke-color.html | 10 +- .../mdc-button/mixins/stroke-width.html | 10 +- .../{ => spec}/mdc-fab/classes/baseline.html | 8 +- .../{ => spec}/mdc-fab/classes/extended.html | 8 +- .../{ => spec}/mdc-fab/classes/mini.html | 8 +- .../screenshot/{ => spec}/mdc-fab/custom.scss | 0 .../{ => spec}/mdc-fab/fixture.scss | 0 .../mdc-fab/mixins/extended-padding.html | 10 +- .../mdc-icon-button/classes/baseline.html | 8 +- .../{ => spec}/mdc-icon-button/custom.scss | 0 .../{ => spec}/mdc-icon-button/fixture.scss | 0 .../mdc-icon-button/mixins/icon-size.html | 10 +- .../mdc-icon-button/mixins/ink-color.html | 10 +- 44 files changed, 352 insertions(+), 298 deletions(-) rename test/screenshot/{ => spec}/fixture.js (100%) rename test/screenshot/{ => spec}/fixture.scss (100%) rename test/screenshot/{ => spec}/mdc-button/classes/baseline-button-with-icons.html (97%) rename test/screenshot/{ => spec}/mdc-button/classes/baseline-button-without-icons.html (92%) rename test/screenshot/{ => spec}/mdc-button/classes/baseline-link-with-icons.html (95%) rename test/screenshot/{ => spec}/mdc-button/classes/baseline-link-without-icons.html (90%) rename test/screenshot/{ => spec}/mdc-button/classes/dense-button-with-icons.html (97%) rename test/screenshot/{ => spec}/mdc-button/classes/dense-button-without-icons.html (92%) rename test/screenshot/{ => spec}/mdc-button/classes/dense-link-with-icons.html (95%) rename test/screenshot/{ => spec}/mdc-button/classes/dense-link-without-icons.html (91%) rename test/screenshot/{ => spec}/mdc-button/custom.scss (100%) rename test/screenshot/{ => spec}/mdc-button/fixture.scss (100%) rename test/screenshot/{ => spec}/mdc-button/mixins/container-fill-color.html (85%) rename test/screenshot/{ => spec}/mdc-button/mixins/corner-radius.html (89%) rename test/screenshot/{ => spec}/mdc-button/mixins/filled-accessible.html (92%) rename test/screenshot/{ => spec}/mdc-button/mixins/horizontal-padding-baseline.html (93%) rename test/screenshot/{ => spec}/mdc-button/mixins/horizontal-padding-dense.html (93%) rename test/screenshot/{ => spec}/mdc-button/mixins/icon-color.html (94%) rename test/screenshot/{ => spec}/mdc-button/mixins/ink-color.html (94%) rename test/screenshot/{ => spec}/mdc-button/mixins/stroke-color.html (85%) rename test/screenshot/{ => spec}/mdc-button/mixins/stroke-width.html (95%) rename test/screenshot/{ => spec}/mdc-fab/classes/baseline.html (90%) rename test/screenshot/{ => spec}/mdc-fab/classes/extended.html (89%) rename test/screenshot/{ => spec}/mdc-fab/classes/mini.html (90%) rename test/screenshot/{ => spec}/mdc-fab/custom.scss (100%) rename test/screenshot/{ => spec}/mdc-fab/fixture.scss (100%) rename test/screenshot/{ => spec}/mdc-fab/mixins/extended-padding.html (87%) rename test/screenshot/{ => spec}/mdc-icon-button/classes/baseline.html (94%) rename test/screenshot/{ => spec}/mdc-icon-button/custom.scss (100%) rename test/screenshot/{ => spec}/mdc-icon-button/fixture.scss (100%) rename test/screenshot/{ => spec}/mdc-icon-button/mixins/icon-size.html (92%) rename test/screenshot/{ => spec}/mdc-icon-button/mixins/ink-color.html (92%) diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index 18ac295065f..ed8f283e3c6 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -1,218 +1,218 @@ { - "mdc-button/classes/baseline-button-with-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-button-with-icons.html", + "spec/mdc-button/classes/baseline-button-with-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/classes/baseline-button-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/classes/baseline-button-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-button-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/06_12_41_520/mdc-button/classes/baseline-button-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/baseline-button-without-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-button-without-icons.html", + "spec/mdc-button/classes/baseline-button-without-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-button-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-button-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-button-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-button-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/baseline-link-with-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-link-with-icons.html", + "spec/mdc-button/classes/baseline-link-with-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/classes/baseline-link-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/09_26_36_096/mdc-button/classes/baseline-link-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-link-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-link-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/baseline-link-without-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/baseline-link-without-icons.html", + "spec/mdc-button/classes/baseline-link-without-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-link-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-link-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-link-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/baseline-link-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/dense-button-with-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-button-with-icons.html", + "spec/mdc-button/classes/dense-button-with-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/classes/dense-button-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/classes/dense-button-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-button-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-button-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/dense-button-without-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-button-without-icons.html", + "spec/mdc-button/classes/dense-button-without-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-button-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-button-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-button-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-button-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/dense-link-with-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-link-with-icons.html", + "spec/mdc-button/classes/dense-link-with-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/classes/dense-link-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/classes/dense-link-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-link-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-link-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_ie_11.png" } }, - "mdc-button/classes/dense-link-without-icons.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/classes/dense-link-without-icons.html", + "spec/mdc-button/classes/dense-link-without-icons.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-link-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-link-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-link-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/classes/dense-link-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_ie_11.png" } }, - "mdc-button/mixins/container-fill-color.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/container-fill-color.html", + "spec/mdc-button/mixins/container-fill-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/container-fill-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/06_16_21_090/mdc-button/mixins/container-fill-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/container-fill-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/container-fill-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_ie_11.png" } }, - "mdc-button/mixins/corner-radius.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/corner-radius.html", + "spec/mdc-button/mixins/corner-radius.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/mixins/corner-radius.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/mixins/corner-radius.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/corner-radius.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/corner-radius.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_ie_11.png" } }, - "mdc-button/mixins/filled-accessible.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/filled-accessible.html", + "spec/mdc-button/mixins/filled-accessible.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/filled-accessible.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/mixins/filled-accessible.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/filled-accessible.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/filled-accessible.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_ie_11.png" } }, - "mdc-button/mixins/horizontal-padding-baseline.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/horizontal-padding-baseline.html", + "spec/mdc-button/mixins/horizontal-padding-baseline.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-baseline.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-baseline.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-baseline.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-baseline.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_ie_11.png" } }, - "mdc-button/mixins/horizontal-padding-dense.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/horizontal-padding-dense.html", + "spec/mdc-button/mixins/horizontal-padding-dense.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-dense.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-dense.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-dense.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/06_00_21_336/mdc-button/mixins/horizontal-padding-dense.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_ie_11.png" } }, - "mdc-button/mixins/icon-color.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/icon-color.html", + "spec/mdc-button/mixins/icon-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/icon-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/mixins/icon-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/icon-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/icon-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_ie_11.png" } }, - "mdc-button/mixins/ink-color.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/ink-color.html", + "spec/mdc-button/mixins/ink-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/ink-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-button/mixins/ink-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/ink-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/ink-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_ie_11.png" } }, - "mdc-button/mixins/stroke-color.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/stroke-color.html", + "spec/mdc-button/mixins/stroke-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/mixins/stroke-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/stroke-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/stroke-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/stroke-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_ie_11.png" } }, - "mdc-button/mixins/stroke-width.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/05/22/16_23_58_618/af3d7271f/mdc-button/mixins/stroke-width.html", + "spec/mdc-button/mixins/stroke-width.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/13/15_50_29_237/mdc-button/mixins/stroke-width.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/stroke-width.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/stroke-width.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-button/mixins/stroke-width.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_ie_11.png" } }, - "mdc-fab/classes/baseline.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/05/25/21_14_58_299/6b0f3e00/mdc-fab/classes/baseline.html", + "spec/mdc-fab/classes/baseline.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/baseline.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-fab/classes/baseline.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/baseline.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/baseline.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_ie_11.png" } }, - "mdc-fab/classes/extended.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/abhiomkar/2018/06/19/19_52_01_078/mdc-fab/classes/extended.html", + "spec/mdc-fab/classes/extended.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/extended.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-fab/classes/extended.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/extended.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/extended.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_ie_11.png" } }, - "mdc-fab/classes/mini.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/05/25/21_14_58_299/6b0f3e00/mdc-fab/classes/mini.html", + "spec/mdc-fab/classes/mini.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/mini.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/12_02_54_125/mdc-fab/classes/mini.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/mini.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-fab/classes/mini.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_ie_11.png" } }, - "mdc-fab/mixins/extended-padding.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/13/22_35_48_118/mdc-fab/mixins/extended-padding.html", + "spec/mdc-fab/mixins/extended-padding.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/13/22_35_48_118/mdc-fab/mixins/extended-padding.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/13/22_35_48_118/mdc-fab/mixins/extended-padding.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/13/22_35_48_118/mdc-fab/mixins/extended-padding.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/13/22_35_48_118/mdc-fab/mixins/extended-padding.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_ie_11.png" } }, - "mdc-icon-button/classes/baseline.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/williamernest/2018/05/30/14_38_21_131/4dc98079/mdc-icon-button/classes/baseline.html", + "spec/mdc-icon-button/classes/baseline.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/classes/baseline.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/classes/baseline.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/classes/baseline.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/classes/baseline.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_ie_11.png" } }, - "mdc-icon-button/mixins/icon-size.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/williamernest/2018/05/30/14_38_21_131/4dc98079/mdc-icon-button/mixins/icon-size.html", + "spec/mdc-icon-button/mixins/icon-size.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/icon-size.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/icon-size.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/icon-size.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/icon-size.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_ie_11.png" } }, - "mdc-icon-button/mixins/ink-color.html": { - "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/williamernest/2018/05/30/14_38_21_131/4dc98079/mdc-icon-button/mixins/ink-color.html", + "spec/mdc-icon-button/mixins/ink-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/ink-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/ink-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/ink-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/05_44_01_282/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" } } } diff --git a/test/screenshot/lib/controller.js b/test/screenshot/lib/controller.js index 98b41ee75b6..ef546b07786 100644 --- a/test/screenshot/lib/controller.js +++ b/test/screenshot/lib/controller.js @@ -193,12 +193,16 @@ class Controller { reportData.screenshots.added_screenshot_list.length + reportData.screenshots.removed_screenshot_list.length; + const boldRed = Logger.colors.bold.red; + const boldGreen = Logger.colors.bold.green; + if (numChanges > 0) { - this.logger_.error(`\n\n${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`); + this.logger_.error(boldRed(`\n\n${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`)); + this.logger_.log('Diff report:', boldRed(reportData.meta.report_html_file.public_url)); } else { - this.logger_.log(`\n\n${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`); + this.logger_.log(boldGreen(`\n\n${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`)); + this.logger_.log('Diff report:', boldGreen(reportData.meta.report_html_file.public_url)); } - this.logger_.log('Diff report:', Logger.colors.bold.red(reportData.meta.report_html_file.public_url)); return reportData; } diff --git a/test/screenshot/lib/file-cache.js b/test/screenshot/lib/file-cache.js index 807a5021c4f..7c74275d2f1 100644 --- a/test/screenshot/lib/file-cache.js +++ b/test/screenshot/lib/file-cache.js @@ -14,7 +14,6 @@ * limitations under the License. */ -const fs = require('mz/fs'); const mkdirp = require('mkdirp'); const os = require('os'); const path = require('path'); @@ -37,7 +36,7 @@ class FileCache { this.tempDirPath_ = path.join(os.tmpdir(), 'mdc-web/url-cache'); /** - * @type {@LocalStorage} + * @type {!LocalStorage} * @private */ this.localStorage_ = new LocalStorage(); @@ -51,9 +50,10 @@ class FileCache { async downloadUrlToDisk(uri, encoding = null) { mkdirp.sync(this.tempDirPath_); - const fakeRelativePath = uri.replace(/.*\/mdc-/, 'mdc-'); // TODO(acdvorak): Document this hack + // TODO(acdvorak): Document this hack + const fakeRelativePath = uri.replace(/.*\/spec\/mdc-/, 'spec/mdc-'); - if (await fs.exists(uri)) { + if (await this.localStorage_.exists(uri)) { return TestFile.create({ absolute_path: path.resolve(uri), relative_path: fakeRelativePath, @@ -63,7 +63,7 @@ class FileCache { const fileName = this.getFilename_(uri); const filePath = path.resolve(this.tempDirPath_, fileName); - if (await fs.exists(filePath)) { + if (await this.localStorage_.exists(filePath)) { return TestFile.create({ absolute_path: filePath, relative_path: fakeRelativePath, @@ -72,12 +72,12 @@ class FileCache { } const buffer = await request({uri, encoding}); - await fs.writeFile(filePath, buffer, {encoding}) + await this.localStorage_.writeBinaryFile(filePath, buffer, encoding) .catch(async (err) => { console.error(`downloadUrlToDisk("${uri}"):`); console.error(err); - if (await fs.exists(filePath)) { - await fs.unlink(filePath); + if (await this.localStorage_.exists(filePath)) { + await this.localStorage_.delete(filePath); } }); diff --git a/test/screenshot/lib/github-api.js b/test/screenshot/lib/github-api.js index d623b736ef4..9487ecbf3a1 100644 --- a/test/screenshot/lib/github-api.js +++ b/test/screenshot/lib/github-api.js @@ -68,7 +68,8 @@ class GitHubApi { } const screenshots = reportData.screenshots; - const numChanges = + const numUnchanged = screenshots.unchanged_screenshot_list.length; + const numChanged = screenshots.changed_screenshot_list.length + screenshots.added_screenshot_list.length + screenshots.removed_screenshot_list.length; @@ -79,12 +80,12 @@ class GitHubApi { let description; if (reportFileUrl) { - if (numChanges > 0) { + if (numChanged > 0) { state = GitHubApi.PullRequestState.FAILURE; - description = `${numChanges} screenshots differ from PR's golden.json`; + description = `${numChanged.toLocaleString()} screenshots differ from PR's golden.json`; } else { state = GitHubApi.PullRequestState.SUCCESS; - description = "All screenshots match PR's golden.json"; + description = `All ${numUnchanged.toLocaleString()} screenshots match PR's golden.json`; } targetUrl = meta.report_html_file.public_url; diff --git a/test/screenshot/lib/golden-file.js b/test/screenshot/lib/golden-file.js index 6019b094c0b..c1d667dce51 100644 --- a/test/screenshot/lib/golden-file.js +++ b/test/screenshot/lib/golden-file.js @@ -82,7 +82,7 @@ class GoldenFile { if (!this.suiteJson_[htmlFilePath]) { this.suiteJson_[htmlFilePath] = { - publicUrl: htmlFileUrl, + public_url: htmlFileUrl, screenshots: {}, }; } @@ -123,7 +123,9 @@ class GoldenFile { for (const userAgentAlias of Object.keys(testPage.screenshots)) { const screenshotImageUrl = testPage.screenshots[userAgentAlias]; - const screenshotImagePath = screenshotImageUrl.replace(/.*\/mdc-/, 'mdc-'); // TODO(acdvorak): Document the hack + + // TODO(acdvorak): Document this hack + const screenshotImagePath = screenshotImageUrl.replace(/.*\/spec\/mdc-/, 'spec/mdc-'); goldenScreenshots.push(GoldenScreenshot.create({ html_file_path: htmlFilePath, diff --git a/test/screenshot/lib/golden-io.js b/test/screenshot/lib/golden-io.js index 2c2a4a2dec0..ac67a32516d 100644 --- a/test/screenshot/lib/golden-io.js +++ b/test/screenshot/lib/golden-io.js @@ -14,13 +14,13 @@ * limitations under the License. */ -const fs = require('mz/fs'); const request = require('request-promise-native'); const stringify = require('json-stable-stringify'); const Cli = require('./cli'); const GitRepo = require('./git-repo'); const GoldenFile = require('./golden-file'); +const LocalStorage = require('./local-storage'); const {GOLDEN_JSON_RELATIVE_PATH} = require('./constants'); /** @@ -40,6 +40,12 @@ class GoldenIo { */ this.gitRepo_ = new GitRepo(); + /** + * @type {!LocalStorage} + * @private + */ + this.localStorage_ = new LocalStorage(); + /** * @type {!Object} * @private @@ -51,7 +57,7 @@ class GoldenIo { * @return {!Promise} */ async readFromLocalFile() { - return new GoldenFile(JSON.parse(await fs.readFile(GOLDEN_JSON_RELATIVE_PATH, {encoding: 'utf8'}))); + return new GoldenFile(JSON.parse(await this.localStorage_.readTextFile(GOLDEN_JSON_RELATIVE_PATH))); } /** @@ -88,7 +94,7 @@ class GoldenIo { const localFilePath = parsedDiffBase.local_file_path; if (localFilePath) { - return fs.readFile(localFilePath, {encoding: 'utf8'}); + return this.localStorage_.readTextFile(localFilePath); } const rev = parsedDiffBase.git_revision; @@ -110,7 +116,7 @@ class GoldenIo { const goldenJsonFilePath = GOLDEN_JSON_RELATIVE_PATH; const goldenJsonFileContent = await this.stringify_(newGoldenFile); - await fs.writeFile(goldenJsonFilePath, goldenJsonFileContent); + await this.localStorage_.writeTextFile(goldenJsonFilePath, goldenJsonFileContent); console.log(`DONE updating "${goldenJsonFilePath}"!`); } diff --git a/test/screenshot/lib/image-cropper.js b/test/screenshot/lib/image-cropper.js index d3fd663e2af..162fa0f4f15 100644 --- a/test/screenshot/lib/image-cropper.js +++ b/test/screenshot/lib/image-cropper.js @@ -15,7 +15,6 @@ */ const Jimp = require('jimp'); -const fs = require('mz/fs'); const TRIM_COLOR_CSS_VALUE = '#abc123'; // Value must match `$test-trim-color` in `fixture.scss` @@ -54,7 +53,6 @@ class ImageCropper { * @return {!Promise} Cropped image buffer */ async autoCropImage(imageData) { - await fs.writeFile('/tmp/image.png', imageData, {encoding: null}); return Jimp.read(imageData) .then( (jimpImage) => { diff --git a/test/screenshot/lib/image-differ.js b/test/screenshot/lib/image-differ.js index 5293c9f86d4..39fad56896d 100644 --- a/test/screenshot/lib/image-differ.js +++ b/test/screenshot/lib/image-differ.js @@ -18,17 +18,25 @@ const Jimp = require('jimp'); const compareImages = require('resemblejs/compareImages'); -const fs = require('mz/fs'); -const mkdirp = require('mkdirp'); const path = require('path'); const mdcProto = require('../proto/mdc.pb').mdc.proto; const {DiffImageResult, Dimensions, TestFile} = mdcProto; +const LocalStorage = require('./local-storage'); + /** * Computes the difference between two screenshot images and generates an image that highlights the pixels that changed. */ class ImageDiffer { + constructor() { + /** + * @type {!LocalStorage} + * @private + */ + this.localStorage_ = new LocalStorage(); + } + /** * @param {!mdc.proto.ReportMeta} meta * @param {!mdc.proto.Screenshot} screenshot @@ -50,7 +58,7 @@ class ImageDiffer { * @private */ async compareOneImage_({meta, actualImageFile, expectedImageFile}) { - const actualImageBuffer = await fs.readFile(actualImageFile.absolute_path); + const actualImageBuffer = await this.localStorage_.readBinaryFile(actualImageFile.absolute_path); if (!expectedImageFile) { const actualJimpImage = await Jimp.read(actualImageBuffer); @@ -62,7 +70,7 @@ class ImageDiffer { }); } - const expectedImageBuffer = await fs.readFile(expectedImageFile.absolute_path); + const expectedImageBuffer = await this.localStorage_.readBinaryFile(expectedImageFile.absolute_path); /** @type {!ResembleApiComparisonResult} */ const resembleComparisonResult = await this.computeDiff_({actualImageBuffer, expectedImageBuffer}); @@ -75,8 +83,7 @@ class ImageDiffer { }); diffImageResult.diff_image_file = diffImageFile; - mkdirp.sync(path.dirname(diffImageFile.absolute_path)); - await fs.writeFile(diffImageFile.absolute_path, diffImageBuffer, {encoding: null}); + await this.localStorage_.writeBinaryFile(diffImageFile.absolute_path, diffImageBuffer); return diffImageResult; } diff --git a/test/screenshot/lib/local-storage.js b/test/screenshot/lib/local-storage.js index 9b19bbdf42d..746e329d26b 100644 --- a/test/screenshot/lib/local-storage.js +++ b/test/screenshot/lib/local-storage.js @@ -16,6 +16,7 @@ 'use strict'; +const del = require('del'); const fs = require('mz/fs'); const fsx = require('fs-extra'); const glob = require('glob'); @@ -62,7 +63,7 @@ class LocalStorage { */ async getTestPageDestinationPaths(reportMeta) { const cwd = reportMeta.local_asset_base_dir; - return glob.sync('**/mdc-*/**/*.html', {cwd, nodir: true}); + return glob.sync('**/spec/mdc-*/**/*.html', {cwd, nodir: true}); } /** @@ -78,11 +79,12 @@ class LocalStorage { /** * @param {string} filePath * @param {!Buffer} fileContent + * @param {?string=} encoding * @return {!Promise} */ - async writeBinaryFile(filePath, fileContent) { + async writeBinaryFile(filePath, fileContent, encoding = null) { mkdirp.sync(path.dirname(filePath)); - await fs.writeFile(filePath, fileContent, {encoding: null}); + await fs.writeFile(filePath, fileContent, {encoding}); } /** @@ -128,6 +130,36 @@ class LocalStorage { return fsx.copy(src, dest); } + /** + * @param {string|!Array} pathPatterns + * @return {!Promise<*>} + */ + async delete(pathPatterns) { + return del(pathPatterns); + } + + /** + * @param {string} filePath + * @return {!Promise} + */ + async exists(filePath) { + return fs.exists(filePath); + } + + /** + * @param {string} filePaths + */ + mkdirpForFilesSync(...filePaths) { + filePaths.forEach((filePath) => mkdirp.sync(path.dirname(filePath))); + } + + /** + * @param {string} dirPaths + */ + mkdirpForDirsSync(...dirPaths) { + dirPaths.forEach((dirPath) => mkdirp.sync(dirPath)); + } + /** * @return {!Promise>} File paths relative to the git repo. E.g.: "test/screenshot/browser.json". * @private diff --git a/test/screenshot/lib/logger.js b/test/screenshot/lib/logger.js index b4589d95820..8dde6305385 100644 --- a/test/screenshot/lib/logger.js +++ b/test/screenshot/lib/logger.js @@ -171,28 +171,28 @@ class Logger { } /** - * @param {!Array<*>} args + * @param {*} args */ log(...args) { console.log(...args); } /** - * @param {!Array<*>} args + * @param {*} args */ info(...args) { console.info(`[${colors.blue('info')}][${this.id_}]`, ...args); } /** - * @param {!Array<*>} args + * @param {*} args */ warn(...args) { console.warn(`[${colors.yellow('warn')}][${this.id_}]`, ...args); } /** - * @param {!Array<*>} args + * @param {*} args */ error(...args) { console.error(`[${colors.bold.red('error')}][${this.id_}]`, ...args); diff --git a/test/screenshot/lib/report-builder.js b/test/screenshot/lib/report-builder.js index 46000800185..c860950aea6 100644 --- a/test/screenshot/lib/report-builder.js +++ b/test/screenshot/lib/report-builder.js @@ -20,7 +20,6 @@ const Jimp = require('jimp'); const childProcess = require('mz/child_process'); const detectPort = require('detect-port'); const express = require('express'); -const mkdirp = require('mkdirp'); const os = require('os'); const osName = require('os-name'); const path = require('path'); @@ -387,11 +386,12 @@ class ReportBuilder { const localDiffImageBaseDir = path.join(TEMP_DIR, 'mdc-web/diffs', remoteUploadBaseDir); const localReportBaseDir = path.join(TEMP_DIR, 'mdc-web/report', remoteUploadBaseDir); - // TODO(acdvorak): Centralize file writing and automatically call mkdirp - mkdirp.sync(path.dirname(localAssetBaseDir)); - mkdirp.sync(path.dirname(localScreenshotImageBaseDir)); - mkdirp.sync(path.dirname(localDiffImageBaseDir)); - mkdirp.sync(path.dirname(localReportBaseDir)); + this.localStorage_.mkdirpForDirsSync( + localAssetBaseDir, + localScreenshotImageBaseDir, + localDiffImageBaseDir, + localReportBaseDir, + ); const mdcVersionString = require('../../../lerna.json').version; const hostOsName = osName(os.platform(), os.release()); diff --git a/test/screenshot/lib/selenium-api.js b/test/screenshot/lib/selenium-api.js index 9ddf05e1404..8b53b4be94d 100644 --- a/test/screenshot/lib/selenium-api.js +++ b/test/screenshot/lib/selenium-api.js @@ -18,8 +18,6 @@ const Jimp = require('jimp'); const UserAgentParser = require('useragent'); -const fs = require('mz/fs'); -const mkdirp = require('mkdirp'); const path = require('path'); const mdcProto = require('../proto/mdc.pb').mdc.proto; @@ -36,6 +34,7 @@ const Constants = require('./constants'); const Duration = require('./duration'); const ImageCropper = require('./image-cropper'); const ImageDiffer = require('./image-differ'); +const LocalStorage = require('./local-storage'); const {Browser, Builder, By, until} = require('selenium-webdriver'); const {CBT_CONCURRENCY_POLL_INTERVAL_MS, CBT_CONCURRENCY_MAX_WAIT_MS} = Constants; const {SELENIUM_FONT_LOAD_WAIT_MS} = Constants; @@ -65,6 +64,12 @@ class SeleniumApi { * @private */ this.imageDiffer_ = new ImageDiffer(); + + /** + * @type {!LocalStorage} + * @private + */ + this.localStorage_ = new LocalStorage(); } /** @@ -435,8 +440,7 @@ class SeleniumApi { const imageFilePathRelative = `${htmlFilePath}.${imageFileNameSuffix}.png`; const imageFilePathAbsolute = path.resolve(meta.local_screenshot_image_base_dir, imageFilePathRelative); - mkdirp.sync(path.dirname(imageFilePathAbsolute)); - await fs.writeFile(imageFilePathAbsolute, imageBuffer, {encoding: null}); + await this.localStorage_.writeBinaryFile(imageFilePathAbsolute, imageBuffer); return TestFile.create({ relative_path: imageFilePathRelative, diff --git a/test/screenshot/fixture.js b/test/screenshot/spec/fixture.js similarity index 100% rename from test/screenshot/fixture.js rename to test/screenshot/spec/fixture.js diff --git a/test/screenshot/fixture.scss b/test/screenshot/spec/fixture.scss similarity index 100% rename from test/screenshot/fixture.scss rename to test/screenshot/spec/fixture.scss diff --git a/test/screenshot/mdc-button/classes/baseline-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html similarity index 97% rename from test/screenshot/mdc-button/classes/baseline-button-with-icons.html rename to test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html index bfaf6e682e4..0ca1951a0f0 100644 --- a/test/screenshot/mdc-button/classes/baseline-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html @@ -19,9 +19,9 @@ Baseline Button Element With Icons - MDC Web Screenshot Test - - - + + + @@ -161,6 +161,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/baseline-button-without-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html similarity index 92% rename from test/screenshot/mdc-button/classes/baseline-button-without-icons.html rename to test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html index 95e46db6ced..4661d0dc5b5 100644 --- a/test/screenshot/mdc-button/classes/baseline-button-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html @@ -19,9 +19,9 @@ Baseline Button Element Without Icons - MDC Web Screenshot Test - - - + + + @@ -72,6 +72,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/baseline-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html similarity index 95% rename from test/screenshot/mdc-button/classes/baseline-link-with-icons.html rename to test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html index b791bfaa1c4..5b190085b95 100644 --- a/test/screenshot/mdc-button/classes/baseline-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html @@ -19,9 +19,9 @@ Baseline Button Link With Icons - MDC Web Screenshot Test - - - + + + @@ -97,6 +97,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/baseline-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html similarity index 90% rename from test/screenshot/mdc-button/classes/baseline-link-without-icons.html rename to test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html index 97b3e98740d..3ae0df07a76 100644 --- a/test/screenshot/mdc-button/classes/baseline-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html @@ -19,9 +19,9 @@ Baseline Button Link Without Icons - MDC Web Screenshot Test - - - + + + @@ -59,6 +59,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/dense-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html similarity index 97% rename from test/screenshot/mdc-button/classes/dense-button-with-icons.html rename to test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html index 4de789fbae0..b569702ea95 100644 --- a/test/screenshot/mdc-button/classes/dense-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html @@ -19,9 +19,9 @@ Dense Button Element With Icons - MDC Web Screenshot Test - - - + + + @@ -177,6 +177,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/dense-button-without-icons.html b/test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html similarity index 92% rename from test/screenshot/mdc-button/classes/dense-button-without-icons.html rename to test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html index 312e4487cbf..f0df11b45a5 100644 --- a/test/screenshot/mdc-button/classes/dense-button-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html @@ -19,9 +19,9 @@ Dense Button Element Without Icons - MDC Web Screenshot Test - - - + + + @@ -72,6 +72,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/dense-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html similarity index 95% rename from test/screenshot/mdc-button/classes/dense-link-with-icons.html rename to test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html index 92f125ceb1b..7a19a8c0cbd 100644 --- a/test/screenshot/mdc-button/classes/dense-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html @@ -19,9 +19,9 @@ Dense Button Link With Icons - MDC Web Screenshot Test - - - + + + @@ -105,6 +105,6 @@ - + diff --git a/test/screenshot/mdc-button/classes/dense-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html similarity index 91% rename from test/screenshot/mdc-button/classes/dense-link-without-icons.html rename to test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html index 278fca89ca1..8739f2a56a4 100644 --- a/test/screenshot/mdc-button/classes/dense-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html @@ -19,9 +19,9 @@ Dense Button Link Without Icons - MDC Web Screenshot Test - - - + + + @@ -59,6 +59,6 @@ - + diff --git a/test/screenshot/mdc-button/custom.scss b/test/screenshot/spec/mdc-button/custom.scss similarity index 100% rename from test/screenshot/mdc-button/custom.scss rename to test/screenshot/spec/mdc-button/custom.scss diff --git a/test/screenshot/mdc-button/fixture.scss b/test/screenshot/spec/mdc-button/fixture.scss similarity index 100% rename from test/screenshot/mdc-button/fixture.scss rename to test/screenshot/spec/mdc-button/fixture.scss diff --git a/test/screenshot/mdc-button/mixins/container-fill-color.html b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html similarity index 85% rename from test/screenshot/mdc-button/mixins/container-fill-color.html rename to test/screenshot/spec/mdc-button/mixins/container-fill-color.html index 46e3d4fbee6..a2fcb12efc7 100644 --- a/test/screenshot/mdc-button/mixins/container-fill-color.html +++ b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html @@ -19,10 +19,10 @@ container-fill-color Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -44,6 +44,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/corner-radius.html b/test/screenshot/spec/mdc-button/mixins/corner-radius.html similarity index 89% rename from test/screenshot/mdc-button/mixins/corner-radius.html rename to test/screenshot/spec/mdc-button/mixins/corner-radius.html index 1e452c5c39e..8d797a30d68 100644 --- a/test/screenshot/mdc-button/mixins/corner-radius.html +++ b/test/screenshot/spec/mdc-button/mixins/corner-radius.html @@ -19,10 +19,10 @@ corner-radius Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -59,6 +59,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/filled-accessible.html b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html similarity index 92% rename from test/screenshot/mdc-button/mixins/filled-accessible.html rename to test/screenshot/spec/mdc-button/mixins/filled-accessible.html index 9678215e18f..5475e3aa521 100644 --- a/test/screenshot/mdc-button/mixins/filled-accessible.html +++ b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html @@ -19,10 +19,10 @@ filled-accessible Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -71,6 +71,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/horizontal-padding-baseline.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html similarity index 93% rename from test/screenshot/mdc-button/mixins/horizontal-padding-baseline.html rename to test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html index 91d7f8ff55b..aba9a80b130 100644 --- a/test/screenshot/mdc-button/mixins/horizontal-padding-baseline.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html @@ -19,10 +19,10 @@ horizontal-padding Baseline Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -86,6 +86,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/horizontal-padding-dense.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html similarity index 93% rename from test/screenshot/mdc-button/mixins/horizontal-padding-dense.html rename to test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html index f69d20271e8..fa52b3cbb6f 100644 --- a/test/screenshot/mdc-button/mixins/horizontal-padding-dense.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html @@ -19,10 +19,10 @@ horizontal-padding Dense Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -86,6 +86,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/icon-color.html b/test/screenshot/spec/mdc-button/mixins/icon-color.html similarity index 94% rename from test/screenshot/mdc-button/mixins/icon-color.html rename to test/screenshot/spec/mdc-button/mixins/icon-color.html index 2d175244ac3..5127d44d87f 100644 --- a/test/screenshot/mdc-button/mixins/icon-color.html +++ b/test/screenshot/spec/mdc-button/mixins/icon-color.html @@ -19,10 +19,10 @@ icon-color Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -95,6 +95,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/ink-color.html b/test/screenshot/spec/mdc-button/mixins/ink-color.html similarity index 94% rename from test/screenshot/mdc-button/mixins/ink-color.html rename to test/screenshot/spec/mdc-button/mixins/ink-color.html index 361f71f1a1e..9cf5b9b72ca 100644 --- a/test/screenshot/mdc-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-button/mixins/ink-color.html @@ -19,10 +19,10 @@ ink-color Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -107,6 +107,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/stroke-color.html b/test/screenshot/spec/mdc-button/mixins/stroke-color.html similarity index 85% rename from test/screenshot/mdc-button/mixins/stroke-color.html rename to test/screenshot/spec/mdc-button/mixins/stroke-color.html index 4ed48e26ade..cb6700c86f1 100644 --- a/test/screenshot/mdc-button/mixins/stroke-color.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-color.html @@ -19,10 +19,10 @@ outline-color Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -44,6 +44,6 @@ - + diff --git a/test/screenshot/mdc-button/mixins/stroke-width.html b/test/screenshot/spec/mdc-button/mixins/stroke-width.html similarity index 95% rename from test/screenshot/mdc-button/mixins/stroke-width.html rename to test/screenshot/spec/mdc-button/mixins/stroke-width.html index c0cea1ce16e..40d8e39df31 100644 --- a/test/screenshot/mdc-button/mixins/stroke-width.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-width.html @@ -19,10 +19,10 @@ outline-width Button Mixin - MDC Web Screenshot Test - - - - + + + + @@ -107,6 +107,6 @@ - + diff --git a/test/screenshot/mdc-fab/classes/baseline.html b/test/screenshot/spec/mdc-fab/classes/baseline.html similarity index 90% rename from test/screenshot/mdc-fab/classes/baseline.html rename to test/screenshot/spec/mdc-fab/classes/baseline.html index c46341cfdf8..97b34559dcb 100644 --- a/test/screenshot/mdc-fab/classes/baseline.html +++ b/test/screenshot/spec/mdc-fab/classes/baseline.html @@ -19,9 +19,9 @@ Baseline FAB (Floating Action Button) - MDC Web Screenshot Test - - - + + + @@ -47,6 +47,6 @@ - + diff --git a/test/screenshot/mdc-fab/classes/extended.html b/test/screenshot/spec/mdc-fab/classes/extended.html similarity index 89% rename from test/screenshot/mdc-fab/classes/extended.html rename to test/screenshot/spec/mdc-fab/classes/extended.html index d1cd1ad3e8c..52025b53ff8 100644 --- a/test/screenshot/mdc-fab/classes/extended.html +++ b/test/screenshot/spec/mdc-fab/classes/extended.html @@ -19,9 +19,9 @@ Extended FAB (Floating Action Button) - MDC Web Screenshot Test - - - + + + @@ -51,6 +51,6 @@ - + diff --git a/test/screenshot/mdc-fab/classes/mini.html b/test/screenshot/spec/mdc-fab/classes/mini.html similarity index 90% rename from test/screenshot/mdc-fab/classes/mini.html rename to test/screenshot/spec/mdc-fab/classes/mini.html index 5983601dc80..a46e179a748 100644 --- a/test/screenshot/mdc-fab/classes/mini.html +++ b/test/screenshot/spec/mdc-fab/classes/mini.html @@ -19,9 +19,9 @@ Mini FAB (Floating Action Button) - MDC Web Screenshot Test - - - + + + @@ -47,6 +47,6 @@ - + diff --git a/test/screenshot/mdc-fab/custom.scss b/test/screenshot/spec/mdc-fab/custom.scss similarity index 100% rename from test/screenshot/mdc-fab/custom.scss rename to test/screenshot/spec/mdc-fab/custom.scss diff --git a/test/screenshot/mdc-fab/fixture.scss b/test/screenshot/spec/mdc-fab/fixture.scss similarity index 100% rename from test/screenshot/mdc-fab/fixture.scss rename to test/screenshot/spec/mdc-fab/fixture.scss diff --git a/test/screenshot/mdc-fab/mixins/extended-padding.html b/test/screenshot/spec/mdc-fab/mixins/extended-padding.html similarity index 87% rename from test/screenshot/mdc-fab/mixins/extended-padding.html rename to test/screenshot/spec/mdc-fab/mixins/extended-padding.html index dd30aecda22..2eefc295e17 100644 --- a/test/screenshot/mdc-fab/mixins/extended-padding.html +++ b/test/screenshot/spec/mdc-fab/mixins/extended-padding.html @@ -19,10 +19,10 @@ extended-padding FAB Mixin - MDC Web Screenshot Test - - - - + + + + @@ -52,6 +52,6 @@ - + diff --git a/test/screenshot/mdc-icon-button/classes/baseline.html b/test/screenshot/spec/mdc-icon-button/classes/baseline.html similarity index 94% rename from test/screenshot/mdc-icon-button/classes/baseline.html rename to test/screenshot/spec/mdc-icon-button/classes/baseline.html index 006dade8e9f..6163836351e 100644 --- a/test/screenshot/mdc-icon-button/classes/baseline.html +++ b/test/screenshot/spec/mdc-icon-button/classes/baseline.html @@ -20,9 +20,9 @@ Baseline Icon Button - MDC Web Screenshot Test - - - + + +
@@ -77,6 +77,6 @@ - + diff --git a/test/screenshot/mdc-icon-button/custom.scss b/test/screenshot/spec/mdc-icon-button/custom.scss similarity index 100% rename from test/screenshot/mdc-icon-button/custom.scss rename to test/screenshot/spec/mdc-icon-button/custom.scss diff --git a/test/screenshot/mdc-icon-button/fixture.scss b/test/screenshot/spec/mdc-icon-button/fixture.scss similarity index 100% rename from test/screenshot/mdc-icon-button/fixture.scss rename to test/screenshot/spec/mdc-icon-button/fixture.scss diff --git a/test/screenshot/mdc-icon-button/mixins/icon-size.html b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html similarity index 92% rename from test/screenshot/mdc-icon-button/mixins/icon-size.html rename to test/screenshot/spec/mdc-icon-button/mixins/icon-size.html index 6bd583873a8..b65a70b07f4 100644 --- a/test/screenshot/mdc-icon-button/mixins/icon-size.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html @@ -20,10 +20,10 @@ icon-size Icon Button - MDC Web Screenshot Test - - - - + + + +
@@ -78,6 +78,6 @@ - + diff --git a/test/screenshot/mdc-icon-button/mixins/ink-color.html b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html similarity index 92% rename from test/screenshot/mdc-icon-button/mixins/ink-color.html rename to test/screenshot/spec/mdc-icon-button/mixins/ink-color.html index a687db5b2c6..d41b53bb16b 100644 --- a/test/screenshot/mdc-icon-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html @@ -20,10 +20,10 @@ ink-color Icon Button - MDC Web Screenshot Test - - - - + + + +
@@ -78,6 +78,6 @@ - + From 2c1caa37f753b75204db78f8e3765cf33a8f488a Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 04:12:00 -0700 Subject: [PATCH 17/53] chore(infrastructure): Link to GitHub tag for MDC version number (#3106) - Also delete obsolete `.gitignore` file --- test/screenshot/.gitignore | 3 --- test/screenshot/report/_metadata.hbs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 test/screenshot/.gitignore diff --git a/test/screenshot/.gitignore b/test/screenshot/.gitignore deleted file mode 100644 index 5b60b587817..00000000000 --- a/test/screenshot/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -report.html -report.json -snapshot.json diff --git a/test/screenshot/report/_metadata.hbs b/test/screenshot/report/_metadata.hbs index 4d29c0d1b66..0dd273702f1 100644 --- a/test/screenshot/report/_metadata.hbs +++ b/test/screenshot/report/_metadata.hbs @@ -52,7 +52,7 @@ MDC Version: - {{meta.mdc_version.version_string}} + {{meta.mdc_version.version_string}} {{#if meta.mdc_version.commit_offset}} {{/if}} From b47fe7d6724a63e50bc1b8097f00deed5b227b1f Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Tue, 17 Jul 2018 10:27:52 -0400 Subject: [PATCH 18/53] fix(theme): Allow CSS variables to be passed to mdc-theme-prop (#3086) --- packages/mdc-theme/README.md | 8 ++++---- packages/mdc-theme/_mixins.scss | 4 ++-- packages/mdc-theme/_variables.scss | 25 +++++++++++++++---------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/mdc-theme/README.md b/packages/mdc-theme/README.md index c9c4eb7afc6..f757529d00c 100644 --- a/packages/mdc-theme/README.md +++ b/packages/mdc-theme/README.md @@ -146,11 +146,11 @@ Determines whether to use light or dark text on top of a given color. @debug mdc-theme-contrast-tone(#9c27b0); // light ``` -#### `mdc-theme-prop-value($property)` +#### `mdc-theme-prop-value($style)` -If `$property` is a literal color value (e.g., `blue`, `#fff`), it is returned verbatim. Otherwise, the value of the -corresponding theme property (from `$mdc-theme-property-values`) is returned. If `$property` is not a color and no -such theme property exists, an error is thrown. +If `$style` is a color (a literal color value, `currentColor`, or a CSS custom property), it is returned verbatim. +Otherwise, `$style` is treated as a theme property name, and the corresponding value from `$mdc-theme-property-values` +is returned. If this also fails, an error is thrown. This is mainly useful in situations where `mdc-theme-prop` cannot be used directly (e.g., `box-shadow`). diff --git a/packages/mdc-theme/_mixins.scss b/packages/mdc-theme/_mixins.scss index 24732f75824..9510df2dd8f 100644 --- a/packages/mdc-theme/_mixins.scss +++ b/packages/mdc-theme/_mixins.scss @@ -18,11 +18,11 @@ // Applies the correct theme color style to the specified property. // $property is typically color or background-color, but can be any CSS property that accepts color values. -// $style should be one of the map keys in $mdc-theme-property-values (_variables.scss), or a literal color value. +// $style should be one of the map keys in $mdc-theme-property-values (_variables.scss), or a color value. // $edgeOptOut controls whether to feature-detect around Edge to avoid emitting CSS variables for it, // intended for use in cases where interactions with pseudo-element styles cause problems due to Edge bugs. @mixin mdc-theme-prop($property, $style, $important: false, $edgeOptOut: false) { - @if type-of($style) == "color" or $style == "currentColor" { + @if mdc-theme-is-valid-theme-prop-value_($style) { @if $important { #{$property}: $style !important; } @else { diff --git a/packages/mdc-theme/_variables.scss b/packages/mdc-theme/_variables.scss index a46d2d88f0d..8c962f39226 100644 --- a/packages/mdc-theme/_variables.scss +++ b/packages/mdc-theme/_variables.scss @@ -98,28 +98,28 @@ $mdc-theme-property-values: ( text-icon-on-dark: mdc-theme-ink-color-for-fill_(icon, dark) ); -// If `$property` is a literal color value (e.g., `blue`, `#fff`), it is returned verbatim. Otherwise, the value of the -// corresponding theme property (from `$mdc-theme-property-values`) is returned. If `$property` is not a color and no -// such theme property exists, an error is thrown. +// If `$style` is a color (a literal color value, `currentColor`, or a CSS custom property), it is returned verbatim. +// Otherwise, `$style` is treated as a theme property name, and the corresponding value from +// `$mdc-theme-property-values` is returned. If this also fails, an error is thrown. // // This is mainly useful in situations where `mdc-theme-prop` cannot be used directly (e.g., `box-shadow`). // // Examples: // -// 1. mdc-theme-prop-value(primary) => "#3f51b5" +// 1. mdc-theme-prop-value(primary) => "#6200ee" // 2. mdc-theme-prop-value(blue) => "blue" // // NOTE: This function must be defined in _variables.scss instead of _functions.scss to avoid circular imports. -@function mdc-theme-prop-value($property) { - @if type-of($property) == "color" or $property == "currentColor" { - @return $property; +@function mdc-theme-prop-value($style) { + @if mdc-theme-is-valid-theme-prop-value_($style) { + @return $style; } - @if not map-has-key($mdc-theme-property-values, $property) { - @error "Invalid theme property: '#{$property}'. Choose one of: #{map-keys($mdc-theme-property-values)}"; + @if not map-has-key($mdc-theme-property-values, $style) { + @error "Invalid theme property: '#{$style}'. Choose one of: #{map-keys($mdc-theme-property-values)}"; } - @return map-get($mdc-theme-property-values, $property); + @return map-get($mdc-theme-property-values, $style); } // NOTE: This function must be defined in _variables.scss instead of _functions.scss to avoid circular imports. @@ -133,3 +133,8 @@ $mdc-theme-property-values: ( @return map-get($color-map-for-tone, $text-style); } + +// NOTE: This function is depended upon by mdc-theme-prop-value (above) and thus must be defined in this file. +@function mdc-theme-is-valid-theme-prop-value_($style) { + @return type-of($style) == "color" or $style == "currentColor" or str_slice($style, 1, 4) == "var("; +} From 608e46ad511468438ac2c2253d53c1a6dec20bbc Mon Sep 17 00:00:00 2001 From: Aaron Hudon Date: Tue, 17 Jul 2018 08:15:41 -0700 Subject: [PATCH 19/53] docs: Fix modal dialog secondary text (#2287) --- demos/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/index.html b/demos/index.html index 1ab46be89d9..c3867cd716e 100644 --- a/demos/index.html +++ b/demos/index.html @@ -70,7 +70,7 @@ Dialog - Secondary text + Implements a modal dialog window From 19955bfaa8b9e31b9f519246dcf501a647d49c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luiz=20Am=C3=A9rico?= Date: Tue, 17 Jul 2018 13:12:09 -0300 Subject: [PATCH 20/53] feat(auto-init): return initialized components (#1333) --- packages/mdc-auto-init/index.js | 3 +++ test/unit/mdc-auto-init/mdc-auto-init.test.js | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/mdc-auto-init/index.js b/packages/mdc-auto-init/index.js index 92880a0b52c..630d934abb6 100644 --- a/packages/mdc-auto-init/index.js +++ b/packages/mdc-auto-init/index.js @@ -37,6 +37,7 @@ function _emit(evtType, evtData, shouldBubble = false) { * Auto-initializes all mdc components on a page. */ export default function mdcAutoInit(root = document, warn = CONSOLE_WARN) { + const components = []; const nodes = root.querySelectorAll('[data-mdc-auto-init]'); for (let i = 0, node; (node = nodes[i]); i++) { const ctorName = node.dataset.mdcAutoInit; @@ -63,9 +64,11 @@ export default function mdcAutoInit(root = document, warn = CONSOLE_WARN) { enumerable: false, configurable: true, }); + components.push(component); } _emit('MDCAutoInit:End', {}); + return components; } mdcAutoInit.register = function(componentName, Ctor, warn = CONSOLE_WARN) { diff --git a/test/unit/mdc-auto-init/mdc-auto-init.test.js b/test/unit/mdc-auto-init/mdc-auto-init.test.js index 860b84448b6..6e0739488f9 100644 --- a/test/unit/mdc-auto-init/mdc-auto-init.test.js +++ b/test/unit/mdc-auto-init/mdc-auto-init.test.js @@ -138,3 +138,11 @@ test('#dispatches a MDCAutoInit:End event when all components are initialized - assert.isOk(evt !== null); assert.equal(evt.type, type); }); + +test('#returns the initialized components', () => { + const root = setupTest(); + const components = mdcAutoInit(root); + + assert.equal(components.length, 1); + assert.isOk(components[0] instanceof FakeComponent); +}); From 033a66c8bdd136827110757a11aeece9866182fd Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 17 Jul 2018 09:52:56 -0700 Subject: [PATCH 21/53] =?UTF-8?q?Update=20eslint=20to=20the=20latest=20ver?= =?UTF-8?q?sion=20=F0=9F=9A=80=20(#2990)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 505 +++++++++++++++++++++++++++++++++++++--------- package.json | 2 +- 2 files changed, 410 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88546a3ebfb..76c70b9d116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -457,20 +457,12 @@ } }, "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-4.1.1.tgz", + "integrity": "sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw==", "dev": true, "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } + "acorn": "^5.0.3" } }, "add-stream": { @@ -584,6 +576,12 @@ "string-width": "^2.0.0" } }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, "ansi-html": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", @@ -2356,6 +2354,12 @@ "integrity": "sha1-lCg191Dk7GGjCOYMLvjMEBEgLvw=", "dev": true }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -4937,13 +4941,12 @@ } }, "doctrine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", - "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "esutils": "^2.0.2" } }, "dom-events": { @@ -5416,74 +5419,147 @@ } }, "eslint": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.10.0.tgz", - "integrity": "sha512-MMVl8P/dYUFZEvolL8PYt7qc5LNdS2lwheq9BYa5Y07FblhcZqFyaUqlS8TW5QITGex21tV4Lk0a3fK8lsJIkA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.1.0.tgz", + "integrity": "sha512-DyH6JsoA1KzA5+OSWFjg56DFJT+sDLO0yokaPZ9qY0UEmYrPA1gEX/G1MnVkmRDsksG4H1foIVz2ZXXM3hHYvw==", "dev": true, "requires": { - "ajv": "^5.2.0", - "babel-code-frame": "^6.22.0", + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", - "debug": "^3.0.1", - "doctrine": "^2.0.0", - "eslint-scope": "^3.7.1", - "espree": "^3.5.1", - "esquery": "^1.0.0", - "estraverse": "^4.2.0", + "cross-spawn": "^6.0.5", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^2.0.0", "functional-red-black-tree": "^1.0.1", "glob": "^7.1.2", - "globals": "^9.17.0", + "globals": "^11.7.0", "ignore": "^3.3.3", "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", - "json-stable-stringify": "^1.0.1", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", + "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", "path-is-inside": "^1.0.2", "pluralize": "^7.0.0", "progress": "^2.0.0", + "regexpp": "^1.1.0", "require-uncached": "^1.0.3", - "semver": "^5.3.0", + "semver": "^5.5.0", + "string.prototype.matchall": "^2.0.0", "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "^4.0.1", - "text-table": "~0.2.0" + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "ajv": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz", + "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.1" + } }, - "ansi-styles": { + "ajv-keywords": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { - "color-convert": "^1.9.0" + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } } }, "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "^3.1.0", + "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, "debug": { @@ -5496,33 +5572,120 @@ } }, "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "globals": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", + "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", "dev": true }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" } }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", "dev": true }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -5530,15 +5693,43 @@ "dev": true, "requires": { "ansi-regex": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } } }, "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", + "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", "dev": true, "requires": { - "has-flag": "^2.0.0" + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" } } } @@ -5559,23 +5750,43 @@ } }, "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", + "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", "dev": true, "requires": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, "espree": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.1.tgz", - "integrity": "sha1-DJiLirRttTEAoZVK5LqZXd0n2H4=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-4.0.0.tgz", + "integrity": "sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg==", "dev": true, "requires": { - "acorn": "^5.1.1", - "acorn-jsx": "^3.0.0" + "acorn": "^5.6.0", + "acorn-jsx": "^4.1.1" + }, + "dependencies": { + "acorn": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", + "dev": true + } } }, "esprima": { @@ -5585,9 +5796,9 @@ "dev": true }, "esquery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", - "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", "dev": true, "requires": { "estraverse": "^4.0.0" @@ -5945,6 +6156,12 @@ "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", "dev": true }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -8367,6 +8584,12 @@ "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", "dev": true }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-to-string-tag-x": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", @@ -9302,13 +9525,10 @@ "dev": true }, "is-resolvable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", - "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", - "dev": true, - "requires": { - "tryit": "^1.0.1" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true }, "is-retry-allowed": { "version": "1.1.0", @@ -9644,6 +9864,12 @@ "jsonify": "~0.0.0" } }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -11521,6 +11747,12 @@ "dev": true, "optional": true }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==", + "dev": true + }, "no-case": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.1.tgz", @@ -13199,6 +13431,12 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", "dev": true }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, "protobufjs": { "version": "6.8.6", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.6.tgz", @@ -13726,6 +13964,21 @@ "is-primitive": "^2.0.0" } }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2" + } + }, + "regexpp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "dev": true + }, "regexpu-core": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", @@ -14282,6 +14535,15 @@ "rx-lite": "*" } }, + "rxjs": { + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, "safe-buffer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", @@ -15643,6 +15905,40 @@ "strip-ansi": "^3.0.0" } }, + "string.prototype.matchall": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-2.0.0.tgz", + "integrity": "sha512-WoZ+B2ypng1dp4iFLF2kmZlwwlE19gmjgKuhL1FJfDgCREWb3ye3SDVHSzLH6bxfnvYmkCxbzkmWcQZHA4P//Q==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "regexp.prototype.flags": "^1.2.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + } + } + }, "string.prototype.padend": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", @@ -16413,6 +16709,12 @@ "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", "dev": true }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + }, "syntax-error": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", @@ -16791,12 +17093,6 @@ } } }, - "tryit": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", - "dev": true - }, "tsscmp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", @@ -17075,6 +17371,23 @@ "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", diff --git a/package.json b/package.json index 13a61d6bee0..10af256b333 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "del-cli": "^1.0.0", "detect-port": "^1.2.3", "dom-events": "^0.1.1", - "eslint": "^4.10.0", + "eslint": "^5.1.0", "eslint-config-google": "^0.8.1", "eslint-plugin-mocha": "^5.0.0", "express": "^4.16.3", From d077ed618b8d0506e5d4a717665e3689f16fa784 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 17 Jul 2018 10:34:17 -0700 Subject: [PATCH 22/53] =?UTF-8?q?Update=20autoprefixer=20to=20the=20latest?= =?UTF-8?q?=20version=20=F0=9F=9A=80=20(#3099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 98 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 55 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76c70b9d116..f118a79e2ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -887,61 +887,39 @@ "dev": true }, "autoprefixer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.0.0.tgz", - "integrity": "sha512-XBEqAoESCyGu3daYmWcTC37Dwmjvs0y40UtUO3MMX+Pd/w7jwNFfUKNtxoMFu0u0wcotP+arDpU3JVH54UV79Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.0.0.tgz", + "integrity": "sha512-bMt9puCb+xk5ds1ghr1zzfJ09+SjOcseHCEawhMjibM5KfxkodW8PQMhhEnllyj4Cz3Yixy9A+/0De2VC9R+dQ==", "dev": true, "requires": { - "browserslist": "^3.0.0", - "caniuse-lite": "^1.0.30000808", + "browserslist": "^4.0.1", + "caniuse-lite": "^1.0.30000865", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", - "postcss": "^6.0.17", + "postcss": "^7.0.0", "postcss-value-parser": "^3.2.3" }, "dependencies": { "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, - "browserslist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.1.1.tgz", - "integrity": "sha512-zHGaPnTt70ywm+glR7uMJFZSl+ADGO67SgD2ae20L+Y3KJUeH4fVa89OkTqKCqAnXFE9mO4LTHBKBqKRlr7VNw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000809", - "electron-to-chromium": "^1.3.33" - } - }, - "caniuse-lite": { - "version": "1.0.30000810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000810.tgz", - "integrity": "sha512-/0Q00Oie9C72P8zQHtFvzmkrMC3oOFUnMWjCy5F2+BE8lzICm91hQPhh0+XIsAFPKOe2Dh3pKgbRmU3EKxfldA==", - "dev": true - }, "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", + "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", - "supports-color": "^5.2.0" + "supports-color": "^5.3.0" } }, - "electron-to-chromium": { - "version": "1.3.33", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz", - "integrity": "sha1-vwBwPWKnxlI4E2V4w1LWxcBCpUU=", - "dev": true - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -949,14 +927,14 @@ "dev": true }, "postcss": { - "version": "6.0.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.19.tgz", - "integrity": "sha512-f13HRz0HtVwVaEuW6J6cOUCBLFtymhgyLPV7t4QEk2UD3twRI9IluDcQNdzQdBpiixkXj2OmzejhhTbSbDxNTg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.0.tgz", + "integrity": "sha512-ACgL/mXREjfCothsspfbxdiXIQowQeEyW7TJgsvAgCK8igWa2ZNCsbGNGnasbEF1++L9xb7qYpo23Aa3rGmiCg==", "dev": true, "requires": { - "chalk": "^2.3.1", + "chalk": "^2.4.1", "source-map": "^0.6.1", - "supports-color": "^5.2.0" + "supports-color": "^5.4.0" } }, "source-map": { @@ -966,9 +944,9 @@ "dev": true }, "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -2062,6 +2040,17 @@ "pako": "~0.2.0" } }, + "browserslist": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.0.1.tgz", + "integrity": "sha512-QqiiIWchEIkney3wY53/huI7ZErouNAdvOkjorUALAwRcu3tEwOV3Sh6He0DnP38mz1JjBpCBb50jQBmaYuHPw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000865", + "electron-to-chromium": "^1.3.52", + "node-releases": "^1.0.0-alpha.10" + } + }, "btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", @@ -2256,6 +2245,12 @@ "map-obj": "^1.0.0" } }, + "caniuse-lite": { + "version": "1.0.30000865", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz", + "integrity": "sha512-vs79o1mOSKRGv/1pSkp4EXgl4ZviWeYReXw60XfacPU64uQWZwJT6vZNmxRF9O+6zu71sJwMxLK5JXxbzuVrLw==", + "dev": true + }, "canvas-prebuilt": { "version": "1.6.5-prerelease.1", "resolved": "https://registry.npmjs.org/canvas-prebuilt/-/canvas-prebuilt-1.6.5-prerelease.1.tgz", @@ -5091,6 +5086,12 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "electron-to-chromium": { + "version": "1.3.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz", + "integrity": "sha1-0tnxJwuko7lnuDHEDvcftNmrXOA=", + "dev": true + }, "elliptic": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", @@ -11936,6 +11937,15 @@ } } }, + "node-releases": { + "version": "1.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.0.0-alpha.10.tgz", + "integrity": "sha512-BSQrRgOfN6L/MoKIa7pRUc7dHvflCXMcqyTBvphixcSsgJTuUd24vAFONuNfVsuwTyz28S1HEc9XN6ZKylk4Hg==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, "node-sass": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", diff --git a/package.json b/package.json index 10af256b333..f48568caf6e 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@octokit/rest": "^15.9.4", "argparse": "^1.0.10", "ascii-table": "0.0.9", - "autoprefixer": "^8.0.0", + "autoprefixer": "^9.0.0", "babel-core": "^6.22.1", "babel-loader": "^7.0.0", "babel-plugin-transform-object-assign": "^6.8.0", From 87bb13d11dccc2cd832bd22e23ad72d044fd9e9b Mon Sep 17 00:00:00 2001 From: Will Ernest <34519388+williamernest@users.noreply.github.com> Date: Tue, 17 Jul 2018 11:20:22 -0700 Subject: [PATCH 23/53] chore: Remove unused extended-fab directory (#3110) --- packages/mdc-extended-fab/README.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 packages/mdc-extended-fab/README.md diff --git a/packages/mdc-extended-fab/README.md b/packages/mdc-extended-fab/README.md deleted file mode 100644 index ea83ac5a0d6..00000000000 --- a/packages/mdc-extended-fab/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Extended Floating Action Button - -The [Extended Floating Action Button component](https://material.io/go/design-extended-fab) is yet to be completed, please follow the [tracking issue](https://github.com/material-components/material-components-web/issues/2663) for more information. - From 1c29bd5a7f12aad5b1bfba7191502789942eb2db Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 12:14:52 -0700 Subject: [PATCH 24/53] chore(infrastructure): Warn if page content overflows mobile viewport (#3112) ### What it does - Detects when page content overflows a `.test-main--mobile-viewport` element - Displays a red warning element on the page and logs a descriptive error in the console - Moves font loading detection out of `SeleniumApi` into `fixture.js` - Fixes bug that prevented "Details" links from displaying in GitHub status checks when screenshot tests threw an error - Renames `.test-main--mobile` to `.test-main--mobile-viewport` ### Example output * Report page: https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/18_42_09_761/report/report.html ![overflow-screenshot](https://user-images.githubusercontent.com/409245/42838935-f539eb92-89b6-11e8-8679-134a3932801b.png) --- test/screenshot/lib/github-api.js | 2 +- test/screenshot/lib/selenium-api.js | 1 - test/screenshot/spec/fixture.js | 35 ++++++++++++++++++- test/screenshot/spec/fixture.scss | 19 +++++++++- .../classes/baseline-button-with-icons.html | 4 +-- .../baseline-button-without-icons.html | 4 +-- .../classes/baseline-link-with-icons.html | 4 +-- .../classes/baseline-link-without-icons.html | 4 +-- .../classes/dense-button-with-icons.html | 4 +-- .../classes/dense-button-without-icons.html | 4 +-- .../classes/dense-link-with-icons.html | 4 +-- .../classes/dense-link-without-icons.html | 4 +-- .../mixins/container-fill-color.html | 4 +-- .../spec/mdc-button/mixins/corner-radius.html | 4 +-- .../mdc-button/mixins/filled-accessible.html | 4 +-- .../mixins/horizontal-padding-baseline.html | 4 +-- .../mixins/horizontal-padding-dense.html | 4 +-- .../spec/mdc-button/mixins/icon-color.html | 4 +-- .../spec/mdc-button/mixins/ink-color.html | 4 +-- .../spec/mdc-button/mixins/stroke-color.html | 4 +-- .../spec/mdc-button/mixins/stroke-width.html | 4 +-- .../spec/mdc-fab/classes/baseline.html | 4 +-- .../spec/mdc-fab/classes/extended.html | 4 +-- .../screenshot/spec/mdc-fab/classes/mini.html | 4 +-- .../spec/mdc-fab/mixins/extended-padding.html | 4 +-- .../mdc-icon-button/classes/baseline.html | 4 +-- .../mdc-icon-button/mixins/icon-size.html | 4 +-- .../mdc-icon-button/mixins/ink-color.html | 4 +-- 28 files changed, 101 insertions(+), 52 deletions(-) diff --git a/test/screenshot/lib/github-api.js b/test/screenshot/lib/github-api.js index 9487ecbf3a1..9f07270d8d3 100644 --- a/test/screenshot/lib/github-api.js +++ b/test/screenshot/lib/github-api.js @@ -107,7 +107,7 @@ class GitHubApi { return await this.createStatus_({ state: GitHubApi.PullRequestState.ERROR, - target_url: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, + targetUrl: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, description: 'Error running screenshot tests', }); } diff --git a/test/screenshot/lib/selenium-api.js b/test/screenshot/lib/selenium-api.js index 8b53b4be94d..379b763aac0 100644 --- a/test/screenshot/lib/selenium-api.js +++ b/test/screenshot/lib/selenium-api.js @@ -464,7 +464,6 @@ class SeleniumApi { const fontTimeoutMs = isOnline ? SELENIUM_FONT_LOAD_WAIT_MS : 1; await driver.get(url); - await driver.executeScript('window.mdc.testFixture.attachFontObserver();'); await driver.wait(until.elementLocated(By.css('[data-fonts-loaded]')), fontTimeoutMs).catch(() => 0); if (delayMs > 0) { diff --git a/test/screenshot/spec/fixture.js b/test/screenshot/spec/fixture.js index a2d66135158..5fe350f97f3 100644 --- a/test/screenshot/spec/fixture.js +++ b/test/screenshot/spec/fixture.js @@ -19,7 +19,13 @@ window.mdc = window.mdc || {}; window.mdc.testFixture = { - attachFontObserver: function() { + onPageLoad: function() { + this.attachFontObserver_(); + this.measureMobileViewport_(); + }, + + /** @private */ + attachFontObserver_: function() { var fontsLoadedPromise = new Promise(function(resolve) { var robotoFont = new FontFaceObserver('Roboto'); var materialIconsFont = new FontFaceObserver('Material Icons'); @@ -38,4 +44,31 @@ window.mdc.testFixture = { document.body.setAttribute('data-fonts-loaded', ''); }); }, + + measureMobileViewport_() { + var mainEl = document.querySelector('.test-main--mobile-viewport'); + if (!mainEl) { + return; + } + + window.requestAnimationFrame(function() { + var setHeight = mainEl.offsetHeight; + mainEl.style.height = 'auto'; + var autoHeight = mainEl.offsetHeight; + mainEl.style.height = ''; + if (autoHeight > setHeight) { + mainEl.classList.add('test-main--overflowing'); + console.error(` +Page content overflows a mobile viewport! +Consider splitting this page into two separate pages. +If you are trying to create a test page for a fullscreen component like drawer or top-app-bar, +remove the 'test-main--mobile-viewport' class from the '
' element. + `.trim()); + } + }); + }, }; + +window.addEventListener('load', function() { + window.mdc.testFixture.onPageLoad(); +}); diff --git a/test/screenshot/spec/fixture.scss b/test/screenshot/spec/fixture.scss index 02d53f225a0..9a1cdd133cc 100644 --- a/test/screenshot/spec/fixture.scss +++ b/test/screenshot/spec/fixture.scss @@ -28,10 +28,11 @@ $test-grid-color: #dddddd; } .test-main { + position: relative; box-sizing: border-box; } -.test-main--mobile { +.test-main--mobile-viewport { width: 350px; // fits 2 columns of buttons within a Galaxy S7 viewport height: 630px; // fits 8 rows of buttons within a Galaxy S7 viewport margin: 5px 0 5px 5px; // Extra padding ensures that CBT's "chromeless" screenshots don't get cut off @@ -39,6 +40,22 @@ $test-grid-color: #dddddd; overflow: hidden; } +.test-main--overflowing::after { + display: block; + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 10px 20px; + background-color: rgba(255, 0, 0, .5); + color: white; + font-family: Roboto, sans-serif; + font-size: .9rem; + font-weight: bold; + text-shadow: 0 0 3px rgba(0, 0, 0, .5); + content: "ERROR: Content overflows mobile viewport!"; +} + .test-grid { display: flex; flex-wrap: wrap; diff --git a/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html index 0ca1951a0f0..c186053923f 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html @@ -19,13 +19,13 @@ Baseline Button Element With Icons - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html index 5b190085b95..9fe37d2abe1 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html @@ -19,13 +19,13 @@ Baseline Button Link With Icons - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html index 3ae0df07a76..e7b700a18c8 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html @@ -19,13 +19,13 @@ Baseline Button Link Without Icons - MDC Web Screenshot Test - + -
+
Link diff --git a/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html index b569702ea95..20166067319 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html @@ -19,13 +19,13 @@ Dense Button Element With Icons - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html index 7a19a8c0cbd..062ffe51916 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html @@ -19,13 +19,13 @@ Dense Button Link With Icons - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html index 8739f2a56a4..4cd38969c62 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html @@ -19,13 +19,13 @@ Dense Button Link Without Icons - MDC Web Screenshot Test - + -
+
Button diff --git a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html index a2fcb12efc7..1971f164060 100644 --- a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html +++ b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html @@ -19,14 +19,14 @@ container-fill-color Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/corner-radius.html b/test/screenshot/spec/mdc-button/mixins/corner-radius.html index 8d797a30d68..6be9fc91494 100644 --- a/test/screenshot/spec/mdc-button/mixins/corner-radius.html +++ b/test/screenshot/spec/mdc-button/mixins/corner-radius.html @@ -19,14 +19,14 @@ corner-radius Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html index 5475e3aa521..1470f8a2ae3 100644 --- a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html +++ b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html @@ -19,14 +19,14 @@ filled-accessible Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html index aba9a80b130..eb6d333e45a 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html @@ -19,14 +19,14 @@ horizontal-padding Baseline Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html index fa52b3cbb6f..019c05a6f8d 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html @@ -19,14 +19,14 @@ horizontal-padding Dense Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/icon-color.html b/test/screenshot/spec/mdc-button/mixins/icon-color.html index 5127d44d87f..31cfcd024b5 100644 --- a/test/screenshot/spec/mdc-button/mixins/icon-color.html +++ b/test/screenshot/spec/mdc-button/mixins/icon-color.html @@ -19,14 +19,14 @@ icon-color Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-color.html b/test/screenshot/spec/mdc-button/mixins/stroke-color.html index cb6700c86f1..29fcfd974a0 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-color.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-color.html @@ -19,14 +19,14 @@ outline-color Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-width.html b/test/screenshot/spec/mdc-button/mixins/stroke-width.html index 40d8e39df31..820429d2f58 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-width.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-width.html @@ -19,14 +19,14 @@ outline-width Button Mixin - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-fab/classes/baseline.html b/test/screenshot/spec/mdc-fab/classes/baseline.html index 97b34559dcb..ee5c887bc75 100644 --- a/test/screenshot/spec/mdc-fab/classes/baseline.html +++ b/test/screenshot/spec/mdc-fab/classes/baseline.html @@ -19,13 +19,13 @@ Baseline FAB (Floating Action Button) - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html index b65a70b07f4..938fe705221 100644 --- a/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html @@ -20,13 +20,13 @@ icon-size Icon Button - MDC Web Screenshot Test - + -
+
diff --git a/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html index d41b53bb16b..29529130c79 100644 --- a/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html @@ -20,13 +20,13 @@ ink-color Icon Button - MDC Web Screenshot Test - + -
+
From 19e3d7f0d24797020f2813858cf08380232e2445 Mon Sep 17 00:00:00 2001 From: Bonnie Zhou Date: Tue, 17 Jul 2018 14:00:29 -0700 Subject: [PATCH 25/53] fix(chips): Remove color change from selected filter chips (#3093) --- packages/mdc-chips/chip/mdc-chip.scss | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/mdc-chips/chip/mdc-chip.scss b/packages/mdc-chips/chip/mdc-chip.scss index afee7a2dd3a..03db2ce648f 100644 --- a/packages/mdc-chips/chip/mdc-chip.scss +++ b/packages/mdc-chips/chip/mdc-chip.scss @@ -30,7 +30,6 @@ @include mdc-chip-corner-radius($mdc-chip-border-radius-default); @include mdc-chip-fill-color($mdc-chip-fill-color-default); @include mdc-chip-ink-color($mdc-chip-ink-color-default); - @include mdc-chip-selected-ink-color(primary); @include mdc-chip-leading-icon-color($mdc-chip-icon-color); @include mdc-chip-trailing-icon-color($mdc-chip-icon-color); @include mdc-chip-leading-icon-size($mdc-chip-leading-icon-size); @@ -61,10 +60,6 @@ opacity: 0; } -.mdc-chip--selected { - @include mdc-theme-prop(background-color, surface); -} - .mdc-chip__text { white-space: nowrap; } @@ -101,6 +96,18 @@ stroke-dashoffset: 0; } +// Change color of selected choice chips + +.mdc-chip-set--choice { + .mdc-chip { + @include mdc-chip-selected-ink-color(primary); + } + + .mdc-chip--selected { + @include mdc-theme-prop(background-color, surface); + } +} + // Add leading checkmark to filter chips with no leading icon .mdc-chip__checkmark-svg { From b8237595e412750c47b84cf02849c255ec34257b Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 16:48:08 -0700 Subject: [PATCH 26/53] chore(infrastructure): Support fullpage screenshots (#3116) ### What it does - Adds support for fullpage screenshots without slowing down tests - Each additional call to `driver.manage().window().setRect({x, y, width, height})` requires a network round trip, which nearly doubles the test time - To avoid excessive calls to `setRect()`, I only call it once for each size - Adds test pages for `mdc-drawer`: - Classes: `mdc-drawer--temporary`, `mdc-drawer--persistent`, `mdc-drawer--permanent` - Mixins: `mdc-drawer-fill-color`, `mdc-drawer-fill-color-accessible`, and `mdc-drawer-ink-color` - Changes mobile viewport dimensions. They're now small enough to fit mobile devices and desktop Chrome driver running on a 1366x768 screen. - Compiles JS files in `spec/` down to ES5 - Updates `test/screenshot/webpack.config.js` - Ensures that browser icons are properly sized even if CSS doesn't load - Refines logging output during capture (color-coded and column-aligned) ### Example output * Diff report: https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/report/report.html --- test/screenshot/golden.json | 246 +++++++++++------- test/screenshot/lib/image-cropper.js | 4 +- test/screenshot/lib/report-writer.js | 2 +- test/screenshot/lib/selenium-api.js | 139 +++++++--- test/screenshot/report/_collection.hbs | 1 + test/screenshot/report/_metadata.hbs | 2 +- test/screenshot/report/report.hbs | 2 +- test/screenshot/report/report.scss | 2 - test/screenshot/spec/fixture.js | 30 +-- test/screenshot/spec/fixture.scss | 2 +- .../classes/baseline-button-with-icons.html | 2 +- .../baseline-button-without-icons.html | 2 +- .../classes/baseline-link-with-icons.html | 2 +- .../classes/baseline-link-without-icons.html | 2 +- .../classes/dense-button-with-icons.html | 2 +- .../classes/dense-button-without-icons.html | 2 +- .../classes/dense-link-with-icons.html | 2 +- .../classes/dense-link-without-icons.html | 2 +- .../mixins/container-fill-color.html | 2 +- .../spec/mdc-button/mixins/corner-radius.html | 2 +- .../mdc-button/mixins/filled-accessible.html | 2 +- .../mixins/horizontal-padding-baseline.html | 2 +- .../mixins/horizontal-padding-dense.html | 2 +- .../spec/mdc-button/mixins/icon-color.html | 2 +- .../spec/mdc-button/mixins/ink-color.html | 2 +- .../spec/mdc-button/mixins/stroke-color.html | 2 +- .../spec/mdc-button/mixins/stroke-width.html | 2 +- .../spec/mdc-drawer/classes/permanent.html | 132 ++++++++++ .../spec/mdc-drawer/classes/persistent.html | 135 ++++++++++ .../spec/mdc-drawer/classes/temporary.html | 135 ++++++++++ test/screenshot/spec/mdc-drawer/fixture.js | 36 +++ test/screenshot/spec/mdc-drawer/fixture.scss | 43 +++ .../mixins/fill-color-accessible.html | 132 ++++++++++ .../spec/mdc-drawer/mixins/fill-color.html | 132 ++++++++++ .../spec/mdc-drawer/mixins/ink-color.html | 132 ++++++++++ .../spec/mdc-fab/classes/baseline.html | 2 +- .../spec/mdc-fab/classes/extended.html | 2 +- .../screenshot/spec/mdc-fab/classes/mini.html | 2 +- .../spec/mdc-fab/mixins/extended-padding.html | 2 +- .../mdc-icon-button/classes/baseline.html | 2 +- .../mdc-icon-button/mixins/icon-size.html | 2 +- .../mdc-icon-button/mixins/ink-color.html | 2 +- test/screenshot/webpack.config.js | 82 +++++- 43 files changed, 1246 insertions(+), 191 deletions(-) create mode 100644 test/screenshot/spec/mdc-drawer/classes/permanent.html create mode 100644 test/screenshot/spec/mdc-drawer/classes/persistent.html create mode 100644 test/screenshot/spec/mdc-drawer/classes/temporary.html create mode 100644 test/screenshot/spec/mdc-drawer/fixture.js create mode 100644 test/screenshot/spec/mdc-drawer/fixture.scss create mode 100644 test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html create mode 100644 test/screenshot/spec/mdc-drawer/mixins/fill-color.html create mode 100644 test/screenshot/spec/mdc-drawer/mixins/ink-color.html diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index ed8f283e3c6..f4a4911aaa5 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -2,217 +2,271 @@ "spec/mdc-button/classes/baseline-button-with-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-with-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/baseline-button-without-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-button-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-button-without-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/baseline-link-with-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-with-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/baseline-link-without-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/baseline-link-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/baseline-link-without-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/dense-button-with-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-with-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/dense-button-without-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-button-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-button-without-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/dense-link-with-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-with-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-with-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-with-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-with-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-with-icons.html.windows_ie_11.png" } }, "spec/mdc-button/classes/dense-link-without-icons.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/classes/dense-link-without-icons.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-without-icons.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-without-icons.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-without-icons.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/classes/dense-link-without-icons.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/container-fill-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/container-fill-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/container-fill-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/container-fill-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/container-fill-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/container-fill-color.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/corner-radius.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/corner-radius.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/corner-radius.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/corner-radius.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/corner-radius.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/corner-radius.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/filled-accessible.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/filled-accessible.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/filled-accessible.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/filled-accessible.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/filled-accessible.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/filled-accessible.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/horizontal-padding-baseline.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-baseline.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/horizontal-padding-dense.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/horizontal-padding-dense.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/icon-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/icon-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/icon-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/icon-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/icon-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/icon-color.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/ink-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/ink-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/ink-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/ink-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/ink-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/ink-color.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/stroke-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-color.html.windows_ie_11.png" } }, "spec/mdc-button/mixins/stroke-width.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-button/mixins/stroke-width.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-width.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-width.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-width.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-width.html.windows_ie_11.png" + } + }, + "spec/mdc-drawer/classes/permanent.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/permanent.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_ie_11.png" + } + }, + "spec/mdc-drawer/classes/persistent.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/persistent.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_ie_11.png" + } + }, + "spec/mdc-drawer/classes/temporary.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/temporary.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_ie_11.png" + } + }, + "spec/mdc-drawer/mixins/fill-color-accessible.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/mixins/fill-color-accessible.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_ie_11.png" + } + }, + "spec/mdc-drawer/mixins/fill-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/mixins/fill-color.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_ie_11.png" + } + }, + "spec/mdc-drawer/mixins/ink-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/mixins/ink-color.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_ie_11.png" } }, "spec/mdc-fab/classes/baseline.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/baseline.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/baseline.html.windows_ie_11.png" } }, "spec/mdc-fab/classes/extended.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/extended.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/extended.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/extended.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/extended.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/extended.html.windows_ie_11.png" } }, "spec/mdc-fab/classes/mini.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/classes/mini.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/mini.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/mini.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/mini.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/classes/mini.html.windows_ie_11.png" } }, "spec/mdc-fab/mixins/extended-padding.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-fab/mixins/extended-padding.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/mixins/extended-padding.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/mixins/extended-padding.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/mixins/extended-padding.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-fab/mixins/extended-padding.html.windows_ie_11.png" } }, "spec/mdc-icon-button/classes/baseline.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/classes/baseline.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/classes/baseline.html.windows_ie_11.png" } }, "spec/mdc-icon-button/mixins/icon-size.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/icon-size.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/icon-size.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/icon-size.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/icon-size.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/icon-size.html.windows_ie_11.png" } }, "spec/mdc-icon-button/mixins/ink-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/07_29_10_421/spec/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" } } } diff --git a/test/screenshot/lib/image-cropper.js b/test/screenshot/lib/image-cropper.js index 162fa0f4f15..473b6bf69f9 100644 --- a/test/screenshot/lib/image-cropper.js +++ b/test/screenshot/lib/image-cropper.js @@ -23,7 +23,7 @@ const TRIM_COLOR_CSS_VALUE = '#abc123'; // Value must match `$test-trim-color` i * match the trim color in order for that row or column to be cropped out. * @type {number} */ -const TRIM_COLOR_PIXEL_MATCH_PCT = 0.05; +const TRIM_COLOR_PIXEL_MATCH_FRACTION = 0.05; /** * Maximum distance (0 to 255 inclusive) that a pixel's R, G, and B color channels can be from the corresponding @@ -147,7 +147,7 @@ class ImageCropper { let foundTrimColor = false; for (const [rowIndex, row] of rows.entries()) { - const isTrimColor = this.getMatchPercentage_(row) >= TRIM_COLOR_PIXEL_MATCH_PCT; + const isTrimColor = this.getMatchPercentage_(row) >= TRIM_COLOR_PIXEL_MATCH_FRACTION; if (isTrimColor) { foundTrimColor = true; diff --git a/test/screenshot/lib/report-writer.js b/test/screenshot/lib/report-writer.js index 378d9ace7df..037777b5b6b 100644 --- a/test/screenshot/lib/report-writer.js +++ b/test/screenshot/lib/report-writer.js @@ -393,7 +393,7 @@ class ReportWriter { function getIconHtml(userAgent) { const title = userAgent.navigator ? userAgent.navigator.full_name : userAgent.alias; return ` - + `.trim(); } diff --git a/test/screenshot/lib/selenium-api.js b/test/screenshot/lib/selenium-api.js index 379b763aac0..688bd4d97f9 100644 --- a/test/screenshot/lib/selenium-api.js +++ b/test/screenshot/lib/selenium-api.js @@ -18,6 +18,7 @@ const Jimp = require('jimp'); const UserAgentParser = require('useragent'); +const colors = require('colors/safe'); const path = require('path'); const mdcProto = require('../proto/mdc.pb').mdc.proto; @@ -25,7 +26,7 @@ const seleniumProto = require('../proto/selenium.pb').selenium.proto; const {Screenshot, TestFile, UserAgent} = mdcProto; const {CaptureState} = Screenshot; -const {BrowserVendorType, FormFactorType, Navigator} = UserAgent; +const {BrowserVendorType, Navigator} = UserAgent; const {RawCapabilities} = seleniumProto; const CbtApi = require('./cbt-api'); @@ -39,6 +40,25 @@ const {Browser, Builder, By, until} = require('selenium-webdriver'); const {CBT_CONCURRENCY_POLL_INTERVAL_MS, CBT_CONCURRENCY_MAX_WAIT_MS} = Constants; const {SELENIUM_FONT_LOAD_WAIT_MS} = Constants; +/** + * @typedef {{ + * name: string, + * color: !CliColor, + * }} CliStatus + */ +const CliStatuses = { + ACTIVE: {name: 'Active', color: colors.bold.cyan}, + QUEUED: {name: 'Queued', color: colors.cyan}, + STARTING: {name: 'Starting', color: colors.green}, + STARTED: {name: 'Started', color: colors.bold.green}, + GET: {name: 'Get', color: colors.bold.white}, + CROP: {name: 'Crop', color: colors.white}, + RETRY: {name: 'Retry', color: colors.red}, + FINISHED: {name: 'Finished', color: colors.bold.green}, + FAILED: {name: 'Failed', color: colors.bold.red}, + QUITTING: {name: 'Quitting', color: colors.white}, +}; + class SeleniumApi { constructor() { /** @@ -93,8 +113,8 @@ class SeleniumApi { const queuedUserAgentAliases = queuedUserAgents.map((ua) => ua.alias); const runningUserAgentLoggable = getLoggableAliases(runningUserAgentAliases); const queuedUserAgentLoggable = getLoggableAliases(queuedUserAgentAliases); - console.log('Running user agents:', runningUserAgentLoggable); - console.log('Queued user agents:', queuedUserAgentLoggable); + this.logStatus_(CliStatuses.ACTIVE, runningUserAgentLoggable); + this.logStatus_(CliStatuses.QUEUED, queuedUserAgentLoggable); await this.captureAllPagesInAllBrowsers_({reportData, userAgents: runningUserAgents}); } @@ -130,21 +150,21 @@ class SeleniumApi { const seleniumSessionId = session.getId(); let changedScreenshots; - const logResult = (verb) => { + const logResult = (status) => { /* eslint-disable camelcase */ const {os_name, os_version, browser_name, browser_version} = userAgent.navigator; - console.log(`${verb} ${browser_name} ${browser_version} on ${os_name} ${os_version}!`); + this.logStatus_(status, `${browser_name} ${browser_version} on ${os_name} ${os_version}!`); /* eslint-enable camelcase */ }; try { changedScreenshots = (await this.driveBrowser_({reportData, userAgent, driver})).changedScreenshots; - logResult('Finished'); + logResult(CliStatuses.FINISHED); } catch (err) { - logResult('Failed'); + logResult(CliStatuses.FAILED); throw err; } finally { - logResult('Quitting'); + logResult(CliStatuses.QUITTING); await driver.quit(); } @@ -218,7 +238,7 @@ class SeleniumApi { driverBuilder.usingServer(this.cbtApi_.getSeleniumServerUrl()); } - console.log(`Starting ${userAgent.alias}...`); + this.logStatus_(CliStatuses.STARTING, `${userAgent.alias}...`); /** @type {!IWebDriver} */ const driver = await driverBuilder.build(); @@ -237,7 +257,7 @@ class SeleniumApi { userAgent.browser_version_value = browser_version; userAgent.image_filename_suffix = this.getImageFileNameSuffix_(userAgent); - console.log(`Started ${browser_name} ${browser_version} on ${os_name} ${os_version}!`); + this.logStatus_(CliStatuses.STARTED, `${browser_name} ${browser_version} on ${os_name} ${os_version}!`); /* eslint-enable camelcase */ return driver; @@ -338,37 +358,36 @@ class SeleniumApi { * @private */ async driveBrowser_({reportData, userAgent, driver}) { - if (userAgent.form_factor_type === FormFactorType.DESKTOP) { - /** @type {!Window} */ - const window = driver.manage().window(); - - // Resize the browser window to roughly match a mobile browser. - // This reduces the byte size of the screenshot image, which speeds up the test significantly. - // TODO(acdvorak): Set this value dynamically - await window.setRect({x: 0, y: 0, width: 400, height: 800}).catch(() => undefined); - } - const meta = reportData.meta; /** @type {!Array} */ const changedScreenshots = []; /** @type {!Array} */ const unchangedScreenshots = []; /** @type {!Array} */ - const screenshotQueue = reportData.screenshots.runnable_screenshot_browser_map[userAgent.alias].screenshots; + const screenshotQueueAll = reportData.screenshots.runnable_screenshot_browser_map[userAgent.alias].screenshots; - for (const screenshot of screenshotQueue) { - screenshot.capture_state = CaptureState.RUNNING; + const screenshotQueues = [ + [true, screenshotQueueAll.filter((screenshot) => this.isSmallComponent_(screenshot.html_file_path))], + [false, screenshotQueueAll.filter((screenshot) => !this.isSmallComponent_(screenshot.html_file_path))], + ]; - const diffImageResult = await this.takeScreenshotWithRetries_({driver, userAgent, screenshot, meta}); + for (const [isSmallComponent, screenshotQueue] of screenshotQueues) { + await this.resizeWindow_({driver, isSmallComponent}); - screenshot.capture_state = CaptureState.DIFFED; - screenshot.diff_image_result = diffImageResult; - screenshot.diff_image_file = diffImageResult.diff_image_file; + for (const screenshot of screenshotQueue) { + screenshot.capture_state = CaptureState.RUNNING; - if (diffImageResult.has_changed) { - changedScreenshots.push(screenshot); - } else { - unchangedScreenshots.push(screenshot); + const diffImageResult = await this.takeScreenshotWithRetries_({driver, userAgent, screenshot, meta}); + + screenshot.capture_state = CaptureState.DIFFED; + screenshot.diff_image_result = diffImageResult; + screenshot.diff_image_file = diffImageResult.diff_image_file; + + if (diffImageResult.has_changed) { + changedScreenshots.push(screenshot); + } else { + unchangedScreenshots.push(screenshot); + } } } @@ -378,6 +397,37 @@ class SeleniumApi { return {changedScreenshots, unchangedScreenshots}; } + /** + * @param {string} url + * @return {boolean} + * @private + */ + isSmallComponent_(url) { + // TODO(acdvorak): Find a better way to do this + const smallComponentNames = [ + 'animation', 'button', 'card', 'checkbox', 'chips', 'elevation', 'fab', 'icon-button', 'icon-toggle', + 'list', 'menu', 'radio', 'ripple', 'select', 'switch', 'textfield', 'theme', 'tooltip', 'typography', + ]; + return new RegExp(`/mdc-(${smallComponentNames.join('|')})/`).test(url); + } + + /** + * @param {!IWebDriver} driver + * @param {boolean} isSmallComponent + * @return {!Promise<{x: number, y: number, width: number, height: number}>} + * @private + */ + async resizeWindow_({driver, isSmallComponent}) { + /** @type {!Window} */ + const window = driver.manage().window(); + const rect = isSmallComponent + ? {x: 0, y: 0, width: 400, height: 768} + : {x: 0, y: 0, width: 1366, height: 768} + ; + await window.setRect(rect).catch(() => undefined); + return rect; + } + /** * @param {!IWebDriver} driver * @param {!mdc.proto.UserAgent} userAgent @@ -387,7 +437,6 @@ class SeleniumApi { * @private */ async takeScreenshotWithRetries_({driver, userAgent, screenshot, meta}) { - const htmlFilePath = screenshot.html_file_path; let delayMs = 0; /** @type {?mdc.proto.DiffImageResult} */ @@ -399,12 +448,12 @@ class SeleniumApi { while (screenshot.retry_count <= screenshot.max_retries) { if (screenshot.retry_count > 0) { const {width, height} = diffImageResult.diff_image_dimensions; - const retryMsg = `Retrying ${htmlFilePath} > ${userAgent.alias}`; + const whichMsg = `${screenshot.actual_html_file.public_url} > ${userAgent.alias}`; const countMsg = `attempt ${screenshot.retry_count} of ${screenshot.max_retries}`; const pixelMsg = `${changedPixelCount.toLocaleString()} pixels differed`; const deltaMsg = `${diffImageResult.changed_pixel_percentage}% of ${width}x${height}`; - console.warn(`${retryMsg} (${countMsg}). ${pixelMsg} (${deltaMsg})`); - delayMs = 1000; + this.logStatus_(CliStatuses.RETRY, `${whichMsg} (${countMsg}). ${pixelMsg} (${deltaMsg})`); + delayMs = 500; } screenshot.actual_image_file = await this.takeScreenshotWithoutRetries_({ @@ -458,10 +507,10 @@ class SeleniumApi { * @private */ async capturePageAsPng_({driver, userAgent, url, delayMs = 0}) { - console.log(`GET ${url} > ${userAgent.alias}...`); + this.logStatus_(CliStatuses.GET, `${url} > ${userAgent.alias}...`); const isOnline = await this.cli_.isOnline(); - const fontTimeoutMs = isOnline ? SELENIUM_FONT_LOAD_WAIT_MS : 1; + const fontTimeoutMs = isOnline ? SELENIUM_FONT_LOAD_WAIT_MS : 500; await driver.get(url); await driver.wait(until.elementLocated(By.css('[data-fonts-loaded]')), fontTimeoutMs).catch(() => 0); @@ -479,9 +528,9 @@ class SeleniumApi { const {width: uncroppedWidth, height: uncroppedHeight} = uncroppedJimpImage.bitmap; const {width: croppedWidth, height: croppedHeight} = croppedJimpImage.bitmap; - console.info(` -Cropped ${url} > ${userAgent.alias} image from ${uncroppedWidth}x${uncroppedHeight} to ${croppedWidth}x${croppedHeight} -`.trim()); + const message = `${url} > ${userAgent.alias} screenshot from ` + + `${uncroppedWidth}x${uncroppedHeight} to ${croppedWidth}x${croppedHeight}`; + this.logStatus_(CliStatuses.CROP, message); return croppedImageBuffer; } @@ -494,6 +543,16 @@ Cropped ${url} > ${userAgent.alias} image from ${uncroppedWidth}x${uncroppedHeig async sleep_(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } + + /** + * @param {!CliStatus} status + * @param {*} args + * @private + */ + logStatus_(status, ...args) { + const maxWidth = Object.values(CliStatuses).map((status) => status.name.length).sort().reverse()[0]; + console.log(status.color(status.name.toUpperCase().padStart(maxWidth, ' ')) + ':', ...args); + } } module.exports = SeleniumApi; diff --git a/test/screenshot/report/_collection.hbs b/test/screenshot/report/_collection.hbs index 66ff1e280dd..a7c8b4e4c60 100644 --- a/test/screenshot/report/_collection.hbs +++ b/test/screenshot/report/_collection.hbs @@ -82,6 +82,7 @@ user_agent.alias }}{{/createCheckboxElement}} diff --git a/test/screenshot/report/_metadata.hbs b/test/screenshot/report/_metadata.hbs index 0dd273702f1..650ec7cae23 100644 --- a/test/screenshot/report/_metadata.hbs +++ b/test/screenshot/report/_metadata.hbs @@ -67,7 +67,7 @@ Author OS: - + {{meta.host_os_name}} diff --git a/test/screenshot/report/report.hbs b/test/screenshot/report/report.hbs index 565a1192e35..785b2659239 100644 --- a/test/screenshot/report/report.hbs +++ b/test/screenshot/report/report.hbs @@ -20,6 +20,6 @@ - + diff --git a/test/screenshot/report/report.scss b/test/screenshot/report/report.scss index 0fe5d3ee15f..04621515169 100644 --- a/test/screenshot/report/report.scss +++ b/test/screenshot/report/report.scss @@ -159,8 +159,6 @@ th { */ .report-user-agent__icon { - width: 16px; - height: 16px; vertical-align: middle; } diff --git a/test/screenshot/spec/fixture.js b/test/screenshot/spec/fixture.js index 5fe350f97f3..15e1f2ae7d0 100644 --- a/test/screenshot/spec/fixture.js +++ b/test/screenshot/spec/fixture.js @@ -14,21 +14,19 @@ * limitations under the License. */ -/* eslint-disable no-var */ - window.mdc = window.mdc || {}; window.mdc.testFixture = { - onPageLoad: function() { + onPageLoad() { this.attachFontObserver_(); this.measureMobileViewport_(); }, /** @private */ - attachFontObserver_: function() { - var fontsLoadedPromise = new Promise(function(resolve) { - var robotoFont = new FontFaceObserver('Roboto'); - var materialIconsFont = new FontFaceObserver('Material Icons'); + attachFontObserver_() { + const fontsLoadedPromise = new Promise((resolve) => { + const robotoFont = new FontFaceObserver('Roboto'); + const materialIconsFont = new FontFaceObserver('Material Icons'); // The `load()` method accepts an optional string of text to ensure that those specific glyphs are available. // For the Material Icons font, we need to pass it one of the icon names. @@ -36,26 +34,28 @@ window.mdc.testFixture = { resolve(); }); - setTimeout(function() { + setTimeout(() => { resolve(); }, 3000); // TODO(acdvorak): Create a constant for font loading timeout values }); - fontsLoadedPromise.then(function() { + + fontsLoadedPromise.then(() => { document.body.setAttribute('data-fonts-loaded', ''); }); }, measureMobileViewport_() { - var mainEl = document.querySelector('.test-main--mobile-viewport'); - if (!mainEl) { + const mainEl = document.querySelector('.test-main'); + if (!mainEl || !mainEl.classList.contains('test-main--mobile-viewport')) { return; } - window.requestAnimationFrame(function() { - var setHeight = mainEl.offsetHeight; + window.requestAnimationFrame(() => { + const setHeight = mainEl.offsetHeight; mainEl.style.height = 'auto'; - var autoHeight = mainEl.offsetHeight; + const autoHeight = mainEl.offsetHeight; mainEl.style.height = ''; + if (autoHeight > setHeight) { mainEl.classList.add('test-main--overflowing'); console.error(` @@ -69,6 +69,6 @@ remove the 'test-main--mobile-viewport' class from the '
}, }; -window.addEventListener('load', function() { +window.addEventListener('load', () => { window.mdc.testFixture.onPageLoad(); }); diff --git a/test/screenshot/spec/fixture.scss b/test/screenshot/spec/fixture.scss index 9a1cdd133cc..b2b6ffd6515 100644 --- a/test/screenshot/spec/fixture.scss +++ b/test/screenshot/spec/fixture.scss @@ -34,7 +34,7 @@ $test-grid-color: #dddddd; .test-main--mobile-viewport { width: 350px; // fits 2 columns of buttons within a Galaxy S7 viewport - height: 630px; // fits 8 rows of buttons within a Galaxy S7 viewport + height: 590px; // fits 8 rows of buttons within a Galaxy S7 viewport margin: 5px 0 5px 5px; // Extra padding ensures that CBT's "chromeless" screenshots don't get cut off border: 1px solid $test-trim-color; overflow: hidden; diff --git a/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html index c186053923f..2f8e45ec535 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-button-with-icons.html @@ -161,6 +161,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html index c091a0e74dd..919e08bb60b 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-button-without-icons.html @@ -72,6 +72,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html index 9fe37d2abe1..589fdb8faf7 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-with-icons.html @@ -97,6 +97,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html index e7b700a18c8..a6fc7a758c3 100644 --- a/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/baseline-link-without-icons.html @@ -59,6 +59,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html index 20166067319..e2bf0599a7c 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-button-with-icons.html @@ -177,6 +177,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html b/test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html index b8349f2e8e9..373b2b5ca69 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-button-without-icons.html @@ -72,6 +72,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html index 062ffe51916..907a60fe354 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-with-icons.html @@ -105,6 +105,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html index 4cd38969c62..992785d7537 100644 --- a/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html +++ b/test/screenshot/spec/mdc-button/classes/dense-link-without-icons.html @@ -59,6 +59,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html index 1971f164060..64162071d2c 100644 --- a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html +++ b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html @@ -44,6 +44,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/corner-radius.html b/test/screenshot/spec/mdc-button/mixins/corner-radius.html index 6be9fc91494..aa080ddd369 100644 --- a/test/screenshot/spec/mdc-button/mixins/corner-radius.html +++ b/test/screenshot/spec/mdc-button/mixins/corner-radius.html @@ -59,6 +59,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html index 1470f8a2ae3..0bfdf3931e9 100644 --- a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html +++ b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html @@ -71,6 +71,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html index eb6d333e45a..ca042b9f4ea 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html @@ -86,6 +86,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html index 019c05a6f8d..a3f10d6c8be 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html @@ -86,6 +86,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/icon-color.html b/test/screenshot/spec/mdc-button/mixins/icon-color.html index 31cfcd024b5..26146492ebe 100644 --- a/test/screenshot/spec/mdc-button/mixins/icon-color.html +++ b/test/screenshot/spec/mdc-button/mixins/icon-color.html @@ -95,6 +95,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/ink-color.html b/test/screenshot/spec/mdc-button/mixins/ink-color.html index 8f769bb8f50..e6f36f464ad 100644 --- a/test/screenshot/spec/mdc-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-button/mixins/ink-color.html @@ -107,6 +107,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-color.html b/test/screenshot/spec/mdc-button/mixins/stroke-color.html index 29fcfd974a0..de1c6c7b9d5 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-color.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-color.html @@ -44,6 +44,6 @@ - + diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-width.html b/test/screenshot/spec/mdc-button/mixins/stroke-width.html index 820429d2f58..f94439b64c3 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-width.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-width.html @@ -107,6 +107,6 @@ - + diff --git a/test/screenshot/spec/mdc-drawer/classes/permanent.html b/test/screenshot/spec/mdc-drawer/classes/permanent.html new file mode 100644 index 00000000000..fc3bbba7455 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/classes/permanent.html @@ -0,0 +1,132 @@ + + + + + + Permanent Drawer - MDC Web Screenshot Test + + + + + + + + + +
+ + +
+
+
+
+ Permanent Drawer +
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+
+
+
+ + + + + + + + diff --git a/test/screenshot/spec/mdc-drawer/classes/persistent.html b/test/screenshot/spec/mdc-drawer/classes/persistent.html new file mode 100644 index 00000000000..158a8d3e346 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/classes/persistent.html @@ -0,0 +1,135 @@ + + + + + + Persistent Drawer - MDC Web Screenshot Test + + + + + + + + + +
+ + +
+
+
+
+ + Persistent Drawer +
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+
+
+
+ + + + + + + + + + diff --git a/test/screenshot/spec/mdc-drawer/classes/temporary.html b/test/screenshot/spec/mdc-drawer/classes/temporary.html new file mode 100644 index 00000000000..f5ed276b3af --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/classes/temporary.html @@ -0,0 +1,135 @@ + + + + + + Temporary Drawer - MDC Web Screenshot Test + + + + + + + + + +
+ + +
+
+
+
+ + Temporary Drawer +
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+
+
+
+ + + + + + + + + + diff --git a/test/screenshot/spec/mdc-drawer/fixture.js b/test/screenshot/spec/mdc-drawer/fixture.js new file mode 100644 index 00000000000..6954f2eca62 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/fixture.js @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const temporaryDrawerEl = document.querySelector('.mdc-drawer--temporary'); +const persistentDrawerEl = document.querySelector('.mdc-drawer--persistent'); + +if (temporaryDrawerEl) { + const MDCTemporaryDrawer = mdc.drawer.MDCTemporaryDrawer; + const temporaryDrawer = new MDCTemporaryDrawer(temporaryDrawerEl); + + document.querySelector('#test-drawer-menu-button').addEventListener('click', () => { + temporaryDrawer.open = !temporaryDrawer.open; + }); +} + +if (persistentDrawerEl) { + const MDCPersistentDrawer = mdc.drawer.MDCPersistentDrawer; + const persistentDrawer = new MDCPersistentDrawer(persistentDrawerEl); + + document.querySelector('#test-drawer-menu-button').addEventListener('click', () => { + persistentDrawer.open = !persistentDrawer.open; + }); +} diff --git a/test/screenshot/spec/mdc-drawer/fixture.scss b/test/screenshot/spec/mdc-drawer/fixture.scss new file mode 100644 index 00000000000..374aba1f664 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/fixture.scss @@ -0,0 +1,43 @@ +// +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import "../../../../packages/mdc-drawer/mixins"; +@import "../../../../packages/mdc-list/mixins"; +@import "../../../../packages/mdc-theme/color-palette"; + +$custom-drawer-color: $material-color-orange-900; + +.test-main--drawer { + display: flex; + flex-direction: row; +} + +.test-drawer-column { + display: flex; + flex-direction: column; +} + +.custom-drawer--fill-color { + @include mdc-drawer-fill-color($custom-drawer-color); +} + +.custom-drawer--fill-color-accessible { + @include mdc-drawer-fill-color-accessible($custom-drawer-color); +} + +.custom-drawer--ink-color { + @include mdc-drawer-ink-color($custom-drawer-color); +} diff --git a/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html b/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html new file mode 100644 index 00000000000..2008b347205 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html @@ -0,0 +1,132 @@ + + + + + + fill-color-accessible Drawer Mixin - MDC Web Screenshot Test + + + + + + + + + +
+ + +
+
+
+
+ Permanent Drawer - fill-color-accessible Mixin +
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+
+
+
+ + + + + + + + diff --git a/test/screenshot/spec/mdc-drawer/mixins/fill-color.html b/test/screenshot/spec/mdc-drawer/mixins/fill-color.html new file mode 100644 index 00000000000..152952e33b4 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/mixins/fill-color.html @@ -0,0 +1,132 @@ + + + + + + fill-color Drawer Mixin - MDC Web Screenshot Test + + + + + + + + + +
+ + +
+
+
+
+ Permanent Drawer - fill-color Mixin +
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+
+
+
+ + + + + + + + diff --git a/test/screenshot/spec/mdc-drawer/mixins/ink-color.html b/test/screenshot/spec/mdc-drawer/mixins/ink-color.html new file mode 100644 index 00000000000..faf8d921d22 --- /dev/null +++ b/test/screenshot/spec/mdc-drawer/mixins/ink-color.html @@ -0,0 +1,132 @@ + + + + + + ink-color Drawer Mixin - MDC Web Screenshot Test + + + + + + + + + +
+ + +
+
+
+
+ Permanent Drawer - ink-color Mixin +
+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum. +

+
+
+
+ + + + + + + + diff --git a/test/screenshot/spec/mdc-fab/classes/baseline.html b/test/screenshot/spec/mdc-fab/classes/baseline.html index ee5c887bc75..aa7685a312b 100644 --- a/test/screenshot/spec/mdc-fab/classes/baseline.html +++ b/test/screenshot/spec/mdc-fab/classes/baseline.html @@ -47,6 +47,6 @@ - + diff --git a/test/screenshot/spec/mdc-fab/classes/extended.html b/test/screenshot/spec/mdc-fab/classes/extended.html index c2537ae2ac1..e979f49bb53 100644 --- a/test/screenshot/spec/mdc-fab/classes/extended.html +++ b/test/screenshot/spec/mdc-fab/classes/extended.html @@ -51,6 +51,6 @@ - + diff --git a/test/screenshot/spec/mdc-fab/classes/mini.html b/test/screenshot/spec/mdc-fab/classes/mini.html index 6f81e69c636..00ed7210456 100644 --- a/test/screenshot/spec/mdc-fab/classes/mini.html +++ b/test/screenshot/spec/mdc-fab/classes/mini.html @@ -47,6 +47,6 @@ - + diff --git a/test/screenshot/spec/mdc-fab/mixins/extended-padding.html b/test/screenshot/spec/mdc-fab/mixins/extended-padding.html index bd777f5385a..a878f220f27 100644 --- a/test/screenshot/spec/mdc-fab/mixins/extended-padding.html +++ b/test/screenshot/spec/mdc-fab/mixins/extended-padding.html @@ -52,6 +52,6 @@ - + diff --git a/test/screenshot/spec/mdc-icon-button/classes/baseline.html b/test/screenshot/spec/mdc-icon-button/classes/baseline.html index 1bce2354cef..3393a07dac7 100644 --- a/test/screenshot/spec/mdc-icon-button/classes/baseline.html +++ b/test/screenshot/spec/mdc-icon-button/classes/baseline.html @@ -77,6 +77,6 @@ - + diff --git a/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html index 938fe705221..10206214532 100644 --- a/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html @@ -78,6 +78,6 @@ - + diff --git a/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html index 29529130c79..2e2103c9823 100644 --- a/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html @@ -78,6 +78,6 @@ - + diff --git a/test/screenshot/webpack.config.js b/test/screenshot/webpack.config.js index a688fa2d3f6..45f1157ceeb 100644 --- a/test/screenshot/webpack.config.js +++ b/test/screenshot/webpack.config.js @@ -19,6 +19,7 @@ const CssBundleFactory = require('../../scripts/webpack/css-bundle-factory'); const Environment = require('../../scripts/build/environment'); const Globber = require('../../scripts/webpack/globber'); +const JsBundleFactory = require('../../scripts/webpack/js-bundle-factory'); const PathResolver = require('../../scripts/build/path-resolver'); const PluginFactory = require('../../scripts/webpack/plugin-factory'); @@ -30,28 +31,93 @@ const globber = new Globber({pathResolver}); const pluginFactory = new PluginFactory({globber}); const copyrightBannerPlugin = pluginFactory.createCopyrightBannerPlugin(); const cssBundleFactory = new CssBundleFactory({env, pathResolver, globber, pluginFactory}); - -const OUTPUT = { - fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out'), - httpDirAbsolutePath: '/out/', -}; +const jsBundleFactory = new JsBundleFactory({env, pathResolver, globber, pluginFactory}); module.exports = [ mainCssALaCarte(), + mainJsCombined(), testCss(), + testJs(), + reportCss(), + reportJs(), ]; function mainCssALaCarte() { - return cssBundleFactory.createMainCssALaCarte({output: OUTPUT}); + return cssBundleFactory.createMainCssALaCarte({ + output: { + fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out'), + httpDirAbsolutePath: '/out/', + }, + }); +} + +function mainJsCombined() { + return jsBundleFactory.createMainJsCombined({ + output: { + fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out'), + httpDirAbsolutePath: '/out/', + }, + }); } function testCss() { return cssBundleFactory.createCustomCss({ bundleName: 'screenshot-test-css', chunkGlobConfig: { - inputDirectory: '/test/screenshot', + inputDirectory: '/test/screenshot/spec', + }, + output: { + fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out/spec'), + httpDirAbsolutePath: '/out/spec/', + }, + plugins: [ + copyrightBannerPlugin, + ], + }); +} + +function testJs() { + return jsBundleFactory.createCustomJs({ + bundleName: 'screenshot-test-js', + chunkGlobConfig: { + inputDirectory: '/test/screenshot/spec', + }, + output: { + fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out/spec'), + httpDirAbsolutePath: '/out/spec/', + }, + plugins: [ + copyrightBannerPlugin, + ], + }); +} + +function reportCss() { + return cssBundleFactory.createCustomCss({ + bundleName: 'screenshot-report-css', + chunkGlobConfig: { + inputDirectory: '/test/screenshot/report', + }, + output: { + fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out/report'), + httpDirAbsolutePath: '/out/report/', + }, + plugins: [ + copyrightBannerPlugin, + ], + }); +} + +function reportJs() { + return jsBundleFactory.createCustomJs({ + bundleName: 'screenshot-report-js', + chunkGlobConfig: { + inputDirectory: '/test/screenshot/report', + }, + output: { + fsDirAbsolutePath: pathResolver.getAbsolutePath('/test/screenshot/out/report'), + httpDirAbsolutePath: '/out/report/', }, - output: OUTPUT, plugins: [ copyrightBannerPlugin, ], From ab63c0ff3fb0bb12bdd6fd72792902627a0ad4e8 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 21:41:29 -0700 Subject: [PATCH 27/53] chore(infrastructure): Generate static index.html directory listing files (#3126) ### What it does - Automatically: - Generates static `index.html` files for every directory - Re-generates the `index.html` files whenever other `*.html` files change - Re-compiles `*.proto` files to `*.pb.js` whenever they change - Moves `auth/`, `commands/`, and `lib/` directories into new `infra/` directory ### Example output * Directory listing: https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/18/04_15_05_707/index.html #### Screenshot: ![image](https://user-images.githubusercontent.com/409245/42859055-b1ab8a02-8a06-11e8-983d-0f4889e3b326.png) --- .travis.yml | 2 +- package-lock.json | 2193 ++++++++++++----- package.json | 3 + test/screenshot/.gitignore | 1 + test/screenshot/{ => infra}/auth/.gitignore | 0 .../{ => infra}/auth/travis.tar.enc | Bin .../{ => infra}/commands/approve.js | 0 test/screenshot/{ => infra}/commands/build.js | 68 +- test/screenshot/{ => infra}/commands/clean.js | 2 +- test/screenshot/{ => infra}/commands/demo.js | 0 test/screenshot/infra/commands/index.js | 134 + test/screenshot/{ => infra}/commands/proto.js | 0 test/screenshot/{ => infra}/commands/serve.js | 0 test/screenshot/{ => infra}/commands/test.js | 0 .../screenshot/{ => infra}/commands/travis.sh | 6 +- test/screenshot/{ => infra}/lib/cbt-api.js | 0 test/screenshot/{ => infra}/lib/cli.js | 7 + .../{ => infra}/lib/cloud-storage.js | 0 test/screenshot/{ => infra}/lib/constants.js | 0 test/screenshot/{ => infra}/lib/controller.js | 0 test/screenshot/{ => infra}/lib/duration.js | 0 test/screenshot/{ => infra}/lib/externs.js | 0 test/screenshot/{ => infra}/lib/file-cache.js | 0 test/screenshot/{ => infra}/lib/git-repo.js | 0 test/screenshot/{ => infra}/lib/github-api.js | 0 .../screenshot/{ => infra}/lib/golden-file.js | 0 test/screenshot/{ => infra}/lib/golden-io.js | 0 .../{ => infra}/lib/image-cropper.js | 0 .../{ => infra}/lib/image-differ.js | 4 +- .../{ => infra}/lib/local-storage.js | 27 +- test/screenshot/{ => infra}/lib/logger.js | 0 .../{ => infra}/lib/process-manager.js | 22 + .../{ => infra}/lib/report-builder.js | 2 +- .../{ => infra}/lib/report-writer.js | 2 +- .../{ => infra}/lib/selenium-api.js | 0 .../{ => infra}/lib/user-agent-store.js | 2 +- test/screenshot/{ => infra}/proto/cbt.pb.js | 0 test/screenshot/{ => infra}/proto/cbt.proto | 0 .../screenshot/{ => infra}/proto/github.pb.js | 0 .../screenshot/{ => infra}/proto/github.proto | 0 test/screenshot/{ => infra}/proto/mdc.pb.js | 0 test/screenshot/{ => infra}/proto/mdc.proto | 0 .../{ => infra}/proto/selenium.pb.js | 0 .../{ => infra}/proto/selenium.proto | 0 test/screenshot/report/report.hbs | 2 +- test/screenshot/run.js | 24 +- test/screenshot/webpack.config.js | 8 +- 47 files changed, 1874 insertions(+), 635 deletions(-) create mode 100644 test/screenshot/.gitignore rename test/screenshot/{ => infra}/auth/.gitignore (100%) rename test/screenshot/{ => infra}/auth/travis.tar.enc (100%) rename test/screenshot/{ => infra}/commands/approve.js (100%) rename test/screenshot/{ => infra}/commands/build.js (60%) rename test/screenshot/{ => infra}/commands/clean.js (92%) rename test/screenshot/{ => infra}/commands/demo.js (100%) create mode 100644 test/screenshot/infra/commands/index.js rename test/screenshot/{ => infra}/commands/proto.js (100%) rename test/screenshot/{ => infra}/commands/serve.js (100%) rename test/screenshot/{ => infra}/commands/test.js (100%) rename test/screenshot/{ => infra}/commands/travis.sh (87%) rename test/screenshot/{ => infra}/lib/cbt-api.js (100%) rename test/screenshot/{ => infra}/lib/cli.js (99%) rename test/screenshot/{ => infra}/lib/cloud-storage.js (100%) rename test/screenshot/{ => infra}/lib/constants.js (100%) rename test/screenshot/{ => infra}/lib/controller.js (100%) rename test/screenshot/{ => infra}/lib/duration.js (100%) rename test/screenshot/{ => infra}/lib/externs.js (100%) rename test/screenshot/{ => infra}/lib/file-cache.js (100%) rename test/screenshot/{ => infra}/lib/git-repo.js (100%) rename test/screenshot/{ => infra}/lib/github-api.js (100%) rename test/screenshot/{ => infra}/lib/golden-file.js (100%) rename test/screenshot/{ => infra}/lib/golden-io.js (100%) rename test/screenshot/{ => infra}/lib/image-cropper.js (100%) rename test/screenshot/{ => infra}/lib/image-differ.js (97%) rename test/screenshot/{ => infra}/lib/local-storage.js (87%) rename test/screenshot/{ => infra}/lib/logger.js (100%) rename test/screenshot/{ => infra}/lib/process-manager.js (81%) rename test/screenshot/{ => infra}/lib/report-builder.js (99%) rename test/screenshot/{ => infra}/lib/report-writer.js (99%) rename test/screenshot/{ => infra}/lib/selenium-api.js (100%) rename test/screenshot/{ => infra}/lib/user-agent-store.js (99%) rename test/screenshot/{ => infra}/proto/cbt.pb.js (100%) rename test/screenshot/{ => infra}/proto/cbt.proto (100%) rename test/screenshot/{ => infra}/proto/github.pb.js (100%) rename test/screenshot/{ => infra}/proto/github.proto (100%) rename test/screenshot/{ => infra}/proto/mdc.pb.js (100%) rename test/screenshot/{ => infra}/proto/mdc.proto (100%) rename test/screenshot/{ => infra}/proto/selenium.pb.js (100%) rename test/screenshot/{ => infra}/proto/selenium.proto (100%) diff --git a/.travis.yml b/.travis.yml index 1413857ac41..27b6ff0e277 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,4 +37,4 @@ before_install: # Source the script to run it in the same shell process. This ensures that any environment variables set by the # script are visible to subsequent Travis CLI commands. # https://superuser.com/a/176788/62792 - - source test/screenshot/commands/travis.sh + - source test/screenshot/infra/commands/travis.sh diff --git a/package-lock.json b/package-lock.json index f118a79e2ae..ac713744181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -607,13 +607,304 @@ "dev": true }, "anymatch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", - "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "arrify": "^1.0.0", - "micromatch": "^2.1.5" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, "aproba": { @@ -704,6 +995,12 @@ "integrity": "sha1-onTthawIhJtr14R8RYB0XcUa37E=", "dev": true }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, "array-filter": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", @@ -829,6 +1126,12 @@ "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", "dev": true }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, "ast-types": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.10.1.tgz", @@ -886,6 +1189,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, "autoprefixer": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.0.0.tgz", @@ -1546,6 +1855,73 @@ "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", "dev": true }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "base64-arraybuffer": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", @@ -1625,9 +2001,9 @@ "dev": true }, "binary-extensions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", - "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, "bl": { @@ -2138,6 +2514,31 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "cacheable-request": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", @@ -2362,86 +2763,232 @@ "dev": true }, "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", "dev": true, "requires": { - "anymatch": "^1.3.0", + "anymatch": "^2.0.0", "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - } - }, - "ci-info": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.0.0.tgz", - "integrity": "sha1-3FKF8rTiUYIWg2gcOBwziPRuxTQ=", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "clean-stack": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", - "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=", - "dev": true - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "dev": true, - "requires": { - "colors": "1.0.3" + "readdirp": "^2.0.0", + "upath": "^1.0.5" }, "dependencies": { - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "dev": true - } - } - }, - "cli-width": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", - "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "ci-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.0.0.tgz", + "integrity": "sha1-3FKF8rTiUYIWg2gcOBwziPRuxTQ=", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "clean-stack": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", + "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=", + "dev": true + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + } + } + }, + "cli-width": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", "dev": true }, "cliui": { @@ -2690,6 +3237,16 @@ "integrity": "sha1-S5BvZw5aljqHt2sOFolkM0G2Ajw=", "dev": true }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, "color-convert": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", @@ -4130,6 +4687,12 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", @@ -4578,6 +5141,12 @@ "meow": "^3.3.0" } }, + "debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", + "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==", + "dev": true + }, "debug": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", @@ -4672,6 +5241,59 @@ "object-keys": "^1.0.8" } }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "defined": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", @@ -6105,6 +6727,27 @@ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", "dev": true }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "external-editor": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.4.tgz", @@ -6430,6 +7073,15 @@ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", "dev": true }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -6479,39 +7131,29 @@ "dev": true }, "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "dev": true, "optional": true, "requires": { - "nan": "^2.3.0", - "node-pre-gyp": "^0.6.39" + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" }, "dependencies": { "abbrev": { - "version": "1.1.0", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, "ansi-regex": { "version": "2.1.1", "bundled": true, "dev": true }, "aproba": { - "version": "1.1.1", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true @@ -6526,109 +7168,35 @@ "readable-stream": "^2.0.6" } }, - "asn1": { - "version": "0.2.3", + "balanced-match": { + "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, - "assert-plus": { - "version": "0.2.0", + "brace-expansion": { + "version": "1.1.11", "bundled": true, "dev": true, - "optional": true + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "asynckit": { - "version": "0.4.0", + "chownr": { + "version": "1.0.1", "bundled": true, "dev": true, "optional": true }, - "aws-sign2": { - "version": "0.6.0", + "code-point-at": { + "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, - "aws4": { - "version": "1.6.0", + "concat-map": { + "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^0.4.1", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -6638,35 +7206,11 @@ "core-util-is": { "version": "1.0.2", "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.x.x" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, "dev": true, - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } + "optional": true }, "debug": { - "version": "2.6.8", + "version": "2.6.9", "bundled": true, "dev": true, "optional": true, @@ -6675,16 +7219,11 @@ } }, "deep-extend": { - "version": "0.4.2", + "version": "0.5.1", "bundled": true, "dev": true, "optional": true }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, "delegates": { "version": "1.0.0", "bundled": true, @@ -6692,74 +7231,25 @@ "optional": true }, "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", + "version": "1.0.3", "bundled": true, "dev": true, "optional": true }, - "form-data": { - "version": "2.1.4", + "fs-minipass": { + "version": "1.2.5", "bundled": true, "dev": true, "optional": true, "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" + "minipass": "^2.2.1" } }, "fs.realpath": { "version": "1.0.0", "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, "dev": true, - "optional": true, - "requires": { - "fstream": "^1.0.0", - "inherits": "2", - "minimatch": "^3.0.0" - } + "optional": true }, "gauge": { "version": "2.7.4", @@ -6777,27 +7267,11 @@ "wide-align": "^1.1.0" } }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, "glob": { "version": "7.1.2", "bundled": true, "dev": true, + "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6807,64 +7281,35 @@ "path-is-absolute": "^1.0.0" } }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "^4.9.1", - "har-schema": "^1.0.5" - } - }, "has-unicode": { "version": "2.0.1", "bundled": true, "dev": true, "optional": true }, - "hawk": { - "version": "3.1.3", + "iconv-lite": { + "version": "0.4.21", "bundled": true, "dev": true, + "optional": true, "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" + "safer-buffer": "^2.1.0" } }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", + "ignore-walk": { + "version": "3.0.1", "bundled": true, "dev": true, "optional": true, "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", "bundled": true, "dev": true, + "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -6876,7 +7321,7 @@ "dev": true }, "ini": { - "version": "1.3.4", + "version": "1.3.5", "bundled": true, "dev": true, "optional": true @@ -6889,111 +7334,43 @@ "number-is-nan": "^1.0.0" } }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, "isarray": { "version": "1.0.0", "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, "dev": true, "optional": true }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", + "minimatch": { + "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } + "brace-expansion": "^1.1.7" } }, - "mime-db": { - "version": "1.27.0", + "minimist": { + "version": "0.0.8", "bundled": true, "dev": true }, - "mime-types": { - "version": "2.1.15", + "minipass": { + "version": "2.2.4", "bundled": true, "dev": true, "requires": { - "mime-db": "~1.27.0" + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" } }, - "minimatch": { - "version": "3.0.4", + "minizlib": { + "version": "1.1.0", "bundled": true, "dev": true, + "optional": true, "requires": { - "brace-expansion": "^1.1.7" + "minipass": "^2.2.1" } }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, "mkdirp": { "version": "0.5.1", "bundled": true, @@ -7008,23 +7385,33 @@ "dev": true, "optional": true }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, "node-pre-gyp": { - "version": "0.6.39", + "version": "0.10.0", "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", - "hawk": "3.1.3", "mkdirp": "^0.5.1", + "needle": "^2.2.0", "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", "rc": "^1.1.7", - "request": "2.81.0", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^2.2.1", - "tar-pack": "^3.4.0" + "tar": "^4" } }, "nopt": { @@ -7037,8 +7424,24 @@ "osenv": "^0.1.4" } }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, "npmlog": { - "version": "4.1.0", + "version": "4.1.2", "bundled": true, "dev": true, "optional": true, @@ -7054,12 +7457,6 @@ "bundled": true, "dev": true }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, "object-assign": { "version": "4.1.1", "bundled": true, @@ -7087,7 +7484,7 @@ "optional": true }, "osenv": { - "version": "0.1.4", + "version": "0.1.5", "bundled": true, "dev": true, "optional": true, @@ -7099,38 +7496,22 @@ "path-is-absolute": { "version": "1.0.1", "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", + "version": "2.0.0", "bundled": true, "dev": true, "optional": true }, "rc": { - "version": "1.2.1", + "version": "1.2.7", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "~0.4.0", + "deep-extend": "^0.5.1", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -7138,119 +7519,70 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "~1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~1.0.0", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~4.2.1", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "performance-now": "^0.2.0", - "qs": "~6.4.0", - "safe-buffer": "^5.0.1", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.0.0" + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "rimraf": { - "version": "2.6.1", + "version": "2.6.2", "bundled": true, "dev": true, + "optional": true, "requires": { "glob": "^7.0.5" } }, "safe-buffer": { - "version": "5.0.1", + "version": "5.1.1", "bundled": true, "dev": true }, - "semver": { - "version": "5.3.0", + "safer-buffer": { + "version": "2.1.2", "bundled": true, "dev": true, "optional": true }, - "set-blocking": { - "version": "2.0.0", + "sax": { + "version": "1.2.4", "bundled": true, "dev": true, "optional": true }, - "signal-exit": { - "version": "3.0.2", + "semver": { + "version": "5.5.0", "bundled": true, "dev": true, "optional": true }, - "sntp": { - "version": "1.0.9", + "set-blocking": { + "version": "2.0.0", "bundled": true, "dev": true, - "requires": { - "hoek": "2.x.x" - } + "optional": true }, - "sshpk": { - "version": "1.13.0", + "signal-exit": { + "version": "3.0.2", "bundled": true, "dev": true, - "optional": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jodid25519": "^1.0.0", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } + "optional": true }, "string-width": { "version": "1.0.2", @@ -7263,19 +7595,14 @@ } }, "string_decoder": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, + "optional": true, "requires": { - "safe-buffer": "^5.0.1" + "safe-buffer": "~5.1.0" } }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, "strip-ansi": { "version": "3.0.1", "bundled": true, @@ -7291,81 +7618,26 @@ "optional": true }, "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.2.0", - "fstream": "^1.0.10", - "fstream-ignore": "^1.0.5", - "once": "^1.3.3", - "readable-stream": "^2.1.4", - "rimraf": "^2.5.1", - "tar": "^2.2.1", - "uid-number": "^0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", + "version": "4.4.1", "bundled": true, "dev": true, "optional": true, "requires": { - "safe-buffer": "^5.0.1" + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" } }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, "util-deprecate": { "version": "1.0.2", "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, "dev": true, "optional": true }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, "wide-align": { "version": "1.1.2", "bundled": true, @@ -7379,6 +7651,11 @@ "version": "1.0.2", "bundled": true, "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true } } }, @@ -7824,6 +8101,12 @@ "readable-stream": "2" } }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -8606,6 +8889,66 @@ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "hash-base": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", @@ -9211,6 +9554,15 @@ "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", "dev": true }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, "is-alphabetical": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.1.tgz", @@ -9278,6 +9630,15 @@ "ci-info": "^1.0.0" } }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", @@ -9290,6 +9651,25 @@ "integrity": "sha1-9ftqlJlq2ejjdh+/vQkfH8qMToI=", "dev": true }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, "is-directory": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", @@ -10048,12 +10428,48 @@ "useragent": "^2.1.12" }, "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, "colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10864,6 +11280,12 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", @@ -11249,6 +11671,12 @@ "pify": "^2.3.0" } }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -11261,6 +11689,15 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, "markdown-escapes": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.1.tgz", @@ -11522,6 +11959,27 @@ "is-plain-obj": "^1.1.0" } }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "mixin-object": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", @@ -11708,12 +12166,57 @@ } }, "nan": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", - "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "dev": true, "optional": true }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -12271,12 +12774,51 @@ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", "dev": true }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "object-keys": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", "dev": true }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -12287,6 +12829,23 @@ "is-extendable": "^0.1.1" } }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "obuf": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz", @@ -12706,6 +13265,12 @@ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", "dev": true }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, "path-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", @@ -12903,6 +13468,12 @@ } } }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, "postcss": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", @@ -13974,6 +14545,16 @@ "is-primitive": "^2.0.0" } }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, "regexp.prototype.flags": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", @@ -14261,6 +14842,12 @@ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", "dev": true }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -14280,6 +14867,12 @@ "signal-exit": "^3.0.2" } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, "retry-axios": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", @@ -14560,6 +15153,15 @@ "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", "dev": true }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", @@ -14969,6 +15571,29 @@ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", "dev": true }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -15133,6 +15758,114 @@ "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", "dev": true }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, "sntp": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", @@ -15286,6 +16019,19 @@ "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", "dev": true }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "source-map-support": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", @@ -15295,6 +16041,12 @@ "source-map": "^0.5.6" } }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, "spdx-correct": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", @@ -15370,6 +16122,15 @@ "is-stream-ended": "^0.1.0" } }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, "split2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", @@ -15726,6 +16487,27 @@ "integrity": "sha1-0g+aYWu08MO5i5GSLSW2QKorxCU=", "dev": true }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "statuses": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", @@ -17016,6 +17798,48 @@ "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=", "dev": true }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, "to-slug-case": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-slug-case/-/to-slug-case-1.0.0.tgz", @@ -17262,6 +18086,41 @@ "x-is-string": "^0.1.0" } }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, "uniq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", @@ -17353,12 +18212,64 @@ "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", "dev": true }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "unzip-response": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", "dev": true }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true + }, "update-notifier": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.1.0.tgz", @@ -17398,6 +18309,12 @@ } } }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -17476,6 +18393,12 @@ "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", "dev": true }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, "useragent": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", @@ -17677,6 +18600,44 @@ "async": "^2.1.2", "chokidar": "^1.7.0", "graceful-fs": "^4.1.2" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, "wbuf": { @@ -17999,12 +18960,39 @@ "yargs": "^6.0.0" }, "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", "dev": true }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -18025,6 +19013,15 @@ "number-is-nan": "^1.0.0" } }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", diff --git a/package.json b/package.json index f48568caf6e..0c64707f607 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "screenshot:build": "node \"$PWD/test/screenshot/run.js\" build", "screenshot:clean": "node \"$PWD/test/screenshot/run.js\" clean", "screenshot:demo": "node \"$PWD/test/screenshot/run.js\" demo", + "screenshot:index": "node \"$PWD/test/screenshot/run.js\" index", "screenshot:proto": "node \"$PWD/test/screenshot/run.js\" proto", "screenshot:serve": "node \"$PWD/test/screenshot/run.js\" serve", "screenshot:test": "node \"$PWD/test/screenshot/run.js\" test", @@ -55,6 +56,7 @@ "bel": "^6.0.0", "camel-case": "^3.0.0", "chai": "^4.0.2", + "chokidar": "^2.0.4", "cli-table": "^0.3.1", "codecov": "^3.0.0", "colors": "^1.3.0", @@ -65,6 +67,7 @@ "css-loader": "^1.0.0", "cssom": "^0.3.2", "cz-conventional-changelog": "^2.0.0", + "debounce": "^1.1.0", "del": "^3.0.0", "del-cli": "^1.0.0", "detect-port": "^1.2.3", diff --git a/test/screenshot/.gitignore b/test/screenshot/.gitignore new file mode 100644 index 00000000000..dcaf71693e4 --- /dev/null +++ b/test/screenshot/.gitignore @@ -0,0 +1 @@ +index.html diff --git a/test/screenshot/auth/.gitignore b/test/screenshot/infra/auth/.gitignore similarity index 100% rename from test/screenshot/auth/.gitignore rename to test/screenshot/infra/auth/.gitignore diff --git a/test/screenshot/auth/travis.tar.enc b/test/screenshot/infra/auth/travis.tar.enc similarity index 100% rename from test/screenshot/auth/travis.tar.enc rename to test/screenshot/infra/auth/travis.tar.enc diff --git a/test/screenshot/commands/approve.js b/test/screenshot/infra/commands/approve.js similarity index 100% rename from test/screenshot/commands/approve.js rename to test/screenshot/infra/commands/approve.js diff --git a/test/screenshot/commands/build.js b/test/screenshot/infra/commands/build.js similarity index 60% rename from test/screenshot/commands/build.js rename to test/screenshot/infra/commands/build.js index ca04978e59d..4ebeb4d7f15 100644 --- a/test/screenshot/commands/build.js +++ b/test/screenshot/infra/commands/build.js @@ -16,10 +16,16 @@ 'use strict'; +const chokidar = require('chokidar'); +const debounce = require('debounce'); + const CleanCommand = require('./clean'); const Cli = require('../lib/cli'); +const Index = require('./index'); const Logger = require('../lib/logger'); const ProcessManager = require('../lib/process-manager'); +const Proto = require('./proto'); +const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); const logger = new Logger(__filename); const processManager = new ProcessManager(); @@ -29,7 +35,6 @@ module.exports = { // Travis sometimes forgets to emit this logger.foldEnd('install.npm'); - const webpackArgs = []; const shouldBuild = await this.shouldBuild_(); const shouldWatch = await this.shouldWatch_(); @@ -37,18 +42,67 @@ module.exports = { return; } - if (shouldWatch) { - webpackArgs.push('--watch'); - } - await CleanCommand.runAsync(); logger.foldStart('screenshot.build', 'Compiling source files'); - processManager.spawnChildProcessSync('npm', ['run', 'screenshot:proto']); - processManager.spawnChildProcessSync('npm', ['run', 'screenshot:webpack', '--', ...webpackArgs]); + + this.buildProtoFiles_(shouldWatch); + this.buildHtmlFiles_(shouldWatch); + + if (shouldWatch) { + processManager.spawnChildProcess('npm', ['run', 'screenshot:webpack', '--', '--watch']); + } else { + processManager.spawnChildProcessSync('npm', ['run', 'screenshot:webpack']); + } + logger.foldEnd('screenshot.build'); }, + /** + * @param {boolean} shouldWatch + * @private + */ + buildProtoFiles_(shouldWatch) { + const compile = debounce(() => Proto.runAsync(), 1000); + if (!shouldWatch) { + compile(); + return; + } + + const watcher = chokidar.watch('**/*.proto', { + cwd: TEST_DIR_RELATIVE_PATH, + awaitWriteFinish: true, + }); + + /* eslint-disable no-unused-vars */ + watcher.on('add', (filePath) => compile()); + watcher.on('change', (filePath) => compile()); + /* eslint-enable no-unused-vars */ + }, + + /** + * @param {boolean} shouldWatch + * @private + */ + buildHtmlFiles_(shouldWatch) { + const compile = debounce(() => Index.runAsync(), 1000); + if (!shouldWatch) { + compile(); + return; + } + + const watcher = chokidar.watch('**/*.html', { + cwd: TEST_DIR_RELATIVE_PATH, + awaitWriteFinish: true, + ignored: ['**/report/report.html', '**/index.html'], + }); + + /* eslint-disable no-unused-vars */ + watcher.on('add', (filePath) => compile()); + watcher.on('unlink', (filePath) => compile()); + /* eslint-enable no-unused-vars */ + }, + /** * @return {!Promise} * @private diff --git a/test/screenshot/commands/clean.js b/test/screenshot/infra/commands/clean.js similarity index 92% rename from test/screenshot/commands/clean.js rename to test/screenshot/infra/commands/clean.js index ca8523e9a8c..e2de2343d25 100644 --- a/test/screenshot/commands/clean.js +++ b/test/screenshot/infra/commands/clean.js @@ -23,7 +23,7 @@ const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); module.exports = { async runAsync() { - const relativePathPatterns = ['out'].map((filename) => { + const relativePathPatterns = ['out', '**/index.html'].map((filename) => { return path.join(TEST_DIR_RELATIVE_PATH, filename); }); await del(relativePathPatterns); diff --git a/test/screenshot/commands/demo.js b/test/screenshot/infra/commands/demo.js similarity index 100% rename from test/screenshot/commands/demo.js rename to test/screenshot/infra/commands/demo.js diff --git a/test/screenshot/infra/commands/index.js b/test/screenshot/infra/commands/index.js new file mode 100644 index 00000000000..d44542c080e --- /dev/null +++ b/test/screenshot/infra/commands/index.js @@ -0,0 +1,134 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const path = require('path'); + +const LocalStorage = require('../lib/local-storage'); +const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); + +// TODO(acdvorak): Clean up this entire file. It's gross. +module.exports = { + async runAsync() { + const localStorage = new LocalStorage(); + + const fakeReportHtmlFilePath = path.join(TEST_DIR_RELATIVE_PATH, 'report/report.html'); + const fakeReportJsonFilePath = path.join(TEST_DIR_RELATIVE_PATH, 'report/report.json'); + + await localStorage.writeTextFile(fakeReportHtmlFilePath, ''); + await localStorage.writeTextFile(fakeReportJsonFilePath, '{}'); + + async function walkDir(parentDirPath, depth = 0) { + const parentDirName = path.basename(parentDirPath); + + const realChildDirNames = localStorage.globDirs('*', parentDirPath) + .map((dirName) => dirName.replace(new RegExp('/+$'), '')); + + const nonHtmlFileNames = localStorage.globFiles('*', parentDirPath) + .filter((name) => !name.endsWith('.html')); + + const deepHtmlFileNames = localStorage.globFiles('**/*.html', parentDirPath) + .filter((name) => path.basename(name) !== 'index.html'); + + for (const dirName of realChildDirNames) { + await walkDir(path.join(parentDirPath, dirName), depth + 1); + } + + const printableChildDirNames = depth === 0 ? [...realChildDirNames] : ['..', ...realChildDirNames]; + const dirLinks = printableChildDirNames.map((childDirName) => { + return ` +
  • ${childDirName}/
  • +`; + }); + + const htmlFileLinks = deepHtmlFileNames.map((deepHtmlFileName) => { + const fileNameMarkup = deepHtmlFileName.split(path.sep).map((part) => { + if (/^mdc-.+$/.test(part) || /\.html$/.test(part)) { + return `${part}`; + } + return part; + }).join(path.sep); + return ` +
  • ${fileNameMarkup}
  • +`; + }); + + const otherFileLinks = nonHtmlFileNames.map((nonHtmlFileName) => { + return ` +
  • ${nonHtmlFileName}
  • +`; + }); + + const linkMarkup = [dirLinks, htmlFileLinks, otherFileLinks] + .filter((links) => links.length > 0) + .map((links) => `
      ${links.join('\n')}
    `) + .join('\n
    \n') + ; + + const html = ` + + + + ${parentDirName} + + + +
    + ${linkMarkup} +
    + + + `; + + await localStorage.writeTextFile(path.join(parentDirPath, 'index.html'), html); + } + + await walkDir(path.join(TEST_DIR_RELATIVE_PATH)); + + await localStorage.delete([fakeReportHtmlFilePath, fakeReportJsonFilePath]); + }, +}; diff --git a/test/screenshot/commands/proto.js b/test/screenshot/infra/commands/proto.js similarity index 100% rename from test/screenshot/commands/proto.js rename to test/screenshot/infra/commands/proto.js diff --git a/test/screenshot/commands/serve.js b/test/screenshot/infra/commands/serve.js similarity index 100% rename from test/screenshot/commands/serve.js rename to test/screenshot/infra/commands/serve.js diff --git a/test/screenshot/commands/test.js b/test/screenshot/infra/commands/test.js similarity index 100% rename from test/screenshot/commands/test.js rename to test/screenshot/infra/commands/test.js diff --git a/test/screenshot/commands/travis.sh b/test/screenshot/infra/commands/travis.sh similarity index 87% rename from test/screenshot/commands/travis.sh rename to test/screenshot/infra/commands/travis.sh index 8f4729d57e4..92c1829a60a 100755 --- a/test/screenshot/commands/travis.sh +++ b/test/screenshot/infra/commands/travis.sh @@ -15,9 +15,9 @@ function exit_if_external_pr() { function extract_api_credentials() { openssl aes-256-cbc -K $encrypted_eead2343bb54_key -iv $encrypted_eead2343bb54_iv \ - -in test/screenshot/auth/travis.tar.enc -out test/screenshot/auth/travis.tar -d + -in test/screenshot/infra/auth/travis.tar.enc -out test/screenshot/infra/auth/travis.tar -d - tar -xf test/screenshot/auth/travis.tar -C test/screenshot/auth/ + tar -xf test/screenshot/infra/auth/travis.tar -C test/screenshot/infra/auth/ echo echo 'git status:' @@ -38,7 +38,7 @@ function install_google_cloud_sdk() { export PATH=$PATH:$HOME/google-cloud-sdk/bin export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - gcloud auth activate-service-account --key-file test/screenshot/auth/gcs.json + gcloud auth activate-service-account --key-file test/screenshot/infra/auth/gcs.json gcloud config set project material-components-web gcloud components install gsutil diff --git a/test/screenshot/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js similarity index 100% rename from test/screenshot/lib/cbt-api.js rename to test/screenshot/infra/lib/cbt-api.js diff --git a/test/screenshot/lib/cli.js b/test/screenshot/infra/lib/cli.js similarity index 99% rename from test/screenshot/lib/cli.js rename to test/screenshot/infra/lib/cli.js index a7ac4ea58d7..cf22a792696 100644 --- a/test/screenshot/lib/cli.js +++ b/test/screenshot/infra/lib/cli.js @@ -71,6 +71,7 @@ class Cli { this.initBuildCommand_(); this.initCleanCommand_(); this.initDemoCommand_(); + this.initIndexCommand_(); this.initProtoCommand_(); this.initServeCommand_(); this.initTestCommand_(); @@ -214,6 +215,12 @@ If a local dev server is not already running, one will be started for the durati this.addNoBuildArg_(subparser); } + initIndexCommand_() { + this.commandParsers_.addParser('index', { + description: 'Generates static index.html directory listing files.', + }); + } + initProtoCommand_() { this.commandParsers_.addParser('proto', { description: 'Compiles Protocol Buffer source files (*.proto) to JavaScript (*.pb.js).', diff --git a/test/screenshot/lib/cloud-storage.js b/test/screenshot/infra/lib/cloud-storage.js similarity index 100% rename from test/screenshot/lib/cloud-storage.js rename to test/screenshot/infra/lib/cloud-storage.js diff --git a/test/screenshot/lib/constants.js b/test/screenshot/infra/lib/constants.js similarity index 100% rename from test/screenshot/lib/constants.js rename to test/screenshot/infra/lib/constants.js diff --git a/test/screenshot/lib/controller.js b/test/screenshot/infra/lib/controller.js similarity index 100% rename from test/screenshot/lib/controller.js rename to test/screenshot/infra/lib/controller.js diff --git a/test/screenshot/lib/duration.js b/test/screenshot/infra/lib/duration.js similarity index 100% rename from test/screenshot/lib/duration.js rename to test/screenshot/infra/lib/duration.js diff --git a/test/screenshot/lib/externs.js b/test/screenshot/infra/lib/externs.js similarity index 100% rename from test/screenshot/lib/externs.js rename to test/screenshot/infra/lib/externs.js diff --git a/test/screenshot/lib/file-cache.js b/test/screenshot/infra/lib/file-cache.js similarity index 100% rename from test/screenshot/lib/file-cache.js rename to test/screenshot/infra/lib/file-cache.js diff --git a/test/screenshot/lib/git-repo.js b/test/screenshot/infra/lib/git-repo.js similarity index 100% rename from test/screenshot/lib/git-repo.js rename to test/screenshot/infra/lib/git-repo.js diff --git a/test/screenshot/lib/github-api.js b/test/screenshot/infra/lib/github-api.js similarity index 100% rename from test/screenshot/lib/github-api.js rename to test/screenshot/infra/lib/github-api.js diff --git a/test/screenshot/lib/golden-file.js b/test/screenshot/infra/lib/golden-file.js similarity index 100% rename from test/screenshot/lib/golden-file.js rename to test/screenshot/infra/lib/golden-file.js diff --git a/test/screenshot/lib/golden-io.js b/test/screenshot/infra/lib/golden-io.js similarity index 100% rename from test/screenshot/lib/golden-io.js rename to test/screenshot/infra/lib/golden-io.js diff --git a/test/screenshot/lib/image-cropper.js b/test/screenshot/infra/lib/image-cropper.js similarity index 100% rename from test/screenshot/lib/image-cropper.js rename to test/screenshot/infra/lib/image-cropper.js diff --git a/test/screenshot/lib/image-differ.js b/test/screenshot/infra/lib/image-differ.js similarity index 97% rename from test/screenshot/lib/image-differ.js rename to test/screenshot/infra/lib/image-differ.js index 39fad56896d..38f5b30d0ce 100644 --- a/test/screenshot/lib/image-differ.js +++ b/test/screenshot/infra/lib/image-differ.js @@ -95,7 +95,7 @@ class ImageDiffer { * @private */ async computeDiff_({actualImageBuffer, expectedImageBuffer}) { - const options = require('../diffing.json').resemble_config; + const options = require('../../diffing.json').resemble_config; return await compareImages( actualImageBuffer, expectedImageBuffer, @@ -131,7 +131,7 @@ class ImageDiffer { const diffPixelRoundPercentage = roundPercentage(diffPixelRawPercentage); const diffPixelFraction = diffPixelRawPercentage / 100; const diffPixelCount = Math.ceil(diffPixelFraction * diffJimpImage.bitmap.width * diffJimpImage.bitmap.height); - const minChangedPixelCount = require('../diffing.json').flaky_tests.min_changed_pixel_count; + const minChangedPixelCount = require('../../diffing.json').flaky_tests.min_changed_pixel_count; const hasChanged = diffPixelCount >= minChangedPixelCount; const diffImageResult = DiffImageResult.create({ expected_image_dimensions: Dimensions.create({ diff --git a/test/screenshot/lib/local-storage.js b/test/screenshot/infra/lib/local-storage.js similarity index 87% rename from test/screenshot/lib/local-storage.js rename to test/screenshot/infra/lib/local-storage.js index 746e329d26b..821d5c8e2e9 100644 --- a/test/screenshot/lib/local-storage.js +++ b/test/screenshot/infra/lib/local-storage.js @@ -59,11 +59,11 @@ class LocalStorage { /** * @param {!mdc.proto.ReportMeta} reportMeta - * @return {!Promise>} File paths relative to the git repo. E.g.: "test/screenshot/browser.json". + * @return {!Promise>} */ async getTestPageDestinationPaths(reportMeta) { const cwd = reportMeta.local_asset_base_dir; - return glob.sync('**/spec/mdc-*/**/*.html', {cwd, nodir: true}); + return glob.sync('**/spec/mdc-*/**/*.html', {cwd, nodir: true, ignore: ['**/index.html']}); } /** @@ -117,10 +117,25 @@ class LocalStorage { * @param {string=} cwd * @return {!Array} */ - glob(pattern, cwd = process.cwd()) { + globFiles(pattern, cwd = process.cwd()) { + if (pattern.endsWith('/')) { + pattern = pattern.replace(new RegExp('/+$'), ''); + } return glob.sync(pattern, {cwd, nodir: true}); } + /** + * @param {string} pattern + * @param {string=} cwd + * @return {!Array} + */ + globDirs(pattern, cwd = process.cwd()) { + if (!pattern.endsWith('/')) { + pattern += '/'; + } + return glob.sync(pattern, {cwd, nodir: false}); + } + /** * @param {string} src * @param {string} dest @@ -181,9 +196,11 @@ class LocalStorage { const ignoredTopLevelFilesAndDirs = await this.gitRepo_.getIgnoredPaths(relativePaths); return relativePaths.filter((relativePath) => { - const isBuildOutputDir = relativePath.split(path.sep).includes('out'); + const pathParts = relativePath.split(path.sep); + const isBuildOutputDir = pathParts.includes('out'); + const isIndexHtmlFile = pathParts[pathParts.length - 1] === 'index.html'; const isIgnoredFile = ignoredTopLevelFilesAndDirs.includes(relativePath); - return isBuildOutputDir || !isIgnoredFile; + return isBuildOutputDir || isIndexHtmlFile || !isIgnoredFile; }); } } diff --git a/test/screenshot/lib/logger.js b/test/screenshot/infra/lib/logger.js similarity index 100% rename from test/screenshot/lib/logger.js rename to test/screenshot/infra/lib/logger.js diff --git a/test/screenshot/lib/process-manager.js b/test/screenshot/infra/lib/process-manager.js similarity index 81% rename from test/screenshot/lib/process-manager.js rename to test/screenshot/infra/lib/process-manager.js index d64ae2d21b8..13971a1257a 100644 --- a/test/screenshot/lib/process-manager.js +++ b/test/screenshot/infra/lib/process-manager.js @@ -20,6 +20,28 @@ const childProcess = require('child_process'); const ps = require('ps-node'); class ProcessManager { + /** + * @param {string} cmd + * @param {!Array} args + * @param {!ChildProcessSpawnOptions=} opts + * @return {!ChildProcess} + */ + spawnChildProcess(cmd, args, opts = {}) { + /** @type {!ChildProcessSpawnOptions} */ + const defaultOpts = { + stdio: 'inherit', + shell: true, + windowsHide: true, + }; + + /** @type {!ChildProcessSpawnOptions} */ + const mergedOpts = Object.assign({}, defaultOpts, opts); + + console.log(`${cmd} ${args.join(' ')}`); + + return childProcess.spawn(cmd, args, mergedOpts); + } + /** * @param {string} cmd * @param {!Array} args diff --git a/test/screenshot/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js similarity index 99% rename from test/screenshot/lib/report-builder.js rename to test/screenshot/infra/lib/report-builder.js index c860950aea6..de52b547e0a 100644 --- a/test/screenshot/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -393,7 +393,7 @@ class ReportBuilder { localReportBaseDir, ); - const mdcVersionString = require('../../../lerna.json').version; + const mdcVersionString = require('../../../../lerna.json').version; const hostOsName = osName(os.platform(), os.release()); const gitStatus = GitStatus.fromObject(await this.gitRepo_.getStatus()); diff --git a/test/screenshot/lib/report-writer.js b/test/screenshot/infra/lib/report-writer.js similarity index 99% rename from test/screenshot/lib/report-writer.js rename to test/screenshot/infra/lib/report-writer.js index 037777b5b6b..4ace08732bd 100644 --- a/test/screenshot/lib/report-writer.js +++ b/test/screenshot/infra/lib/report-writer.js @@ -302,7 +302,7 @@ class ReportWriter { /** @private */ registerPartials_() { - const partialFilePaths = this.localStorage_.glob(path.join(TEST_DIR_RELATIVE_PATH, 'report/_*.hbs')); + const partialFilePaths = this.localStorage_.globFiles(path.join(TEST_DIR_RELATIVE_PATH, 'report/_*.hbs')); for (const partialFilePath of partialFilePaths) { // TODO(acdvorak): What about hyphen/dash characters? const name = path.basename(partialFilePath) diff --git a/test/screenshot/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js similarity index 100% rename from test/screenshot/lib/selenium-api.js rename to test/screenshot/infra/lib/selenium-api.js diff --git a/test/screenshot/lib/user-agent-store.js b/test/screenshot/infra/lib/user-agent-store.js similarity index 99% rename from test/screenshot/lib/user-agent-store.js rename to test/screenshot/infra/lib/user-agent-store.js index 13506a7be93..e828633a448 100644 --- a/test/screenshot/lib/user-agent-store.js +++ b/test/screenshot/infra/lib/user-agent-store.js @@ -60,7 +60,7 @@ class UserAgentStore { * @private */ getAllAliases_() { - return require('../browser.json').user_agent_aliases; + return require('../../browser.json').user_agent_aliases; } /** diff --git a/test/screenshot/proto/cbt.pb.js b/test/screenshot/infra/proto/cbt.pb.js similarity index 100% rename from test/screenshot/proto/cbt.pb.js rename to test/screenshot/infra/proto/cbt.pb.js diff --git a/test/screenshot/proto/cbt.proto b/test/screenshot/infra/proto/cbt.proto similarity index 100% rename from test/screenshot/proto/cbt.proto rename to test/screenshot/infra/proto/cbt.proto diff --git a/test/screenshot/proto/github.pb.js b/test/screenshot/infra/proto/github.pb.js similarity index 100% rename from test/screenshot/proto/github.pb.js rename to test/screenshot/infra/proto/github.pb.js diff --git a/test/screenshot/proto/github.proto b/test/screenshot/infra/proto/github.proto similarity index 100% rename from test/screenshot/proto/github.proto rename to test/screenshot/infra/proto/github.proto diff --git a/test/screenshot/proto/mdc.pb.js b/test/screenshot/infra/proto/mdc.pb.js similarity index 100% rename from test/screenshot/proto/mdc.pb.js rename to test/screenshot/infra/proto/mdc.pb.js diff --git a/test/screenshot/proto/mdc.proto b/test/screenshot/infra/proto/mdc.proto similarity index 100% rename from test/screenshot/proto/mdc.proto rename to test/screenshot/infra/proto/mdc.proto diff --git a/test/screenshot/proto/selenium.pb.js b/test/screenshot/infra/proto/selenium.pb.js similarity index 100% rename from test/screenshot/proto/selenium.pb.js rename to test/screenshot/infra/proto/selenium.pb.js diff --git a/test/screenshot/proto/selenium.proto b/test/screenshot/infra/proto/selenium.proto similarity index 100% rename from test/screenshot/proto/selenium.proto rename to test/screenshot/infra/proto/selenium.proto diff --git a/test/screenshot/report/report.hbs b/test/screenshot/report/report.hbs index 785b2659239..8d44290e1d4 100644 --- a/test/screenshot/report/report.hbs +++ b/test/screenshot/report/report.hbs @@ -19,7 +19,7 @@ - + diff --git a/test/screenshot/run.js b/test/screenshot/run.js index b82cfbab3e5..31ec450d1bf 100644 --- a/test/screenshot/run.js +++ b/test/screenshot/run.js @@ -16,37 +16,41 @@ 'use strict'; -const Cli = require('./lib/cli'); -const Duration = require('./lib/duration'); -const {ExitCode} = require('./lib/constants'); +const Cli = require('./infra/lib/cli'); +const Duration = require('./infra/lib/duration'); +const {ExitCode} = require('./infra/lib/constants'); const COMMAND_MAP = { async approve() { - return require('./commands/approve').runAsync(); + return require('./infra/commands/approve').runAsync(); }, async build() { - return require('./commands/build').runAsync(); + return require('./infra/commands/build').runAsync(); }, async clean() { - return require('./commands/clean').runAsync(); + return require('./infra/commands/clean').runAsync(); }, async demo() { - return require('./commands/demo').runAsync(); + return require('./infra/commands/demo').runAsync(); + }, + + async index() { + return require('./infra/commands/index').runAsync(); }, async proto() { - return require('./commands/proto').runAsync(); + return require('./infra/commands/proto').runAsync(); }, async serve() { - return require('./commands/serve').runAsync(); + return require('./infra/commands/serve').runAsync(); }, async test() { - return require('./commands/test').runAsync(); + return require('./infra/commands/test').runAsync(); }, }; diff --git a/test/screenshot/webpack.config.js b/test/screenshot/webpack.config.js index 45f1157ceeb..bb040f6fa13 100644 --- a/test/screenshot/webpack.config.js +++ b/test/screenshot/webpack.config.js @@ -36,8 +36,8 @@ const jsBundleFactory = new JsBundleFactory({env, pathResolver, globber, pluginF module.exports = [ mainCssALaCarte(), mainJsCombined(), - testCss(), - testJs(), + specCss(), + specJs(), reportCss(), reportJs(), ]; @@ -60,7 +60,7 @@ function mainJsCombined() { }); } -function testCss() { +function specCss() { return cssBundleFactory.createCustomCss({ bundleName: 'screenshot-test-css', chunkGlobConfig: { @@ -76,7 +76,7 @@ function testCss() { }); } -function testJs() { +function specJs() { return jsBundleFactory.createCustomJs({ bundleName: 'screenshot-test-js', chunkGlobConfig: { From f9819bb5c82602c5af9de64778ae251add058367 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 22:30:42 -0700 Subject: [PATCH 28/53] chore(infrastructure): Add `--parallels` CLI flag; don't retry big diffs (#3127) ### What it does - Replaces the `--max-parallels` boolean flag with a `--parallels` numeric flag - Doesn't retry screenshots when the diff percentage is greater than 10% of pixels (they're probably not flakes) --- test/screenshot/infra/lib/cli.js | 20 ++++++++++---------- test/screenshot/infra/lib/selenium-api.js | 20 +++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/test/screenshot/infra/lib/cli.js b/test/screenshot/infra/lib/cli.js index cf22a792696..d229ffa4840 100644 --- a/test/screenshot/infra/lib/cli.js +++ b/test/screenshot/infra/lib/cli.js @@ -290,14 +290,14 @@ E.g.: '--browser=chrome,-mobile' is the same as '--browser=chrome --browser=-mob }); this.addArg_(subparser, { - optionNames: ['--max-parallels'], - type: 'boolean', + optionNames: ['--parallels'], + type: 'integer', + defaultValue: 0, description: ` -If this option is present, CBT tests will run the maximum number of parallel browser VMs allowed by our plan. -The default behavior is to start 3 browsers if nobody else is running tests, or 1 browser if other tests are running. -IMPORTANT: To ensure that other developers can run their tests too, only use this option during off-peak hours when you -know nobody else is going to be running tests. -This option is capped by A) our CBT account allowance, and B) the number of available VMs. +Maximum number of browser VMs to run in parallel (subject to our CBT plan limit and VM availability). +A value of '0' will start 3 browsers if nobody else is running tests, or 1 browser if other tests are already running. +IMPORTANT: To ensure that multiple developers can run their tests simultaneously, do not set this value higher than 1 +during normal business hours when other people are likely to be running tests. `, }); @@ -344,9 +344,9 @@ that you know are going to have diffs. return this.args_['--diff-base']; } - /** @return {boolean} */ - get maxParallels() { - return this.args_['--max-parallels']; + /** @return {number} */ + get parallels() { + return this.args_['--parallels']; } /** @return {number} */ diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 688bd4d97f9..45556cb0fc7 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -187,12 +187,13 @@ class SeleniumApi { const startTimeMs = Date.now(); while (true) { - /** @type {!mdc.proto.cbt.CbtConcurrencyStats} */ + /** @type {!cbt.proto.CbtConcurrencyStats} */ const stats = await this.cbtApi_.fetchConcurrencyStats(); const active = stats.active_concurrent_selenium_tests; const max = stats.max_concurrent_selenium_tests; + const available = max - active; - if (active === max) { + if (!available) { const elapsedTimeMs = Date.now() - startTimeMs; const elapsedTimeHuman = Duration.millis(elapsedTimeMs).toHumanShort(); if (elapsedTimeMs > CBT_CONCURRENCY_MAX_WAIT_MS) { @@ -208,14 +209,14 @@ class SeleniumApi { continue; } - if (this.cli_.maxParallels) { - return max - active; - } + const requested = Math.min(this.cli_.parallels, available); // If nobody else is running any tests, run half the number of concurrent tests allowed by our CBT account. // This gives us _some_ parallelism while still allowing other users to run their tests. // If someone else is already running tests, only run one test at a time. - return active === 0 ? Math.ceil(max / 2) : 1; + const half = active === 0 ? Math.ceil(max / 2) : 1; + + return requested === 0 ? half : requested; } } @@ -442,10 +443,10 @@ class SeleniumApi { /** @type {?mdc.proto.DiffImageResult} */ let diffImageResult = null; - /** @type {?number} */ - let changedPixelCount = null; + /** @type {number} */ let changedPixelCount = 0; + /** @type {number} */ let changedPixelFraction = 0; - while (screenshot.retry_count <= screenshot.max_retries) { + while (screenshot.retry_count <= screenshot.max_retries && changedPixelFraction < 0.10) { if (screenshot.retry_count > 0) { const {width, height} = diffImageResult.diff_image_dimensions; const whichMsg = `${screenshot.actual_html_file.public_url} > ${userAgent.alias}`; @@ -466,6 +467,7 @@ class SeleniumApi { } changedPixelCount = diffImageResult.changed_pixel_count; + changedPixelFraction = diffImageResult.changed_pixel_fraction; screenshot.retry_count++; } From de5fa6b2cfdf877232f7f5ef786710db4154b119 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 17 Jul 2018 23:37:39 -0700 Subject: [PATCH 29/53] chore(infrastructure): Display '0' values in CLI `--help` (#3128) Also: - Adds `max_auto_retry_changed_pixel_fraction` property to `diffing.json` - Fixes `@typedef` syntax in `externs.js` --- test/screenshot/diffing.json | 3 +- test/screenshot/infra/commands/build.js | 8 ++-- test/screenshot/infra/lib/cli.js | 54 ++++++++++++----------- test/screenshot/infra/lib/externs.js | 39 ++++++---------- test/screenshot/infra/lib/selenium-api.js | 8 ++-- 5 files changed, 51 insertions(+), 61 deletions(-) diff --git a/test/screenshot/diffing.json b/test/screenshot/diffing.json index 756690018bc..fa9a67bdf24 100644 --- a/test/screenshot/diffing.json +++ b/test/screenshot/diffing.json @@ -5,6 +5,7 @@ } }, "flaky_tests": { - "min_changed_pixel_count": 15 + "min_changed_pixel_count": 15, + "max_auto_retry_changed_pixel_fraction": 0.10 } } diff --git a/test/screenshot/infra/commands/build.js b/test/screenshot/infra/commands/build.js index 4ebeb4d7f15..65790b8c388 100644 --- a/test/screenshot/infra/commands/build.js +++ b/test/screenshot/infra/commands/build.js @@ -21,10 +21,10 @@ const debounce = require('debounce'); const CleanCommand = require('./clean'); const Cli = require('../lib/cli'); -const Index = require('./index'); +const IndexCommand = require('./index'); const Logger = require('../lib/logger'); const ProcessManager = require('../lib/process-manager'); -const Proto = require('./proto'); +const ProtoCommand = require('./proto'); const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); const logger = new Logger(__filename); @@ -63,7 +63,7 @@ module.exports = { * @private */ buildProtoFiles_(shouldWatch) { - const compile = debounce(() => Proto.runAsync(), 1000); + const compile = debounce(() => ProtoCommand.runAsync(), 1000); if (!shouldWatch) { compile(); return; @@ -85,7 +85,7 @@ module.exports = { * @private */ buildHtmlFiles_(shouldWatch) { - const compile = debounce(() => Index.runAsync(), 1000); + const compile = debounce(() => IndexCommand.runAsync(), 1000); if (!shouldWatch) { compile(); return; diff --git a/test/screenshot/infra/lib/cli.js b/test/screenshot/infra/lib/cli.js index d229ffa4840..ff34ace108f 100644 --- a/test/screenshot/infra/lib/cli.js +++ b/test/screenshot/infra/lib/cli.js @@ -85,6 +85,8 @@ class Cli { * @private */ addArg_(parser, config) { + const metaval = config.exampleValue || config.defaultValue; + const metavar = metaval === 0 ? '0' : (metaval || (config.type || '').toUpperCase() || 'VALUE'); parser.addArgument(config.optionNames, { help: config.description.trim(), dest: config.optionNames[config.optionNames.length - 1], @@ -92,7 +94,7 @@ class Cli { action: config.type === 'array' ? 'append' : (config.type === 'boolean' ? 'storeTrue' : 'store'), required: config.isRequired || false, defaultValue: config.defaultValue, - metavar: config.exampleValue || config.defaultValue, + metavar, }); } @@ -249,6 +251,30 @@ If a local dev server is not already running, one will be started for the durati this.addNoFetchArg_(subparser); this.addOfflineArg_(subparser); + this.addArg_(subparser, { + optionNames: ['--parallels'], + type: 'integer', + defaultValue: 0, + description: ` +Maximum number of browser VMs to run in parallel (subject to our CBT plan limit and VM availability). +A value of '0' will start 3 browsers if nobody else is running tests, or 1 browser if other tests are already running. +IMPORTANT: To ensure that multiple developers can run their tests simultaneously, do not set this value higher than 1 +during normal business hours when other people are likely to be running tests. +`, + }); + + this.addArg_(subparser, { + optionNames: ['--retries'], + type: 'integer', + defaultValue: 3, + description: ` +Number of times to retry a screenshot that comes back with diffs. If you're not expecting any diffs, automatically +retrying screenshots can help decrease noise from flaky browser rendering. However, if you're making a change that +intentionally affects the rendered output, there's no point slowing down the test by retrying a bunch of screenshots +that you know are going to have diffs. +`, + }); + this.addArg_(subparser, { optionNames: ['--diff-base'], defaultValue: GOLDEN_JSON_RELATIVE_PATH, @@ -257,7 +283,7 @@ File path, URL, or Git ref of a 'golden.json' file to diff against. Typically a branch name or commit hash, but may also be a local file path or public URL. Git refs may optionally be suffixed with ':path/to/golden.json' (the default is '${GOLDEN_JSON_RELATIVE_PATH}'). E.g., '${GOLDEN_JSON_RELATIVE_PATH}' (default), 'HEAD', 'master', 'origin/master', 'feat/foo/bar', '01abc11e0', -'/tmp/golden.json', 'https://storage.googleapis.com/.../test/screenshot/golden.json'. +'/tmp/golden.json', 'https://storage.googleapis.com/.../golden.json'. `, }); @@ -286,30 +312,6 @@ To negate a pattern, prefix it with a '-' character. E.g.: '--browser=chrome,-mobile' will test Chrome on desktop, but not on mobile. Passing this option more than once is equivalent to passing a single comma-separated value. E.g.: '--browser=chrome,-mobile' is the same as '--browser=chrome --browser=-mobile'. -`, - }); - - this.addArg_(subparser, { - optionNames: ['--parallels'], - type: 'integer', - defaultValue: 0, - description: ` -Maximum number of browser VMs to run in parallel (subject to our CBT plan limit and VM availability). -A value of '0' will start 3 browsers if nobody else is running tests, or 1 browser if other tests are already running. -IMPORTANT: To ensure that multiple developers can run their tests simultaneously, do not set this value higher than 1 -during normal business hours when other people are likely to be running tests. -`, - }); - - this.addArg_(subparser, { - optionNames: ['--retries'], - type: 'integer', - defaultValue: 3, - description: ` -Number of times to retry a screenshot that comes back with diffs. If you're not expecting any diffs, automatically -retrying screenshots can help decrease noise from flaky browser rendering. However, if you're making a change that -intentionally affects the rendered output, there's no point slowing down the test by retrying a bunch of screenshots -that you know are going to have diffs. `, }); } diff --git a/test/screenshot/infra/lib/externs.js b/test/screenshot/infra/lib/externs.js index bff9ae87d27..c3d334fdd37 100644 --- a/test/screenshot/infra/lib/externs.js +++ b/test/screenshot/infra/lib/externs.js @@ -29,18 +29,16 @@ * unreviewedUserAgentCbEls: !Array, * changelistDict: !ReportUiChangelistDict, * reviewStatusCountDict: !ReportUiReviewStatusCountDict, - * }} + * }} ReportUiState */ -let ReportUiState; /** * @typedef {{ * changed: !ReportUiChangelistState, * added: !ReportUiChangelistState, * removed: !ReportUiChangelistState, - * }} + * }} ReportUiChangelistDict */ -let ReportUiChangelistDict; /** * @typedef {{ @@ -51,14 +49,12 @@ let ReportUiChangelistDict; * uncheckedUserAgentCbEls: !Array, * reviewStatusCountDict: !ReportUiReviewStatusCountDict, * pageDict: !ReportUiPageDict, - * }} + * }} ReportUiChangelistState */ -let ReportUiChangelistState; /** - * @typedef {!Object} + * @typedef {!Object} ReportUiPageDict */ -let ReportUiPageDict; /** * @typedef {{ @@ -68,14 +64,12 @@ let ReportUiPageDict; * checkedUserAgentCbEls: !Array, * uncheckedUserAgentCbEls: !Array, * reviewStatusCountDict: !ReportUiReviewStatusCountDict, - * }} + * }} ReportUiPageState */ -let ReportUiPageState; /** - * @typedef {!Object} + * @typedef {!Object} ReportUiReviewStatusCountDict */ -let ReportUiReviewStatusCountDict; /* @@ -91,9 +85,8 @@ let ReportUiReviewStatusCountDict; * type: ?string, * defaultValue: ?*, * exampleValue: ?string, - * }} + * }} CliOptionConfig */ -let CliOptionConfig; /* @@ -109,9 +102,8 @@ let CliOptionConfig; * analysisTime: number, * getImageDataUrl: function(text: string): string, * getBuffer: function(includeOriginal: boolean): !Buffer, - * }} + * }} ResembleApiComparisonResult */ -let ResembleApiComparisonResult; /** * @typedef {{ @@ -119,9 +111,8 @@ let ResembleApiComparisonResult; * left: number, * bottom: number, * right: number, - * }} + * }} ResembleApiBoundingBox */ -let ResembleApiBoundingBox; /* @@ -135,9 +126,8 @@ let ResembleApiBoundingBox; * ppid: number, * command: string, * arguments: !Array, - * }} + * }} PsNodeProcess */ -let PsNodeProcess; /* @@ -157,18 +147,16 @@ let PsNodeProcess; * shell: ?boolean, * windowsVerbatimArguments: ?boolean, * windowsHide: ?boolean, - * }} + * }} ChildProcessSpawnOptions */ -let ChildProcessSpawnOptions; /** * @typedef {{ * status: number, * signal: ?string, * pid: number, - * }} + * }} ChildProcessSpawnResult */ -let ChildProcessSpawnResult; /* @@ -177,6 +165,5 @@ let ChildProcessSpawnResult; /** - * @typedef {{r: number, g: number, b: number, a: number}} + * @typedef {{r: number, g: number, b: number, a: number}} RGBA */ -let RGBA; diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 45556cb0fc7..25d4cb1cee6 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -442,11 +442,11 @@ class SeleniumApi { /** @type {?mdc.proto.DiffImageResult} */ let diffImageResult = null; + let changedPixelCount = 0; + let changedPixelFraction = 0; + const maxPixelFraction = require('../../diffing.json').flaky_tests.max_auto_retry_changed_pixel_fraction; - /** @type {number} */ let changedPixelCount = 0; - /** @type {number} */ let changedPixelFraction = 0; - - while (screenshot.retry_count <= screenshot.max_retries && changedPixelFraction < 0.10) { + while (screenshot.retry_count <= screenshot.max_retries && changedPixelFraction <= maxPixelFraction) { if (screenshot.retry_count > 0) { const {width, height} = diffImageResult.diff_image_dimensions; const whichMsg = `${screenshot.actual_html_file.public_url} > ${userAgent.alias}`; From a5bea1b1094a5fdea7cc277792409ed65fc105d5 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Wed, 18 Jul 2018 08:00:21 -0700 Subject: [PATCH 30/53] chore(infrastructure): Combine `custom.scss` and `fixture.scss` files (#3130) Both files are only a few lines long, so it seems silly to have two of them. --- test/screenshot/infra/lib/controller.js | 1 + test/screenshot/spec/mdc-button/custom.scss | 55 ------------------- test/screenshot/spec/mdc-button/fixture.scss | 40 ++++++++++++++ .../mixins/container-fill-color.html | 1 - .../spec/mdc-button/mixins/corner-radius.html | 1 - .../mdc-button/mixins/filled-accessible.html | 1 - .../mixins/horizontal-padding-baseline.html | 1 - .../mixins/horizontal-padding-dense.html | 1 - .../spec/mdc-button/mixins/icon-color.html | 1 - .../spec/mdc-button/mixins/ink-color.html | 1 - .../spec/mdc-button/mixins/stroke-color.html | 1 - .../spec/mdc-button/mixins/stroke-width.html | 1 - test/screenshot/spec/mdc-fab/custom.scss | 30 ---------- test/screenshot/spec/mdc-fab/fixture.scss | 15 +++++ .../spec/mdc-fab/mixins/extended-padding.html | 1 - .../spec/mdc-icon-button/custom.scss | 29 ---------- .../spec/mdc-icon-button/fixture.scss | 14 +++++ .../mdc-icon-button/mixins/icon-size.html | 1 - .../mdc-icon-button/mixins/ink-color.html | 1 - 19 files changed, 70 insertions(+), 126 deletions(-) delete mode 100644 test/screenshot/spec/mdc-button/custom.scss delete mode 100644 test/screenshot/spec/mdc-fab/custom.scss delete mode 100644 test/screenshot/spec/mdc-icon-button/custom.scss diff --git a/test/screenshot/infra/lib/controller.js b/test/screenshot/infra/lib/controller.js index ef546b07786..9608f343566 100644 --- a/test/screenshot/infra/lib/controller.js +++ b/test/screenshot/infra/lib/controller.js @@ -186,6 +186,7 @@ class Controller { await this.cloudStorage_.uploadDiffReport(reportData); this.logger_.foldEnd('screenshot.report'); + this.logger_.log(''); // TODO(acdvorak): Store this directly in the proto so we don't have to recalculate it all over the place const numChanges = diff --git a/test/screenshot/spec/mdc-button/custom.scss b/test/screenshot/spec/mdc-button/custom.scss deleted file mode 100644 index 62d71251cf8..00000000000 --- a/test/screenshot/spec/mdc-button/custom.scss +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright 2018 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -@import "../../../packages/mdc-button/mixins"; -@import "../../../packages/mdc-theme/color-palette"; - -$custom-button-color: $material-color-red-300; -$custom-button-custom-corner-radius: 8px; -$custom-button-custom-outline-width: 4px; -$custom-button-custom-horizontal-padding: 24px; - -.custom-button--ink-color { - @include mdc-button-ink-color($custom-button-color); -} - -.custom-button--container-fill-color { - @include mdc-button-container-fill-color($custom-button-color); -} - -.custom-button--filled-accessible { - @include mdc-button-filled-accessible($custom-button-color); -} - -.custom-button--outline-color { - @include mdc-button-outline-color($custom-button-color); -} - -.custom-button--outline-width { - @include mdc-button-outline-width($custom-button-custom-outline-width); -} - -.custom-button--corner-radius { - @include mdc-button-corner-radius($custom-button-custom-corner-radius); -} - -.custom-button--icon-color { - @include mdc-button-icon-color($custom-button-color); -} - -.custom-button--horizontal-padding { - @include mdc-button-horizontal-padding($custom-button-custom-horizontal-padding); -} diff --git a/test/screenshot/spec/mdc-button/fixture.scss b/test/screenshot/spec/mdc-button/fixture.scss index 05771f18ae4..94f97789d69 100644 --- a/test/screenshot/spec/mdc-button/fixture.scss +++ b/test/screenshot/spec/mdc-button/fixture.scss @@ -14,7 +14,47 @@ // limitations under the License. // +@import "../../../../packages/mdc-button/mixins"; +@import "../../../../packages/mdc-theme/color-palette"; + +$custom-button-color: $material-color-red-300; +$custom-button-custom-corner-radius: 8px; +$custom-button-custom-outline-width: 4px; +$custom-button-custom-horizontal-padding: 24px; + .test-cell--button { width: 171px; height: 71px; } + +.custom-button--ink-color { + @include mdc-button-ink-color($custom-button-color); +} + +.custom-button--container-fill-color { + @include mdc-button-container-fill-color($custom-button-color); +} + +.custom-button--filled-accessible { + @include mdc-button-filled-accessible($custom-button-color); +} + +.custom-button--outline-color { + @include mdc-button-outline-color($custom-button-color); +} + +.custom-button--outline-width { + @include mdc-button-outline-width($custom-button-custom-outline-width); +} + +.custom-button--corner-radius { + @include mdc-button-corner-radius($custom-button-custom-corner-radius); +} + +.custom-button--icon-color { + @include mdc-button-icon-color($custom-button-color); +} + +.custom-button--horizontal-padding { + @include mdc-button-horizontal-padding($custom-button-custom-horizontal-padding); +} diff --git a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html index 64162071d2c..b429d2f8b2f 100644 --- a/test/screenshot/spec/mdc-button/mixins/container-fill-color.html +++ b/test/screenshot/spec/mdc-button/mixins/container-fill-color.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/corner-radius.html b/test/screenshot/spec/mdc-button/mixins/corner-radius.html index aa080ddd369..3773867ecde 100644 --- a/test/screenshot/spec/mdc-button/mixins/corner-radius.html +++ b/test/screenshot/spec/mdc-button/mixins/corner-radius.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html index 0bfdf3931e9..fd953216b22 100644 --- a/test/screenshot/spec/mdc-button/mixins/filled-accessible.html +++ b/test/screenshot/spec/mdc-button/mixins/filled-accessible.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html index ca042b9f4ea..3ddf5e79bf4 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-baseline.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html index a3f10d6c8be..7625528a577 100644 --- a/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html +++ b/test/screenshot/spec/mdc-button/mixins/horizontal-padding-dense.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/icon-color.html b/test/screenshot/spec/mdc-button/mixins/icon-color.html index 26146492ebe..11bca0fccc2 100644 --- a/test/screenshot/spec/mdc-button/mixins/icon-color.html +++ b/test/screenshot/spec/mdc-button/mixins/icon-color.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/ink-color.html b/test/screenshot/spec/mdc-button/mixins/ink-color.html index e6f36f464ad..f6e71ff9263 100644 --- a/test/screenshot/spec/mdc-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-button/mixins/ink-color.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-color.html b/test/screenshot/spec/mdc-button/mixins/stroke-color.html index de1c6c7b9d5..b82c581a1d8 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-color.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-color.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-button/mixins/stroke-width.html b/test/screenshot/spec/mdc-button/mixins/stroke-width.html index f94439b64c3..5fed1c11dd7 100644 --- a/test/screenshot/spec/mdc-button/mixins/stroke-width.html +++ b/test/screenshot/spec/mdc-button/mixins/stroke-width.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-fab/custom.scss b/test/screenshot/spec/mdc-fab/custom.scss deleted file mode 100644 index d55f8b1414c..00000000000 --- a/test/screenshot/spec/mdc-fab/custom.scss +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright 2018 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -@import "../../../packages/mdc-fab/mixins"; - -.custom-fab--extended-padding-and-icon-size { - @include mdc-fab-extended-padding(12px, 24px); - @include mdc-fab-icon-size(32px); -} - -.custom-fab--extended-padding { - @include mdc-fab-extended-padding(16px, 24px); -} - -.custom-fab--extended-label-padding { - @include mdc-fab-extended-label-padding(24px); -} diff --git a/test/screenshot/spec/mdc-fab/fixture.scss b/test/screenshot/spec/mdc-fab/fixture.scss index 75aaf9a1093..40a9fdb73d5 100644 --- a/test/screenshot/spec/mdc-fab/fixture.scss +++ b/test/screenshot/spec/mdc-fab/fixture.scss @@ -14,6 +14,8 @@ // limitations under the License. // +@import "../../../../packages/mdc-fab/mixins"; + .test-cell--fab { width: 81px; height: 81px; @@ -23,3 +25,16 @@ width: 191px; height: 71px; } + +.custom-fab--extended-padding-and-icon-size { + @include mdc-fab-extended-padding(12px, 24px); + @include mdc-fab-icon-size(32px); +} + +.custom-fab--extended-padding { + @include mdc-fab-extended-padding(16px, 24px); +} + +.custom-fab--extended-label-padding { + @include mdc-fab-extended-label-padding(24px); +} diff --git a/test/screenshot/spec/mdc-fab/mixins/extended-padding.html b/test/screenshot/spec/mdc-fab/mixins/extended-padding.html index a878f220f27..bbcf233b780 100644 --- a/test/screenshot/spec/mdc-fab/mixins/extended-padding.html +++ b/test/screenshot/spec/mdc-fab/mixins/extended-padding.html @@ -22,7 +22,6 @@ - diff --git a/test/screenshot/spec/mdc-icon-button/custom.scss b/test/screenshot/spec/mdc-icon-button/custom.scss deleted file mode 100644 index 0d20454c37d..00000000000 --- a/test/screenshot/spec/mdc-icon-button/custom.scss +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright 2018 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -@import "../../../packages/mdc-icon-button/mixins"; -@import "../../../packages/mdc-theme/color-palette"; - -$custom-icon-button-icon-ink-color: $material-color-red-500; -$custom-icon-button-size: 36px; - -.custom-icon-button--ink-color { - @include mdc-icon-button-ink-color($custom-icon-button-icon-ink-color); -} - -.custom-icon-button--icon-size { - @include mdc-icon-button-size($custom-icon-button-size); -} diff --git a/test/screenshot/spec/mdc-icon-button/fixture.scss b/test/screenshot/spec/mdc-icon-button/fixture.scss index f81a618f3a0..22570208188 100644 --- a/test/screenshot/spec/mdc-icon-button/fixture.scss +++ b/test/screenshot/spec/mdc-icon-button/fixture.scss @@ -14,7 +14,21 @@ // limitations under the License. // +@import "../../../../packages/mdc-icon-button/mixins"; +@import "../../../../packages/mdc-theme/color-palette"; + +$custom-icon-button-icon-ink-color: $material-color-red-500; +$custom-icon-button-size: 36px; + .test-cell--icon-button { width: 91px; height: 91px; } + +.custom-icon-button--ink-color { + @include mdc-icon-button-ink-color($custom-icon-button-icon-ink-color); +} + +.custom-icon-button--icon-size { + @include mdc-icon-button-size($custom-icon-button-size); +} diff --git a/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html index 10206214532..04bb079ba24 100644 --- a/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/icon-size.html @@ -23,7 +23,6 @@ -
    diff --git a/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html index 2e2103c9823..fd40e429653 100644 --- a/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-icon-button/mixins/ink-color.html @@ -23,7 +23,6 @@ -
    From 66f8bf76cdb17902e7b21dddf83860fdf8d30c1c Mon Sep 17 00:00:00 2001 From: Esteban Gonzalez Date: Wed, 18 Jul 2018 10:30:58 -0700 Subject: [PATCH 31/53] feat(floating-label): Add max-width mixin (#2956) --- packages/mdc-floating-label/README.md | 1 + packages/mdc-floating-label/_mixins.scss | 4 ++++ packages/mdc-floating-label/mdc-floating-label.scss | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/packages/mdc-floating-label/README.md b/packages/mdc-floating-label/README.md index 7a77b27ffa2..41d06fe5967 100644 --- a/packages/mdc-floating-label/README.md +++ b/packages/mdc-floating-label/README.md @@ -79,6 +79,7 @@ Mixin | Description `mdc-floating-label-shake-keyframes($modifier, $positionY, $positionX, $scale)` | Generates a CSS `@keyframes` at-rule for an invalid label shake. Used in conjunction with the `mdc-floating-label-shake-animation` mixin. `mdc-floating-label-shake-animation($modifier)` | Applies shake keyframe animation to label. `mdc-floating-label-float-position($positionY, $positionX, $scale)` | Sets position of label when floating. +`mdc-floating-label-max-width($max-width)` | Sets the max width of the label. ## `MDCFloatingLabel` Properties and Methods diff --git a/packages/mdc-floating-label/_mixins.scss b/packages/mdc-floating-label/_mixins.scss index 92bcab3ce45..bc7b35c2dff 100644 --- a/packages/mdc-floating-label/_mixins.scss +++ b/packages/mdc-floating-label/_mixins.scss @@ -67,3 +67,7 @@ animation: mdc-floating-label-shake-float-above-#{$modifier} 250ms 1; } } + +@mixin mdc-floating-label-max-width($max-width) { + max-width: $max-width; +} diff --git a/packages/mdc-floating-label/mdc-floating-label.scss b/packages/mdc-floating-label/mdc-floating-label.scss index f8bf56b7f81..3f16d028579 100644 --- a/packages/mdc-floating-label/mdc-floating-label.scss +++ b/packages/mdc-floating-label/mdc-floating-label.scss @@ -37,7 +37,13 @@ transform $mdc-floating-label-transition-duration $mdc-animation-standard-curve-timing-function, color $mdc-floating-label-transition-duration $mdc-animation-standard-curve-timing-function; line-height: 1.15rem; + text-overflow: ellipsis; + white-space: nowrap; cursor: text; + overflow: hidden; + // Force the label into its own layer to prevent visible layer promotion adjustments + // when the ripple is activated behind it. + will-change: transform; @include mdc-rtl { /* @noflip */ From 9754dda83b19e96c36a8e1e0297857e99a80facd Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Wed, 18 Jul 2018 10:42:59 -0700 Subject: [PATCH 32/53] chore(infrastructure): Reduce noisy Travis job log output (#3131) ### What it does A few recent Travis jobs have had their job logs truncated for being too long. Let's fix that. - Reduces Travis job log output from ~8k lines to ~2k lines - Suppresses noisy log output from `install` step - Only runs `npm install` (but not `npm ls`) - Travis runs `npm ls` by default, which produces about 4k log lines. This makes the log output harder to read and slower to load in the UI, and it doesn't seem terribly useful. - Suppresses noisy log output from gcloud installer's `tar` command - Makes the log output easier to read and faster to load in the Travis job log UI - Colorizes log output for "parallel execution limit reached" warning messages --- .travis.yml | 4 ++ test/screenshot/infra/commands/build.js | 24 +++++------ test/screenshot/infra/commands/travis.sh | 51 +++++++++++++---------- test/screenshot/infra/lib/selenium-api.js | 4 +- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index 27b6ff0e277..b52f6552228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,3 +38,7 @@ before_install: # script are visible to subsequent Travis CLI commands. # https://superuser.com/a/176788/62792 - source test/screenshot/infra/commands/travis.sh +install: + - rm -rf node_modules + - npm install + #- npm ls # Noisy output, but useful for debugging npm package dependency version issues diff --git a/test/screenshot/infra/commands/build.js b/test/screenshot/infra/commands/build.js index 65790b8c388..0cc872fb394 100644 --- a/test/screenshot/infra/commands/build.js +++ b/test/screenshot/infra/commands/build.js @@ -63,9 +63,11 @@ module.exports = { * @private */ buildProtoFiles_(shouldWatch) { - const compile = debounce(() => ProtoCommand.runAsync(), 1000); + const buildRightNow = () => ProtoCommand.runAsync(); + const buildDelayed = debounce(buildRightNow, 1000); + if (!shouldWatch) { - compile(); + buildRightNow(); return; } @@ -74,10 +76,8 @@ module.exports = { awaitWriteFinish: true, }); - /* eslint-disable no-unused-vars */ - watcher.on('add', (filePath) => compile()); - watcher.on('change', (filePath) => compile()); - /* eslint-enable no-unused-vars */ + watcher.on('add', buildDelayed); + watcher.on('change', buildDelayed); }, /** @@ -85,9 +85,11 @@ module.exports = { * @private */ buildHtmlFiles_(shouldWatch) { - const compile = debounce(() => IndexCommand.runAsync(), 1000); + const buildRightNow = () => IndexCommand.runAsync(); + const buildDelayed = debounce(buildRightNow, 1000); + if (!shouldWatch) { - compile(); + buildRightNow(); return; } @@ -97,10 +99,8 @@ module.exports = { ignored: ['**/report/report.html', '**/index.html'], }); - /* eslint-disable no-unused-vars */ - watcher.on('add', (filePath) => compile()); - watcher.on('unlink', (filePath) => compile()); - /* eslint-enable no-unused-vars */ + watcher.on('add', buildDelayed); + watcher.on('unlink', buildDelayed); }, /** diff --git a/test/screenshot/infra/commands/travis.sh b/test/screenshot/infra/commands/travis.sh index 92c1829a60a..934f8ae01ee 100755 --- a/test/screenshot/infra/commands/travis.sh +++ b/test/screenshot/infra/commands/travis.sh @@ -13,47 +13,52 @@ function exit_if_external_pr() { fi } +function print_travis_env_vars() { + echo + env | grep TRAVIS + echo +} + function extract_api_credentials() { openssl aes-256-cbc -K $encrypted_eead2343bb54_key -iv $encrypted_eead2343bb54_iv \ -in test/screenshot/infra/auth/travis.tar.enc -out test/screenshot/infra/auth/travis.tar -d tar -xf test/screenshot/infra/auth/travis.tar -C test/screenshot/infra/auth/ - - echo - echo 'git status:' - echo - git status - echo - env | grep TRAVIS - echo } function install_google_cloud_sdk() { - if [[ ! -d $HOME/google-cloud-sdk ]]; then + export PATH=$PATH:$HOME/google-cloud-sdk/bin + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + which gcloud 2>&1 > /dev/null + + if [[ $? == 0 ]]; then + echo 'gcloud already installed' + echo + else + echo 'gcloud not installed' + echo + + rm -rf $HOME/google-cloud-sdk curl -o /tmp/gcp-sdk.bash https://sdk.cloud.google.com chmod +x /tmp/gcp-sdk.bash - /tmp/gcp-sdk.bash --disable-prompts - fi - export PATH=$PATH:$HOME/google-cloud-sdk/bin - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + # The gcloud installer runs `tar -C "$install_dir" -zxvf "$download_dst"`, which generates a lot of noisy output. + # Filter out all lines from `tar`. + /tmp/gcp-sdk.bash | grep -v -E '^google-cloud-sdk/' + fi gcloud auth activate-service-account --key-file test/screenshot/infra/auth/gcs.json gcloud config set project material-components-web gcloud components install gsutil - - which gsutil 2>&1 > /dev/null - if [[ $? != 0 ]]; then - pip install --upgrade pip - pip install gsutil - fi + gcloud components update gsutil } -if [[ "$TEST_SUITE" == 'screenshot' ]] || [[ "$TEST_SUITE" == 'unit' ]]; then +if [[ "$TEST_SUITE" == 'unit' ]]; then exit_if_external_pr -fi - -if [[ "$TEST_SUITE" == 'screenshot' ]]; then +elif [[ "$TEST_SUITE" == 'screenshot' ]]; then + exit_if_external_pr + print_travis_env_vars extract_api_credentials install_google_cloud_sdk fi diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 25d4cb1cee6..696c455ed64 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -49,6 +49,7 @@ const {SELENIUM_FONT_LOAD_WAIT_MS} = Constants; const CliStatuses = { ACTIVE: {name: 'Active', color: colors.bold.cyan}, QUEUED: {name: 'Queued', color: colors.cyan}, + WAITING: {name: 'Waiting', color: colors.magenta}, STARTING: {name: 'Starting', color: colors.green}, STARTED: {name: 'Started', color: colors.bold.green}, GET: {name: 'Get', color: colors.bold.white}, @@ -202,7 +203,8 @@ class SeleniumApi { const waitTimeMs = CBT_CONCURRENCY_POLL_INTERVAL_MS; const waitTimeHuman = Duration.millis(waitTimeMs).toHumanShort(); - console.warn( + this.logStatus_( + CliStatuses.WAITING, `Parallel execution limit reached. ${max} tests are already running on CBT. Will retry in ${waitTimeHuman}...` ); await this.sleep_(waitTimeMs); From fe488a98758940f15a76260b782c461d58b414f2 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Wed, 18 Jul 2018 13:25:07 -0700 Subject: [PATCH 33/53] chore(checkbox): add screenshot tests for baseline checkbox --- test/screenshot/golden.json | 9 ++ .../spec/mdc-checkbox/classes/baseline.html | 147 ++++++++++++++++++ test/screenshot/spec/mdc-checkbox/fixture.js | 2 + .../screenshot/spec/mdc-checkbox/fixture.scss | 20 +++ 4 files changed, 178 insertions(+) create mode 100644 test/screenshot/spec/mdc-checkbox/classes/baseline.html create mode 100644 test/screenshot/spec/mdc-checkbox/fixture.js create mode 100644 test/screenshot/spec/mdc-checkbox/fixture.scss diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index f4a4911aaa5..cc5a128f8c1 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -152,6 +152,15 @@ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-button/mixins/stroke-width.html.windows_ie_11.png" } }, + "spec/mdc-checkbox/classes/baseline.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/mattgoo/2018/07/18/19_29_49_289/spec/mdc-checkbox/classes/baseline.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/mattgoo/2018/07/18/19_29_49_289/spec/mdc-checkbox/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/mattgoo/2018/07/18/19_29_49_289/spec/mdc-checkbox/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/mattgoo/2018/07/18/19_29_49_289/spec/mdc-checkbox/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/mattgoo/2018/07/18/19_29_49_289/spec/mdc-checkbox/classes/baseline.html.windows_ie_11.png" + } + }, "spec/mdc-drawer/classes/permanent.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/permanent.html", "screenshots": { diff --git a/test/screenshot/spec/mdc-checkbox/classes/baseline.html b/test/screenshot/spec/mdc-checkbox/classes/baseline.html new file mode 100644 index 00000000000..786ffc361e5 --- /dev/null +++ b/test/screenshot/spec/mdc-checkbox/classes/baseline.html @@ -0,0 +1,147 @@ + + + + + + Baseline Checkbox - MDC Web Screenshot Test + + + + + + + +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    + +
    + + + +
    +
    +
    +
    + +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + diff --git a/test/screenshot/spec/mdc-checkbox/fixture.js b/test/screenshot/spec/mdc-checkbox/fixture.js new file mode 100644 index 00000000000..2d10e4025c9 --- /dev/null +++ b/test/screenshot/spec/mdc-checkbox/fixture.js @@ -0,0 +1,2 @@ +document.getElementById('checkbox-indeterminate').indeterminate = true; +document.getElementById('checkbox-indeterminate-disabled').indeterminate = true; diff --git a/test/screenshot/spec/mdc-checkbox/fixture.scss b/test/screenshot/spec/mdc-checkbox/fixture.scss new file mode 100644 index 00000000000..12509de7371 --- /dev/null +++ b/test/screenshot/spec/mdc-checkbox/fixture.scss @@ -0,0 +1,20 @@ +// +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +.test-cell--checkbox { + width: 91px; + height: 91px; +} From 54c9c94be2e1458e1d6876d38eb5ee708ebbc947 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Wed, 18 Jul 2018 16:49:49 -0700 Subject: [PATCH 34/53] chore(infrastructure): Kill user's stalled Selenium tests on startup (#3140) ### What it does - Before running any tests, checks if the authenticated CBT user has any "active" Selenium tests that haven't received a command in over 2 minutes. If so, they're probably stalled - kill 'em! - If the user presses Ctrl+C while the Selenium tests are running, the VMs are killed before the process exits. #### Caveats - If the Node process exits abnormally (e.g., `kill -9` or a cancelled Travis job), the Selenium VMs will continue to run as zombies for 24 hours or until manually killed by the user. AFAIK there's nothing we can do about this. - If the HTTP requests to CBT take a long time to complete, Node may hang indefinitely before exiting. - We can only kill Selenium tests that were started by the same user. E.g., Ken can't kill Andy's stale tests. ### Example output ![image](https://user-images.githubusercontent.com/409245/42911903-b523f156-8aa1-11e8-81db-646a4e14539c.png) --- test/screenshot/infra/lib/cbt-api.js | 50 +++++++- test/screenshot/infra/lib/constants.js | 15 ++- test/screenshot/infra/lib/controller.js | 10 ++ test/screenshot/infra/lib/externs.js | 148 ++++++++++++++++++++++ test/screenshot/infra/lib/selenium-api.js | 48 ++++++- 5 files changed, 265 insertions(+), 6 deletions(-) diff --git a/test/screenshot/infra/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js index 5972f2e719c..968fc8c4d3f 100644 --- a/test/screenshot/infra/lib/cbt-api.js +++ b/test/screenshot/infra/lib/cbt-api.js @@ -14,6 +14,7 @@ * limitations under the License. */ +const colors = require('colors'); const request = require('request-promise-native'); const mdcProto = require('../proto/mdc.pb').mdc.proto; @@ -26,12 +27,13 @@ const {CbtAccount, CbtActiveTestCounts, CbtConcurrencyStats} = cbtProto; const {RawCapabilities} = seleniumProto; const Cli = require('./cli'); +const Duration = require('./duration'); const MDC_CBT_USERNAME = process.env.MDC_CBT_USERNAME; const MDC_CBT_AUTHKEY = process.env.MDC_CBT_AUTHKEY; const REST_API_BASE_URL = 'https://crossbrowsertesting.com/api/v3'; const SELENIUM_SERVER_URL = `http://${MDC_CBT_USERNAME}:${MDC_CBT_AUTHKEY}@hub.crossbrowsertesting.com:80/wd/hub`; -const {ExitCode} = require('./constants'); +const {ExitCode, SELENIUM_STALLED_TIME_MS} = require('./constants'); /** @type {?Promise>} */ let allBrowsersPromise; @@ -348,6 +350,52 @@ https://crossbrowsertesting.com/account }; } + /** + * @return {!Promise} + */ + async killStalledSeleniumTests() { + // NOTE: This only returns Selenium tests running on the authenticated CBT user's account. + // It does NOT return Selenium tests running under other users. + /** @type {!CbtSeleniumListResponse} */ + const listResponse = await this.sendRequest_('GET', '/selenium?active=true&num=100'); + + const activeSeleniumTestIds = listResponse.selenium.map((test) => test.selenium_test_id); + + /** @type {!Array} */ + const infoResponses = await Promise.all(activeSeleniumTestIds.map((seleniumTestId) => { + return this.sendRequest_('GET', `/selenium/${seleniumTestId}`); + })); + + const stalledSeleniumTestIds = []; + + for (const infoResponse of infoResponses) { + const lastCommand = infoResponse.commands[infoResponse.commands.length - 1]; + if (!lastCommand) { + continue; + } + + const commandTimestampMs = new Date(lastCommand.date_issued).getTime(); + if (!Duration.hasElapsed(SELENIUM_STALLED_TIME_MS, commandTimestampMs)) { + continue; + } + + stalledSeleniumTestIds.push(infoResponse.selenium_test_id); + } + + await this.killSeleniumTests(stalledSeleniumTestIds); + } + + /** + * @param {!Array} seleniumTestIds + * @return {!Promise} + */ + async killSeleniumTests(seleniumTestIds) { + await Promise.all(seleniumTestIds.map((seleniumTestId) => { + console.log(`${colors.red('Killing')} zombie Selenium test ${colors.bold(seleniumTestId)}`); + return this.sendRequest_('DELETE', `/selenium/${seleniumTestId}`); + })); + } + /** * @param {string} method * @param {string} endpoint diff --git a/test/screenshot/infra/lib/constants.js b/test/screenshot/infra/lib/constants.js index ab45a42f63d..182c06e574b 100644 --- a/test/screenshot/infra/lib/constants.js +++ b/test/screenshot/infra/lib/constants.js @@ -51,7 +51,13 @@ module.exports = { * Number of milliseconds to wait for fonts to load on a test page in Selenium before giving up. * @type {number} */ - SELENIUM_FONT_LOAD_WAIT_MS: 3000, + SELENIUM_FONT_LOAD_WAIT_MS: 3 * 1000, // 3 seconds + + /** + * Number of milliseconds a Selenium test should wait to receive commands before being considered "stalled". + * @type {number} + */ + SELENIUM_STALLED_TIME_MS: 2 * 60 * 1000, // 2 minutes ExitCode: { OK: 0, @@ -61,8 +67,9 @@ module.exports = { UNSUPPORTED_CLI_COMMAND: 14, HTTP_PORT_ALREADY_IN_USE: 15, MISSING_ENV_VAR: 16, - UNHANDLED_PROMISE_REJECTION: 17, - CHANGES_FOUND: 18, - UNSUPPORTED_EXTERNAL_PR: 19, + UNSUPPORTED_EXTERNAL_PR: 17, + UNHANDLED_PROMISE_REJECTION: 18, + UNCAUGHT_EXCEPTION: 19, + CHANGES_FOUND: 20, }, }; diff --git a/test/screenshot/infra/lib/controller.js b/test/screenshot/infra/lib/controller.js index 9608f343566..21540e89303 100644 --- a/test/screenshot/infra/lib/controller.js +++ b/test/screenshot/infra/lib/controller.js @@ -19,6 +19,7 @@ const mdcProto = require('../proto/mdc.pb').mdc.proto; const {GitRevision} = mdcProto; +const CbtApi = require('./cbt-api'); const Cli = require('./cli'); const CloudStorage = require('./cloud-storage'); const Duration = require('./duration'); @@ -32,6 +33,12 @@ const {ExitCode} = require('./constants'); class Controller { constructor() { + /** + * @type {!CbtApi} + * @private + */ + this.cbtApi_ = new CbtApi(); + /** * @type {!Cli} * @private @@ -98,6 +105,9 @@ class Controller { if (isOnline && shouldFetch) { await this.gitRepo_.fetch(); } + if (isOnline) { + await this.cbtApi_.killStalledSeleniumTests(); + } return this.reportBuilder_.initForCapture(); } diff --git a/test/screenshot/infra/lib/externs.js b/test/screenshot/infra/lib/externs.js index c3d334fdd37..262e84553ea 100644 --- a/test/screenshot/infra/lib/externs.js +++ b/test/screenshot/infra/lib/externs.js @@ -89,6 +89,154 @@ */ +/* + * CBT (CrossBrowserTesting.com) + */ + + +/** + * @typedef {{ + * width: number, + * height: number, + * desktop_width: number, + * desktop_height: number, + * name: number, + * requested_name: number, + * }} CbtSeleniumResolution + */ + +/** + * @typedef {{ + * name: string, + * type: string, + * version: string, + * api_name: string, + * device: string, + * device_type: ?string, + * icon_class: string, + * requested_api_name: string, + * }} CbtSeleniumOs + */ + +/** + * @typedef {{ + * name: string, + * type: string, + * version: string, + * api_name: string, + * icon_class: string, + * requested_api_name: string, + * }} CbtSeleniumBrowser + */ + +/** + * @typedef {{ + * hash: string, + * date_added: string, + * is_finished: boolean, + * description: ?string, + * tags: !Array, + * show_result_web_url: ?string, + * show_result_public_url: ?string, + * video: ?string, + * image: ?string, + * thumbnail_image: ?string, + * }} CbtSeleniumVideo + */ + +/** + * @typedef {{ + * hash: string, + * date_added: string, + * is_finished: boolean, + * description: ?string, + * tags: !Array, + * show_result_web_url: ?string, + * show_result_public_url: ?string, + * video: ?string, + * pcap: ?string, + * har: ?string, + * }} CbtSeleniumNetwork + */ + +/** + * @typedef {{ + * body: string, + * method: string, + * path: string, + * date_issued: string, + * hash: ?string, + * response_code: number, + * response_body: ?string, + * step_number: ?number, + * }} CbtSeleniumCommand + */ + +/** + * @typedef {{ + * selenium: !Array, + * }} CbtSeleniumListResponse + */ + +/** + * @typedef {{ + * selenium_test_id: string, + * selenium_session_id: string, + * start_date: string, + * finish_date: ?string, + * test_score: string, + * active: boolean, + * state: string, + * show_result_web_url: ?string, + * show_result_public_url: ?string, + * download_results_zip_url: ?string, + * download_results_zip_public_url: ?string, + * launch_live_test_url: ?string, + * resolution: !CbtSeleniumResolution, + * os: !CbtSeleniumOs, + * browser: !CbtSeleniumBrowser, + * }} CbtSeleniumListItem + */ + +/** + * @typedef {{ + * selenium_test_id: string, + * selenium_session_id: string, + * start_date: string, + * finish_date: ?string, + * test_score: string, + * active: boolean, + * state: string, + * startup_finish_date: string, + * url: string, + * client_platform: string, + * client_browser: string, + * use_copyrect: boolean, + * scale: string, + * is_packet_capturing: number, + * tunnel_id: number, + * archived: number, + * selenium_version: string, + * show_result_web_url: ?string, + * show_result_public_url: ?string, + * download_results_zip_url: ?string, + * download_results_zip_public_url: ?string, + * launch_live_test_url: ?string, + * resolution: !CbtSeleniumResolution, + * os: !CbtSeleniumOs, + * browser: !CbtSeleniumBrowser, + * requestMethod: string, + * api_version: string, + * description: ?string, + * tags: !Array, + * videos: !Array, + * snapshots: !Array, + * networks: !Array, + * commands: !Array, + * }} CbtSeleniumInfoResponse + */ + + /* * Resemble.js API externs */ diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 696c455ed64..4d690b43ff0 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -37,7 +37,7 @@ const ImageCropper = require('./image-cropper'); const ImageDiffer = require('./image-differ'); const LocalStorage = require('./local-storage'); const {Browser, Builder, By, until} = require('selenium-webdriver'); -const {CBT_CONCURRENCY_POLL_INTERVAL_MS, CBT_CONCURRENCY_MAX_WAIT_MS} = Constants; +const {CBT_CONCURRENCY_POLL_INTERVAL_MS, CBT_CONCURRENCY_MAX_WAIT_MS, ExitCode} = Constants; const {SELENIUM_FONT_LOAD_WAIT_MS} = Constants; /** @@ -91,6 +91,14 @@ class SeleniumApi { * @private */ this.localStorage_ = new LocalStorage(); + + /** + * @type {!Set} + * @private + */ + this.seleniumSessionIds_ = new Set(); + + this.killBrowsersOnExit_(); } /** @@ -151,6 +159,8 @@ class SeleniumApi { const seleniumSessionId = session.getId(); let changedScreenshots; + this.seleniumSessionIds_.add(seleniumSessionId); + const logResult = (status) => { /* eslint-disable camelcase */ const {os_name, os_version, browser_name, browser_version} = userAgent.navigator; @@ -167,6 +177,7 @@ class SeleniumApi { } finally { logResult(CliStatuses.QUITTING); await driver.quit(); + this.seleniumSessionIds_.delete(seleniumSessionId); } await this.cbtApi_.setTestScore({ @@ -539,6 +550,41 @@ class SeleniumApi { return croppedImageBuffer; } + /** @private */ + killBrowsersOnExit_() { + const killThemAll = async () => { + console.log('Killing Selenium tests:', this.seleniumSessionIds_); + await this.cbtApi_.killSeleniumTests(Array.from(this.seleniumSessionIds_)); + console.log('Killed Selenium tests!'); + // Give the HTTP requests a chance to complete before exiting + await this.sleep_(Duration.seconds(1).toMillis()); + }; + + // catches ctrl+c event + process.on('SIGINT', () => { + const exit = () => process.exit(ExitCode.SIGINT); + killThemAll().then(exit, exit); + }); + + // catches "kill pid" + process.on('SIGTERM', () => { + const exit = () => process.exit(ExitCode.SIGTERM); + killThemAll().then(exit, exit); + }); + + process.on('uncaughtException', (err) => { + console.error(err); + const exit = () => process.exit(ExitCode.UNCAUGHT_EXCEPTION); + killThemAll().then(exit, exit); + }); + + process.on('unhandledRejection', (err) => { + console.error(err); + const exit = () => process.exit(ExitCode.UNHANDLED_PROMISE_REJECTION); + killThemAll().then(exit, exit); + }); + } + /** * @param {number} ms * @return {!Promise} From 9aca09246e86b55f5965d3fe86883eabf067ed30 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Thu, 19 Jul 2018 18:02:33 -0400 Subject: [PATCH 35/53] chore(text-field): Add baseline screenshot test page (#3137) --- test/screenshot/golden.json | 9 +++ .../classes/baseline-textfield.html | 61 +++++++++++++++++++ test/screenshot/spec/mdc-textfield/fixture.js | 3 + .../spec/mdc-textfield/fixture.scss | 23 +++++++ 4 files changed, 96 insertions(+) create mode 100644 test/screenshot/spec/mdc-textfield/classes/baseline-textfield.html create mode 100644 test/screenshot/spec/mdc-textfield/fixture.js create mode 100644 test/screenshot/spec/mdc-textfield/fixture.scss diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index cc5a128f8c1..c1a58d54deb 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -277,5 +277,14 @@ "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_firefox_61.png", "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" } + }, + "spec/mdc-textfield/classes/baseline-textfield.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html.windows_ie_11.png" + } } } diff --git a/test/screenshot/spec/mdc-textfield/classes/baseline-textfield.html b/test/screenshot/spec/mdc-textfield/classes/baseline-textfield.html new file mode 100644 index 00000000000..1be4e61a6ac --- /dev/null +++ b/test/screenshot/spec/mdc-textfield/classes/baseline-textfield.html @@ -0,0 +1,61 @@ + + + + + + Baseline Text Field Element - MDC Web Screenshot Test + + + + + + + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    + + +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + diff --git a/test/screenshot/spec/mdc-textfield/fixture.js b/test/screenshot/spec/mdc-textfield/fixture.js new file mode 100644 index 00000000000..1f072ea4821 --- /dev/null +++ b/test/screenshot/spec/mdc-textfield/fixture.js @@ -0,0 +1,3 @@ +[].forEach.call(document.querySelectorAll('.mdc-text-field'), function(el) { + mdc.textField.MDCTextField.attachTo(el); +}); diff --git a/test/screenshot/spec/mdc-textfield/fixture.scss b/test/screenshot/spec/mdc-textfield/fixture.scss new file mode 100644 index 00000000000..4f9ed82738b --- /dev/null +++ b/test/screenshot/spec/mdc-textfield/fixture.scss @@ -0,0 +1,23 @@ +// +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import "../../../../packages/mdc-textfield/mixins"; +@import "../../../../packages/mdc-theme/color-palette"; + +.test-cell--textfield { + width: 301px; + height: 101px; +} From 539329161f5c2e4c6dbfbdd891becd1e1c4a6951 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Thu, 19 Jul 2018 16:48:32 -0700 Subject: [PATCH 36/53] chore(infrastructure): Refactor screenshot code (#3148) ### What it does - Moves `--diff-base` parsing out of `Cli` into new `DiffBaseParser` class - Kills all Selenium tests when an error is thrown by one of them - Prints progress on the console - Avoids scrollbars in drawer test pages by setting consistent paragraph `margin` and `line-height` - Updates `golden.json` - Prints more helpful error messages for external PRs in Travis (links to a Travis support article) - Checks network connectivity and fetches CBT browsers once (at startup) and caches the result - Automatically disables retries for `--offline` tests - Adds `types/` directory and splits up `types.js` into separate `*.js` files ### Example output ![image](https://user-images.githubusercontent.com/409245/42974121-d5e580fc-8b6a-11e8-90b4-54dd37902d48.png) --- test/screenshot/golden.json | 48 +-- test/screenshot/infra/commands/travis.sh | 17 +- test/screenshot/infra/lib/cbt-api.js | 11 +- test/screenshot/infra/lib/cli.js | 335 ++---------------- test/screenshot/infra/lib/cloud-storage.js | 4 +- test/screenshot/infra/lib/constants.js | 24 +- test/screenshot/infra/lib/controller.js | 15 +- test/screenshot/infra/lib/diff-base-parser.js | 332 +++++++++++++++++ test/screenshot/infra/lib/golden-io.js | 25 +- test/screenshot/infra/lib/logger.js | 6 +- test/screenshot/infra/lib/report-builder.js | 29 +- test/screenshot/infra/lib/selenium-api.js | 135 +++++-- test/screenshot/infra/lib/user-agent-store.js | 2 +- .../externs.js => types/cbt-api-externs.js} | 157 +------- test/screenshot/infra/types/cli-types.js | 26 ++ .../infra/types/node-api-externs.js | 90 +++++ .../screenshot/infra/types/report-ui-types.js | 69 ++++ test/screenshot/run.js | 43 +-- .../spec/mdc-drawer/classes/permanent.html | 8 +- .../spec/mdc-drawer/classes/persistent.html | 8 +- .../spec/mdc-drawer/classes/temporary.html | 8 +- test/screenshot/spec/mdc-drawer/fixture.scss | 5 + .../mixins/fill-color-accessible.html | 8 +- .../spec/mdc-drawer/mixins/fill-color.html | 8 +- .../spec/mdc-drawer/mixins/ink-color.html | 8 +- 25 files changed, 800 insertions(+), 621 deletions(-) create mode 100644 test/screenshot/infra/lib/diff-base-parser.js rename test/screenshot/infra/{lib/externs.js => types/cbt-api-externs.js} (57%) create mode 100644 test/screenshot/infra/types/cli-types.js create mode 100644 test/screenshot/infra/types/node-api-externs.js create mode 100644 test/screenshot/infra/types/report-ui-types.js diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index c1a58d54deb..d57ae5718be 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -164,55 +164,55 @@ "spec/mdc-drawer/classes/permanent.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/permanent.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/permanent.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/permanent.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/permanent.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/permanent.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/permanent.html.windows_ie_11.png" } }, "spec/mdc-drawer/classes/persistent.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/persistent.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/persistent.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/persistent.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/persistent.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/persistent.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/persistent.html.windows_ie_11.png" } }, "spec/mdc-drawer/classes/temporary.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/classes/temporary.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/classes/temporary.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/temporary.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/temporary.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/temporary.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/classes/temporary.html.windows_ie_11.png" } }, "spec/mdc-drawer/mixins/fill-color-accessible.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/mixins/fill-color-accessible.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color-accessible.html.windows_ie_11.png" } }, "spec/mdc-drawer/mixins/fill-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/mixins/fill-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/fill-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/fill-color.html.windows_ie_11.png" } }, "spec/mdc-drawer/mixins/ink-color.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/21_05_09_679/spec/mdc-drawer/mixins/ink-color.html", "screenshots": { - "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_chrome_67.png", - "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_edge_17.png", - "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_firefox_61.png", - "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-drawer/mixins/ink-color.html.windows_ie_11.png" + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/ink-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/ink-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/ink-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/19/21_11_42_130/spec/mdc-drawer/mixins/ink-color.html.windows_ie_11.png" } }, "spec/mdc-fab/classes/baseline.html": { diff --git a/test/screenshot/infra/commands/travis.sh b/test/screenshot/infra/commands/travis.sh index 934f8ae01ee..c7b6c338d3d 100755 --- a/test/screenshot/infra/commands/travis.sh +++ b/test/screenshot/infra/commands/travis.sh @@ -1,15 +1,22 @@ #!/usr/bin/env bash -function print_error() { - echo -e "\033[31m\033[1m$@\033[0m" +function print_stderr() { + echo "$@" >&2 +} + +function log_error() { + print_stderr -e "\033[31m\033[1m$@\033[0m" } function exit_if_external_pr() { if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then echo - print_error "Error: $TEST_SUITE tests are not supported on external PRs." - print_error "Skipping $TEST_SUITE tests." - exit 19 + log_error "ERROR: $TEST_SUITE tests are not supported on external PRs for security reasons." + print_stderr + print_stderr "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions" + print_stderr + log_error "Skipping $TEST_SUITE tests." + exit 10 fi } diff --git a/test/screenshot/infra/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js index 968fc8c4d3f..4100ab5c4f4 100644 --- a/test/screenshot/infra/lib/cbt-api.js +++ b/test/screenshot/infra/lib/cbt-api.js @@ -26,7 +26,7 @@ const {FormFactorType, OsVendorType, BrowserVendorType, BrowserVersionType} = Us const {CbtAccount, CbtActiveTestCounts, CbtConcurrencyStats} = cbtProto; const {RawCapabilities} = seleniumProto; -const Cli = require('./cli'); +const DiffBaseParser = require('./diff-base-parser'); const Duration = require('./duration'); const MDC_CBT_USERNAME = process.env.MDC_CBT_USERNAME; @@ -41,10 +41,10 @@ let allBrowsersPromise; class CbtApi { constructor() { /** - * @type {!Cli} + * @type {!DiffBaseParser} * @private */ - this.cli_ = new Cli(); + this.diffBaseParser_ = new DiffBaseParser(); this.validateEnvVars_(); } @@ -140,9 +140,8 @@ https://crossbrowsertesting.com/account * @param {!mdc.proto.ReportMeta} meta * @param {!mdc.proto.UserAgent} userAgent * @return {!Promise} - * @private */ - async getDesiredCapabilities_({meta, userAgent}) { + async getDesiredCapabilities({meta, userAgent}) { // TODO(acdvorak): Create a type for this /** @type {{device: !cbt.proto.CbtDevice, browser: !cbt.proto.CbtBrowser}} */ const matchingCbtUserAgent = await this.getMatchingCbtUserAgent_(userAgent); @@ -316,7 +315,7 @@ https://crossbrowsertesting.com/account */ async getCbtTestNameAndBuildNameForReport_(meta) { /** @type {?mdc.proto.GitRevision} */ - const travisGitRev = await this.cli_.getTravisGitRevision(); + const travisGitRev = await this.diffBaseParser_.getTravisGitRevision(); if (travisGitRev) { return this.getCbtTestNameAndBuildNameForGitRev_(travisGitRev); } diff --git a/test/screenshot/infra/lib/cli.js b/test/screenshot/infra/lib/cli.js index ff34ace108f..17589d2a077 100644 --- a/test/screenshot/infra/lib/cli.js +++ b/test/screenshot/infra/lib/cli.js @@ -17,39 +17,19 @@ 'use strict'; const mdcProto = require('../proto/mdc.pb').mdc.proto; -const {ApprovalId, DiffBase, GitRevision} = mdcProto; +const {ApprovalId} = mdcProto; const argparse = require('argparse'); const checkIsOnline = require('is-online'); -const fs = require('mz/fs'); -const {GOLDEN_JSON_RELATIVE_PATH} = require('./constants'); const Duration = require('./duration'); -const GitHubApi = require('./github-api'); -const GitRepo = require('./git-repo'); +const {GOLDEN_JSON_RELATIVE_PATH} = require('./constants'); -const HTTP_URL_REGEX = new RegExp('^https?://'); +/** @type {?boolean} */ +let isOnlineCached; class Cli { constructor() { - /** - * @type {!GitHubApi} - * @private - */ - this.gitHubApi_ = new GitHubApi(); - - /** - * @type {!GitRepo} - * @private - */ - this.gitRepo_ = new GitRepo(); - - /** - * @type {?boolean} - * @private - */ - this.isOnlineCached_ = null; - /** * @type {!ArgumentParser} * @private @@ -79,6 +59,20 @@ class Cli { this.args_ = this.rootParser_.parseArgs(); } + /** + * @return {!Promise} + */ + async checkIsOnline() { + if (typeof isOnlineCached !== 'boolean') { + if (this.offline) { + isOnlineCached = false; + } else { + isOnlineCached = await checkIsOnline({timeout: Duration.seconds(5).toMillis()}); + } + } + return isOnlineCached; + } + /** * @param {!ArgumentParser|!ActionContainer} parser * @param {!CliOptionConfig} config @@ -472,298 +466,17 @@ E.g.: '--browser=chrome,-mobile' is the same as '--browser=chrome --browser=-mob } /** - * @return {!Promise} - */ - async isOnline() { - if (this.offline) { - return false; - } - - if (typeof this.isOnlineCached_ !== 'boolean') { - this.isOnlineCached_ = await checkIsOnline({timeout: Duration.seconds(5).toMillis()}); - } - - return this.isOnlineCached_; - } - - /** - * TODO(acdvorak): Move this method out of Cli class - it doesn't belong here. - * @return {!Promise} - */ - async parseGoldenDiffBase() { - /** @type {?mdc.proto.GitRevision} */ - const travisGitRevision = await this.getTravisGitRevision(); - if (travisGitRevision) { - return DiffBase.create({ - type: DiffBase.Type.GIT_REVISION, - git_revision: travisGitRevision, - }); - } - return this.parseDiffBase(); - } - - /** - * TODO(acdvorak): Move this method out of Cli class - it doesn't belong here. - * @param {string} rawDiffBase - * @return {!Promise} - */ - async parseDiffBase(rawDiffBase = this.diffBase) { - const isOnline = await this.isOnline(); - const isRealBranch = (branch) => Boolean(branch) && !['master', 'origin/master', 'HEAD'].includes(branch); - - /** @type {!mdc.proto.DiffBase} */ - const parsedDiffBase = await this.parseDiffBase_(rawDiffBase); - const parsedBranch = parsedDiffBase.git_revision ? parsedDiffBase.git_revision.branch : null; - - if (isOnline && isRealBranch(parsedBranch)) { - const prNumber = await this.gitHubApi_.getPullRequestNumber(parsedBranch); - if (prNumber) { - parsedDiffBase.git_revision.pr_number = prNumber; - } - } - - return parsedDiffBase; - } - - /** - * TODO(acdvorak): Move this method out of Cli class - it doesn't belong here. - * @param {string} rawDiffBase - * @return {!Promise} - * @private - */ - async parseDiffBase_(rawDiffBase = this.diffBase) { - // Diff against a public `golden.json` URL. - // E.g.: `--diff-base=https://storage.googleapis.com/.../golden.json` - const isUrl = HTTP_URL_REGEX.test(rawDiffBase); - if (isUrl) { - return this.createPublicUrlDiffBase_(rawDiffBase); - } - - // Diff against a local `golden.json` file. - // E.g.: `--diff-base=/tmp/golden.json` - const isLocalFile = await fs.exists(rawDiffBase); - if (isLocalFile) { - return this.createLocalFileDiffBase_(rawDiffBase); - } - - const [inputGoldenRef, inputGoldenPath] = rawDiffBase.split(':'); - const goldenFilePath = inputGoldenPath || GOLDEN_JSON_RELATIVE_PATH; - const fullGoldenRef = await this.gitRepo_.getFullSymbolicName(inputGoldenRef); - - // Diff against a specific git commit. - // E.g.: `--diff-base=abcd1234` - if (!fullGoldenRef) { - return this.createCommitDiffBase_(inputGoldenRef, goldenFilePath); - } - - const {remoteRef, localRef, tagRef} = this.getRefType_(fullGoldenRef); - - // Diff against a remote git branch. - // E.g.: `--diff-base=origin/master` or `--diff-base=origin/feat/button/my-fancy-feature` - if (remoteRef) { - return this.createRemoteBranchDiffBase_(remoteRef, goldenFilePath); - } - - // Diff against a remote git tag. - // E.g.: `--diff-base=v0.34.1` - if (tagRef) { - return this.createRemoteTagDiffBase_(tagRef, goldenFilePath); - } - - // Diff against a local git branch. - // E.g.: `--diff-base=master` or `--diff-base=HEAD` - return this.createLocalBranchDiffBase_(localRef, goldenFilePath); - } - - /** - * @return {?Promise} - */ - async getTravisGitRevision() { - const travisBranch = process.env.TRAVIS_BRANCH; - const travisTag = process.env.TRAVIS_TAG; - const travisPrNumber = Number(process.env.TRAVIS_PULL_REQUEST); - const travisPrBranch = process.env.TRAVIS_PULL_REQUEST_BRANCH; - const travisPrSha = process.env.TRAVIS_PULL_REQUEST_SHA; - - if (travisPrNumber) { - const commit = await this.gitRepo_.getFullCommitHash(travisPrSha); - const author = await this.gitRepo_.getCommitAuthor(commit); - return GitRevision.create({ - type: GitRevision.Type.TRAVIS_PR, - golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, - commit, - author, - branch: travisPrBranch || travisBranch, - pr_number: travisPrNumber, - }); - } - - if (travisTag) { - const commit = await this.gitRepo_.getFullCommitHash(travisTag); - const author = await this.gitRepo_.getCommitAuthor(commit); - return GitRevision.create({ - type: GitRevision.Type.REMOTE_TAG, - golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, - commit, - author, - tag: travisTag, - }); - } - - if (travisBranch) { - const commit = await this.gitRepo_.getFullCommitHash(travisBranch); - const author = await this.gitRepo_.getCommitAuthor(commit); - return GitRevision.create({ - type: GitRevision.Type.LOCAL_BRANCH, - golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, - commit, - author, - branch: travisBranch, - }); - } - - return null; - } - - /** - * @param {string} publicUrl - * @return {!mdc.proto.DiffBase} - * @private - */ - createPublicUrlDiffBase_(publicUrl) { - return DiffBase.create({ - type: DiffBase.Type.PUBLIC_URL, - input_string: publicUrl, - public_url: publicUrl, - }); - } - - /** - * @param {string} localFilePath - * @return {!mdc.proto.DiffBase} - * @private + * @return {boolean} */ - createLocalFileDiffBase_(localFilePath) { - return DiffBase.create({ - type: DiffBase.Type.FILE_PATH, - input_string: localFilePath, - local_file_path: localFilePath, - is_default_local_file: localFilePath === GOLDEN_JSON_RELATIVE_PATH, - }); - } - - /** - * @param {string} commit - * @param {string} goldenJsonFilePath - * @return {!mdc.proto.DiffBase} - * @private - */ - async createCommitDiffBase_(commit, goldenJsonFilePath) { - const author = await this.gitRepo_.getCommitAuthor(commit); - - return DiffBase.create({ - type: DiffBase.Type.GIT_REVISION, - git_revision: GitRevision.create({ - type: GitRevision.Type.COMMIT, - input_string: `${commit}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format - golden_json_file_path: goldenJsonFilePath, - commit, - author, - }), - }); - } - - /** - * @param {string} remoteRef - * @param {string} goldenJsonFilePath - * @return {!mdc.proto.DiffBase} - * @private - */ - async createRemoteBranchDiffBase_(remoteRef, goldenJsonFilePath) { - const allRemoteNames = await this.gitRepo_.getRemoteNames(); - const remote = allRemoteNames.find((curRemoteName) => remoteRef.startsWith(curRemoteName + '/')); - const branch = remoteRef.substr(remote.length + 1); // add 1 for forward-slash separator - const commit = await this.gitRepo_.getFullCommitHash(remoteRef); - const author = await this.gitRepo_.getCommitAuthor(commit); - - return DiffBase.create({ - type: DiffBase.Type.GIT_REVISION, - git_revision: GitRevision.create({ - type: GitRevision.Type.REMOTE_BRANCH, - input_string: `${remoteRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format - golden_json_file_path: goldenJsonFilePath, - commit, - author, - remote, - branch, - }), - }); + isOnline() { + return isOnlineCached === true; } /** - * @param {string} tagRef - * @param {string} goldenJsonFilePath - * @return {!mdc.proto.DiffBase} - * @private - */ - async createRemoteTagDiffBase_(tagRef, goldenJsonFilePath) { - const commit = await this.gitRepo_.getFullCommitHash(tagRef); - const author = await this.gitRepo_.getCommitAuthor(commit); - - return DiffBase.create({ - type: DiffBase.Type.GIT_REVISION, - git_revision: GitRevision.create({ - type: GitRevision.Type.REMOTE_TAG, - input_string: `${tagRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format - golden_json_file_path: goldenJsonFilePath, - commit, - author, - remote: 'origin', - tag: tagRef, - }), - }); - } - - /** - * @param {string} branch - * @param {string} goldenJsonFilePath - * @return {!mdc.proto.DiffBase} - * @private - */ - async createLocalBranchDiffBase_(branch, goldenJsonFilePath) { - const commit = await this.gitRepo_.getFullCommitHash(branch); - const author = await this.gitRepo_.getCommitAuthor(commit); - - return DiffBase.create({ - type: DiffBase.Type.GIT_REVISION, - git_revision: GitRevision.create({ - type: GitRevision.Type.LOCAL_BRANCH, - input_string: `${branch}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format - golden_json_file_path: goldenJsonFilePath, - commit, - author, - branch, - }), - }); - } - - /** - * @param {string} fullRef - * @return {{remoteRef: string, localRef: string, tagRef: string}} - * @private + * @return {boolean} */ - getRefType_(fullRef) { - const getShortGoldenRef = (type) => { - const regex = new RegExp(`^refs/${type}s/(.+)$`); - const match = regex.exec(fullRef) || []; - return match[1]; - }; - - const remoteRef = getShortGoldenRef('remote'); - const localRef = getShortGoldenRef('head'); - const tagRef = getShortGoldenRef('tag'); - - return {remoteRef, localRef, tagRef}; + isOffline() { + return !this.isOnline(); } } diff --git a/test/screenshot/infra/lib/cloud-storage.js b/test/screenshot/infra/lib/cloud-storage.js index a3c5c2e71ed..d94d28b20e2 100644 --- a/test/screenshot/infra/lib/cloud-storage.js +++ b/test/screenshot/infra/lib/cloud-storage.js @@ -80,8 +80,8 @@ class CloudStorage { */ async uploadDirectory_(noun, reportData, localSourceDir) { const isSourceDirEmpty = glob.sync('**/*', {cwd: localSourceDir, nodir: true}).length === 0; - const isOnline = await this.cli_.isOnline(); - if (isSourceDirEmpty || !isOnline) { + const isOffline = this.cli_.isOffline(); + if (isSourceDirEmpty || isOffline) { return; } diff --git a/test/screenshot/infra/lib/constants.js b/test/screenshot/infra/lib/constants.js index 182c06e574b..a534bdd4d9f 100644 --- a/test/screenshot/infra/lib/constants.js +++ b/test/screenshot/infra/lib/constants.js @@ -61,15 +61,19 @@ module.exports = { ExitCode: { OK: 0, - UNKNOWN_ERROR: 11, - SIGINT: 12, // ctrl-c - SIGTERM: 13, // kill - UNSUPPORTED_CLI_COMMAND: 14, - HTTP_PORT_ALREADY_IN_USE: 15, - MISSING_ENV_VAR: 16, - UNSUPPORTED_EXTERNAL_PR: 17, - UNHANDLED_PROMISE_REJECTION: 18, - UNCAUGHT_EXCEPTION: 19, - CHANGES_FOUND: 20, + + /** ctrl-c */ + SIGINT: 11, + + /** kill */ + SIGTERM: 12, + + UNKNOWN_ERROR: 13, + UNCAUGHT_EXCEPTION: 14, + UNHANDLED_PROMISE_REJECTION: 15, + UNSUPPORTED_CLI_COMMAND: 16, + MISSING_ENV_VAR: 17, + HTTP_PORT_ALREADY_IN_USE: 18, + CHANGES_FOUND: 19, }, }; diff --git a/test/screenshot/infra/lib/controller.js b/test/screenshot/infra/lib/controller.js index 21540e89303..c592304bdf9 100644 --- a/test/screenshot/infra/lib/controller.js +++ b/test/screenshot/infra/lib/controller.js @@ -100,7 +100,7 @@ class Controller { * @return {!Promise} */ async initForCapture() { - const isOnline = await this.cli_.isOnline(); + const isOnline = this.cli_.isOnline(); const shouldFetch = this.cli_.shouldFetch; if (isOnline && shouldFetch) { await this.gitRepo_.fetch(); @@ -115,7 +115,7 @@ class Controller { * @return {!Promise} */ async initForDemo() { - const isOnline = await this.cli_.isOnline(); + const isOnline = this.cli_.isOnline(); const shouldFetch = this.cli_.shouldFetch; if (isOnline && shouldFetch) { await this.gitRepo_.fetch(); @@ -207,11 +207,12 @@ class Controller { const boldRed = Logger.colors.bold.red; const boldGreen = Logger.colors.bold.green; + this.logger_.log('\n'); if (numChanges > 0) { - this.logger_.error(boldRed(`\n\n${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`)); + this.logger_.error(boldRed(`${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`)); this.logger_.log('Diff report:', boldRed(reportData.meta.report_html_file.public_url)); } else { - this.logger_.log(boldGreen(`\n\n${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`)); + this.logger_.log(boldGreen('0 screenshots changed!\n')); this.logger_.log('Diff report:', boldGreen(reportData.meta.report_html_file.public_url)); } @@ -235,7 +236,11 @@ class Controller { reportData.screenshots.added_screenshot_list.length + reportData.screenshots.removed_screenshot_list.length; - return numChanges > 0 ? ExitCode.CHANGES_FOUND : ExitCode.OK; + const isOnline = this.cli_.isOnline(); + if (isOnline && numChanges > 0) { + return ExitCode.CHANGES_FOUND; + } + return ExitCode.OK; } /** diff --git a/test/screenshot/infra/lib/diff-base-parser.js b/test/screenshot/infra/lib/diff-base-parser.js new file mode 100644 index 00000000000..4b1954199fb --- /dev/null +++ b/test/screenshot/infra/lib/diff-base-parser.js @@ -0,0 +1,332 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const mdcProto = require('../proto/mdc.pb').mdc.proto; +const {DiffBase, GitRevision} = mdcProto; + +const fs = require('mz/fs'); +const {GOLDEN_JSON_RELATIVE_PATH} = require('./constants'); + +const Cli = require('./cli'); +const GitHubApi = require('./github-api'); +const GitRepo = require('./git-repo'); + +const HTTP_URL_REGEX = new RegExp('^https?://'); + +class DiffBaseParser { + constructor() { + /** + * @type {!Cli} + * @private + */ + this.cli_ = new Cli(); + + /** + * @type {!GitHubApi} + * @private + */ + this.gitHubApi_ = new GitHubApi(); + + /** + * @type {!GitRepo} + * @private + */ + this.gitRepo_ = new GitRepo(); + } + + /** + * @return {!Promise} + */ + async parseGoldenDiffBase() { + /** @type {?mdc.proto.GitRevision} */ + const travisGitRevision = await this.getTravisGitRevision(); + if (travisGitRevision) { + return DiffBase.create({ + type: DiffBase.Type.GIT_REVISION, + git_revision: travisGitRevision, + }); + } + return this.parseDiffBase(); + } + + /** + * TODO(acdvorak): Move this method out of DiffBaseParser class - it doesn't belong here. + * @param {string} rawDiffBase + * @return {!Promise} + */ + async parseDiffBase(rawDiffBase = this.cli_.diffBase) { + const isOnline = this.cli_.isOnline(); + const isRealBranch = (branch) => Boolean(branch) && !['master', 'origin/master', 'HEAD'].includes(branch); + + /** @type {!mdc.proto.DiffBase} */ + const parsedDiffBase = await this.parseDiffBase_(rawDiffBase); + const parsedBranch = parsedDiffBase.git_revision ? parsedDiffBase.git_revision.branch : null; + + if (isOnline && isRealBranch(parsedBranch)) { + const prNumber = await this.gitHubApi_.getPullRequestNumber(parsedBranch); + if (prNumber) { + parsedDiffBase.git_revision.pr_number = prNumber; + } + } + + return parsedDiffBase; + } + + /** + * TODO(acdvorak): Move this method out of DiffBaseParser class - it doesn't belong here. + * @param {string} rawDiffBase + * @return {!Promise} + * @private + */ + async parseDiffBase_(rawDiffBase = this.cli_.diffBase) { + // Diff against a public `golden.json` URL. + // E.g.: `--diff-base=https://storage.googleapis.com/.../golden.json` + const isUrl = HTTP_URL_REGEX.test(rawDiffBase); + if (isUrl) { + return this.createPublicUrlDiffBase_(rawDiffBase); + } + + // Diff against a local `golden.json` file. + // E.g.: `--diff-base=/tmp/golden.json` + const isLocalFile = await fs.exists(rawDiffBase); + if (isLocalFile) { + return this.createLocalFileDiffBase_(rawDiffBase); + } + + const [inputGoldenRef, inputGoldenPath] = rawDiffBase.split(':'); + const goldenFilePath = inputGoldenPath || GOLDEN_JSON_RELATIVE_PATH; + const fullGoldenRef = await this.gitRepo_.getFullSymbolicName(inputGoldenRef); + + // Diff against a specific git commit. + // E.g.: `--diff-base=abcd1234` + if (!fullGoldenRef) { + return this.createCommitDiffBase_(inputGoldenRef, goldenFilePath); + } + + const {remoteRef, localRef, tagRef} = this.getRefType_(fullGoldenRef); + + // Diff against a remote git branch. + // E.g.: `--diff-base=origin/master` or `--diff-base=origin/feat/button/my-fancy-feature` + if (remoteRef) { + return this.createRemoteBranchDiffBase_(remoteRef, goldenFilePath); + } + + // Diff against a remote git tag. + // E.g.: `--diff-base=v0.34.1` + if (tagRef) { + return this.createRemoteTagDiffBase_(tagRef, goldenFilePath); + } + + // Diff against a local git branch. + // E.g.: `--diff-base=master` or `--diff-base=HEAD` + return this.createLocalBranchDiffBase_(localRef, goldenFilePath); + } + + /** + * @return {?Promise} + */ + async getTravisGitRevision() { + const travisBranch = process.env.TRAVIS_BRANCH; + const travisTag = process.env.TRAVIS_TAG; + const travisPrNumber = Number(process.env.TRAVIS_PULL_REQUEST); + const travisPrBranch = process.env.TRAVIS_PULL_REQUEST_BRANCH; + const travisPrSha = process.env.TRAVIS_PULL_REQUEST_SHA; + + if (travisPrNumber) { + const commit = await this.gitRepo_.getFullCommitHash(travisPrSha); + const author = await this.gitRepo_.getCommitAuthor(commit); + return GitRevision.create({ + type: GitRevision.Type.TRAVIS_PR, + golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, + commit, + author, + branch: travisPrBranch || travisBranch, + pr_number: travisPrNumber, + }); + } + + if (travisTag) { + const commit = await this.gitRepo_.getFullCommitHash(travisTag); + const author = await this.gitRepo_.getCommitAuthor(commit); + return GitRevision.create({ + type: GitRevision.Type.REMOTE_TAG, + golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, + commit, + author, + tag: travisTag, + }); + } + + if (travisBranch) { + const commit = await this.gitRepo_.getFullCommitHash(travisBranch); + const author = await this.gitRepo_.getCommitAuthor(commit); + return GitRevision.create({ + type: GitRevision.Type.LOCAL_BRANCH, + golden_json_file_path: GOLDEN_JSON_RELATIVE_PATH, + commit, + author, + branch: travisBranch, + }); + } + + return null; + } + + /** + * @param {string} publicUrl + * @return {!mdc.proto.DiffBase} + * @private + */ + createPublicUrlDiffBase_(publicUrl) { + return DiffBase.create({ + type: DiffBase.Type.PUBLIC_URL, + input_string: publicUrl, + public_url: publicUrl, + }); + } + + /** + * @param {string} localFilePath + * @return {!mdc.proto.DiffBase} + * @private + */ + createLocalFileDiffBase_(localFilePath) { + return DiffBase.create({ + type: DiffBase.Type.FILE_PATH, + input_string: localFilePath, + local_file_path: localFilePath, + is_default_local_file: localFilePath === GOLDEN_JSON_RELATIVE_PATH, + }); + } + + /** + * @param {string} commit + * @param {string} goldenJsonFilePath + * @return {!mdc.proto.DiffBase} + * @private + */ + async createCommitDiffBase_(commit, goldenJsonFilePath) { + const author = await this.gitRepo_.getCommitAuthor(commit); + + return DiffBase.create({ + type: DiffBase.Type.GIT_REVISION, + git_revision: GitRevision.create({ + type: GitRevision.Type.COMMIT, + input_string: `${commit}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format + golden_json_file_path: goldenJsonFilePath, + commit, + author, + }), + }); + } + + /** + * @param {string} remoteRef + * @param {string} goldenJsonFilePath + * @return {!mdc.proto.DiffBase} + * @private + */ + async createRemoteBranchDiffBase_(remoteRef, goldenJsonFilePath) { + const allRemoteNames = await this.gitRepo_.getRemoteNames(); + const remote = allRemoteNames.find((curRemoteName) => remoteRef.startsWith(curRemoteName + '/')); + const branch = remoteRef.substr(remote.length + 1); // add 1 for forward-slash separator + const commit = await this.gitRepo_.getFullCommitHash(remoteRef); + const author = await this.gitRepo_.getCommitAuthor(commit); + + return DiffBase.create({ + type: DiffBase.Type.GIT_REVISION, + git_revision: GitRevision.create({ + type: GitRevision.Type.REMOTE_BRANCH, + input_string: `${remoteRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format + golden_json_file_path: goldenJsonFilePath, + commit, + author, + remote, + branch, + }), + }); + } + + /** + * @param {string} tagRef + * @param {string} goldenJsonFilePath + * @return {!mdc.proto.DiffBase} + * @private + */ + async createRemoteTagDiffBase_(tagRef, goldenJsonFilePath) { + const commit = await this.gitRepo_.getFullCommitHash(tagRef); + const author = await this.gitRepo_.getCommitAuthor(commit); + + return DiffBase.create({ + type: DiffBase.Type.GIT_REVISION, + git_revision: GitRevision.create({ + type: GitRevision.Type.REMOTE_TAG, + input_string: `${tagRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format + golden_json_file_path: goldenJsonFilePath, + commit, + author, + remote: 'origin', + tag: tagRef, + }), + }); + } + + /** + * @param {string} branch + * @param {string} goldenJsonFilePath + * @return {!mdc.proto.DiffBase} + * @private + */ + async createLocalBranchDiffBase_(branch, goldenJsonFilePath) { + const commit = await this.gitRepo_.getFullCommitHash(branch); + const author = await this.gitRepo_.getCommitAuthor(commit); + + return DiffBase.create({ + type: DiffBase.Type.GIT_REVISION, + git_revision: GitRevision.create({ + type: GitRevision.Type.LOCAL_BRANCH, + input_string: `${branch}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format + golden_json_file_path: goldenJsonFilePath, + commit, + author, + branch, + }), + }); + } + + /** + * @param {string} fullRef + * @return {{remoteRef: string, localRef: string, tagRef: string}} + * @private + */ + getRefType_(fullRef) { + const getShortGoldenRef = (type) => { + const regex = new RegExp(`^refs/${type}s/(.+)$`); + const match = regex.exec(fullRef) || []; + return match[1]; + }; + + const remoteRef = getShortGoldenRef('remote'); + const localRef = getShortGoldenRef('head'); + const tagRef = getShortGoldenRef('tag'); + + return {remoteRef, localRef, tagRef}; + } +} + +module.exports = DiffBaseParser; diff --git a/test/screenshot/infra/lib/golden-io.js b/test/screenshot/infra/lib/golden-io.js index ac67a32516d..35cf6b81244 100644 --- a/test/screenshot/infra/lib/golden-io.js +++ b/test/screenshot/infra/lib/golden-io.js @@ -18,6 +18,7 @@ const request = require('request-promise-native'); const stringify = require('json-stable-stringify'); const Cli = require('./cli'); +const DiffBaseParser = require('./diff-base-parser'); const GitRepo = require('./git-repo'); const GoldenFile = require('./golden-file'); const LocalStorage = require('./local-storage'); @@ -34,6 +35,12 @@ class GoldenIo { */ this.cli_ = new Cli(); + /** + * @type {!DiffBaseParser} + * @private + */ + this.diffBaseParser_ = new DiffBaseParser(); + /** * @type {!GitRepo} * @private @@ -82,7 +89,7 @@ class GoldenIo { */ async readFromDiffBase_(rawDiffBase) { /** @type {!mdc.proto.DiffBase} */ - const parsedDiffBase = await this.cli_.parseDiffBase(rawDiffBase); + const parsedDiffBase = await this.diffBaseParser_.parseDiffBase(rawDiffBase); const publicUrl = parsedDiffBase.public_url; if (publicUrl) { @@ -128,22 +135,6 @@ class GoldenIo { async stringify_(object) { return stringify(object, {space: ' '}) + '\n'; } - - /** - * Creates a deep clone of the given `source` object's own enumerable properties. - * Non-JSON-serializable properties (such as functions or symbols) are silently discarded. - * The returned value is structurally equivalent, but not referentially equal, to the input. - * In Java parlance: - * clone.equals(source) // true - * clone == source // false - * @param {!T} source JSON object to clone - * @return {!T} Deep clone of `source` object - * @template T - * @private - */ - deepCloneJson_(source) { - return JSON.parse(JSON.stringify(source)); - } } module.exports = GoldenIo; diff --git a/test/screenshot/infra/lib/logger.js b/test/screenshot/infra/lib/logger.js index 8dde6305385..06c07605fb0 100644 --- a/test/screenshot/infra/lib/logger.js +++ b/test/screenshot/infra/lib/logger.js @@ -181,21 +181,21 @@ class Logger { * @param {*} args */ info(...args) { - console.info(`[${colors.blue('info')}][${this.id_}]`, ...args); + console.info(...args); } /** * @param {*} args */ warn(...args) { - console.warn(`[${colors.yellow('warn')}][${this.id_}]`, ...args); + console.warn(...args); } /** * @param {*} args */ error(...args) { - console.error(`[${colors.bold.red('error')}][${this.id_}]`, ...args); + console.error(...args); } /** diff --git a/test/screenshot/infra/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js index de52b547e0a..da3524cf299 100644 --- a/test/screenshot/infra/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -30,7 +30,9 @@ const {Approvals, DiffImageResult, Dimensions, GitRevision, GitStatus, GoldenScr const {ReportData, ReportMeta, Screenshot, Screenshots, ScreenshotList, TestFile, User, UserAgents} = mdcProto; const {InclusionType, CaptureState} = Screenshot; +const CbtApi = require('./cbt-api'); const Cli = require('./cli'); +const DiffBaseParser = require('./diff-base-parser'); const FileCache = require('./file-cache'); const GitHubApi = require('./github-api'); const GitRepo = require('./git-repo'); @@ -45,12 +47,24 @@ const TEMP_DIR = os.tmpdir(); class ReportBuilder { constructor() { + /** + * @type {!CbtApi} + * @private + */ + this.cbtApi_ = new CbtApi(); + /** * @type {!Cli} * @private */ this.cli_ = new Cli(); + /** + * @type {!DiffBaseParser} + * @private + */ + this.diffBaseParser_ = new DiffBaseParser(); + /** * @type {!FileCache} * @private @@ -115,8 +129,8 @@ class ReportBuilder { async initForCapture() { this.logger_.foldStart('screenshot.init', 'ReportBuilder#initForCapture()'); - /** @type {boolean} */ - const isOnline = await this.cli_.isOnline(); + await this.cbtApi_.fetchAvailableDevices(); + /** @type {!mdc.proto.ReportMeta} */ const reportMeta = await this.createReportMetaProto_(); /** @type {!mdc.proto.UserAgents} */ @@ -125,7 +139,7 @@ class ReportBuilder { await this.localStorage_.copyAssetsToTempDir(reportMeta); // In offline mode, we start a local web server to test on instead of using GCS. - if (!isOnline) { + if (this.cli_.isOffline()) { await this.startTemporaryHttpServer_(reportMeta); } @@ -368,7 +382,7 @@ class ReportBuilder { * @private */ async createReportMetaProto_() { - const isOnline = await this.cli_.isOnline(); + const isOnline = this.cli_.isOnline(); // We only need to start up a local web server if the user is running in offline mode. // Otherwise, HTML files are uploaded to (and served) by GCS. @@ -399,10 +413,10 @@ class ReportBuilder { const gitStatus = GitStatus.fromObject(await this.gitRepo_.getStatus()); /** @type {!mdc.proto.DiffBase} */ - const goldenDiffBase = await this.cli_.parseGoldenDiffBase(); + const goldenDiffBase = await this.diffBaseParser_.parseGoldenDiffBase(); /** @type {!mdc.proto.DiffBase} */ - const snapshotDiffBase = await this.cli_.parseDiffBase('HEAD'); + const snapshotDiffBase = await this.diffBaseParser_.parseDiffBase('HEAD'); /** @type {!mdc.proto.GitRevision} */ const goldenGitRevision = goldenDiffBase.git_revision; @@ -679,6 +693,7 @@ class ReportBuilder { }); for (const userAgent of allUserAgents) { + const maxRetries = this.cli_.isOnline() ? this.cli_.retries : 0; const userAgentAlias = userAgent.alias; const isScreenshotRunnable = isHtmlFileRunnable && userAgent.is_runnable; const expectedScreenshotImageUrl = goldenFile.getScreenshotImageUrl({htmlFilePath, userAgentAlias}); @@ -697,7 +712,7 @@ class ReportBuilder { actual_html_file: actualHtmlFile, expected_image_file: expectedImageFile, retry_count: 0, - max_retries: this.cli_.retries, + max_retries: maxRetries, })); } } diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 4d690b43ff0..3033ebc3397 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -46,6 +46,7 @@ const {SELENIUM_FONT_LOAD_WAIT_MS} = Constants; * color: !CliColor, * }} CliStatus */ + const CliStatuses = { ACTIVE: {name: 'Active', color: colors.bold.cyan}, QUEUED: {name: 'Queued', color: colors.cyan}, @@ -54,7 +55,10 @@ const CliStatuses = { STARTED: {name: 'Started', color: colors.bold.green}, GET: {name: 'Get', color: colors.bold.white}, CROP: {name: 'Crop', color: colors.white}, - RETRY: {name: 'Retry', color: colors.red}, + PASS: {name: 'Pass', color: colors.green}, + FAIL: {name: 'Fail', color: colors.red}, + RETRY: {name: 'Retry', color: colors.magenta}, + CAPTURED: {name: 'Captured', color: colors.bold.grey}, FINISHED: {name: 'Finished', color: colors.bold.green}, FAILED: {name: 'Failed', color: colors.bold.red}, QUITTING: {name: 'Quitting', color: colors.white}, @@ -98,7 +102,27 @@ class SeleniumApi { */ this.seleniumSessionIds_ = new Set(); - this.killBrowsersOnExit_(); + /** + * @type {number} + * @private + */ + this.numPending_ = 0; + + /** + * @type {number} + * @private + */ + this.numCompleted_ = 0; + + /** + * @type {boolean} + * @private + */ + this.isKilled_ = false; + + if (this.cli_.isOnline()) { + this.killBrowsersOnExit_(); + } } /** @@ -110,6 +134,9 @@ class SeleniumApi { let queuedUserAgents = runnableUserAgents.slice(); let runningUserAgents; + this.numPending_ = reportData.screenshots.runnable_screenshot_list.length; + this.numCompleted_ = 0; + function getLoggableAliases(userAgentAliases) { return userAgentAliases.length > 0 ? userAgentAliases.join(', ') : '(none)'; } @@ -127,6 +154,8 @@ class SeleniumApi { await this.captureAllPagesInAllBrowsers_({reportData, userAgents: runningUserAgents}); } + console.log(''); + return reportData; } @@ -161,10 +190,10 @@ class SeleniumApi { this.seleniumSessionIds_.add(seleniumSessionId); - const logResult = (status) => { + const logResult = (status, ...args) => { /* eslint-disable camelcase */ const {os_name, os_version, browser_name, browser_version} = userAgent.navigator; - this.logStatus_(status, `${browser_name} ${browser_version} on ${os_name} ${os_version}!`); + this.logStatus_(status, `${browser_name} ${browser_version} on ${os_name} ${os_version}!`, ...args); /* eslint-enable camelcase */ }; @@ -172,7 +201,8 @@ class SeleniumApi { changedScreenshots = (await this.driveBrowser_({reportData, userAgent, driver})).changedScreenshots; logResult(CliStatuses.FINISHED); } catch (err) { - logResult(CliStatuses.FAILED); + logResult(CliStatuses.FAILED, err); + await this.killBrowsers_(); throw err; } finally { logResult(CliStatuses.QUITTING); @@ -180,10 +210,12 @@ class SeleniumApi { this.seleniumSessionIds_.delete(seleniumSessionId); } - await this.cbtApi_.setTestScore({ - seleniumSessionId, - changedScreenshots, - }); + if (this.cli_.isOnline()) { + await this.cbtApi_.setTestScore({ + seleniumSessionId, + changedScreenshots, + }); + } } /** @@ -191,8 +223,7 @@ class SeleniumApi { * @private */ async getMaxParallelTests_() { - const isOnline = await this.cli_.isOnline(); - if (!isOnline) { + if (this.cli_.isOffline()) { return 1; } @@ -247,7 +278,7 @@ class SeleniumApi { userAgent.desired_capabilities = desiredCapabilities; driverBuilder.withCapabilities(desiredCapabilities); - const isOnline = await this.cli_.isOnline(); + const isOnline = this.cli_.isOnline(); if (isOnline) { driverBuilder.usingServer(this.cbtApi_.getSeleniumServerUrl()); } @@ -284,9 +315,8 @@ class SeleniumApi { * @private */ async getDesiredCapabilities_({meta, userAgent}) { - const isOnline = await this.cli_.isOnline(); - if (isOnline) { - return await this.cbtApi_.getDesiredCapabilities_({meta, userAgent}); + if (this.cli_.isOnline()) { + return await this.cbtApi_.getDesiredCapabilities({meta, userAgent}); } const browserVendorMap = { @@ -380,6 +410,7 @@ class SeleniumApi { /** @type {!Array} */ const screenshotQueueAll = reportData.screenshots.runnable_screenshot_browser_map[userAgent.alias].screenshots; + // TODO(acdvorak): Find a better way to do this const screenshotQueues = [ [true, screenshotQueueAll.filter((screenshot) => this.isSmallComponent_(screenshot.html_file_path))], [false, screenshotQueueAll.filter((screenshot) => !this.isSmallComponent_(screenshot.html_file_path))], @@ -397,10 +428,17 @@ class SeleniumApi { screenshot.diff_image_result = diffImageResult; screenshot.diff_image_file = diffImageResult.diff_image_file; + this.numPending_--; + this.numCompleted_++; + + const message = `${screenshot.actual_html_file.public_url} > ${screenshot.user_agent.alias}`; + if (diffImageResult.has_changed) { changedScreenshots.push(screenshot); + this.logStatus_(CliStatuses.FAIL, message); } else { unchangedScreenshots.push(screenshot); + this.logStatus_(CliStatuses.PASS, message); } } } @@ -524,7 +562,7 @@ class SeleniumApi { async capturePageAsPng_({driver, userAgent, url, delayMs = 0}) { this.logStatus_(CliStatuses.GET, `${url} > ${userAgent.alias}...`); - const isOnline = await this.cli_.isOnline(); + const isOnline = this.cli_.isOnline(); const fontTimeoutMs = isOnline ? SELENIUM_FONT_LOAD_WAIT_MS : 500; await driver.get(url); @@ -552,39 +590,51 @@ class SeleniumApi { /** @private */ killBrowsersOnExit_() { - const killThemAll = async () => { - console.log('Killing Selenium tests:', this.seleniumSessionIds_); - await this.cbtApi_.killSeleniumTests(Array.from(this.seleniumSessionIds_)); - console.log('Killed Selenium tests!'); - // Give the HTTP requests a chance to complete before exiting - await this.sleep_(Duration.seconds(1).toMillis()); - }; - // catches ctrl+c event process.on('SIGINT', () => { const exit = () => process.exit(ExitCode.SIGINT); - killThemAll().then(exit, exit); + this.killBrowsers_().then(exit, exit); }); // catches "kill pid" process.on('SIGTERM', () => { const exit = () => process.exit(ExitCode.SIGTERM); - killThemAll().then(exit, exit); + this.killBrowsers_().then(exit, exit); }); process.on('uncaughtException', (err) => { console.error(err); const exit = () => process.exit(ExitCode.UNCAUGHT_EXCEPTION); - killThemAll().then(exit, exit); + this.killBrowsers_().then(exit, exit); }); process.on('unhandledRejection', (err) => { console.error(err); const exit = () => process.exit(ExitCode.UNHANDLED_PROMISE_REJECTION); - killThemAll().then(exit, exit); + this.killBrowsers_().then(exit, exit); }); } + /** @private */ + async killBrowsers_() { + if (this.isKilled_) { + return; + } + this.isKilled_ = true; + + const ids = Array.from(this.seleniumSessionIds_); + this.seleniumSessionIds_.clear(); + + console.log('\n'); + + await this.cbtApi_.killSeleniumTests(ids); + + console.log(`Killed ${ids.length} Selenium tests!`); + + // Give the HTTP requests a chance to complete before exiting + await this.sleep_(Duration.seconds(4).toMillis()); + } + /** * @param {number} ms * @return {!Promise} @@ -600,8 +650,33 @@ class SeleniumApi { * @private */ logStatus_(status, ...args) { - const maxWidth = Object.values(CliStatuses).map((status) => status.name.length).sort().reverse()[0]; - console.log(status.color(status.name.toUpperCase().padStart(maxWidth, ' ')) + ':', ...args); + // Don't output misleading errors + if (this.isKilled_) { + return; + } + + // https://stackoverflow.com/a/6774395/467582 + const eraseCurrentLine = '\r' + String.fromCodePoint(27) + '[K'; + const maxStatusWidth = Object.values(CliStatuses).map((status) => status.name.length).sort().reverse()[0]; + const colorStatus = status.color(status.name.toUpperCase().padStart(maxStatusWidth, ' ')); + + console.log(eraseCurrentLine + colorStatus + ':', ...args); + + if (process.env.TRAVIS === 'true') { + return; + } + + const pending = this.numPending_; + const completed = this.numCompleted_; + const total = pending + completed; + const percent = (total === 0 ? 0 : (100 * completed / total).toFixed(1)); + + const colorCaptured = CliStatuses.CAPTURED.color(CliStatuses.CAPTURED.name.toUpperCase()); + const colorCompleted = colors.bold.white(completed.toLocaleString()); + const colorTotal = colors.bold.white(total.toLocaleString()); + const colorPercent = colors.bold.white(`${percent}%`); + + process.stdout.write(`${colorCaptured}: ${colorCompleted} of ${colorTotal} screenshots (${colorPercent} complete)`); } } diff --git a/test/screenshot/infra/lib/user-agent-store.js b/test/screenshot/infra/lib/user-agent-store.js index e828633a448..32c04e9f9ef 100644 --- a/test/screenshot/infra/lib/user-agent-store.js +++ b/test/screenshot/infra/lib/user-agent-store.js @@ -132,7 +132,7 @@ Expected browser vendor to be one of [${validBrowserVendors}], but got '${browse ); } - const isOnline = await this.cli_.isOnline(); + const isOnline = this.cli_.isOnline(); const isEnabledByCli = this.isAliasEnabled_(alias); const isAvailableLocally = await this.isAvailableLocally_(browserVendorType); const isRunnable = isEnabledByCli && (isOnline || isAvailableLocally); diff --git a/test/screenshot/infra/lib/externs.js b/test/screenshot/infra/types/cbt-api-externs.js similarity index 57% rename from test/screenshot/infra/lib/externs.js rename to test/screenshot/infra/types/cbt-api-externs.js index 262e84553ea..7056a98f3dd 100644 --- a/test/screenshot/infra/lib/externs.js +++ b/test/screenshot/infra/types/cbt-api-externs.js @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,81 +14,6 @@ * limitations under the License. */ -/* eslint-disable no-unused-vars */ - - -/* - * Report UI - */ - - -/** - * @typedef {{ - * checkedUserAgentCbEls: !Array, - * uncheckedUserAgentCbEls: !Array, - * unreviewedUserAgentCbEls: !Array, - * changelistDict: !ReportUiChangelistDict, - * reviewStatusCountDict: !ReportUiReviewStatusCountDict, - * }} ReportUiState - */ - -/** - * @typedef {{ - * changed: !ReportUiChangelistState, - * added: !ReportUiChangelistState, - * removed: !ReportUiChangelistState, - * }} ReportUiChangelistDict - */ - -/** - * @typedef {{ - * cbEl: !HTMLInputElement, - * countEl: !HTMLElement, - * reviewStatusEl: !HTMLElement, - * checkedUserAgentCbEls: !Array, - * uncheckedUserAgentCbEls: !Array, - * reviewStatusCountDict: !ReportUiReviewStatusCountDict, - * pageDict: !ReportUiPageDict, - * }} ReportUiChangelistState - */ - -/** - * @typedef {!Object} ReportUiPageDict - */ - -/** - * @typedef {{ - * cbEl: !HTMLInputElement, - * countEl: !HTMLElement, - * reviewStatusEl: !HTMLElement, - * checkedUserAgentCbEls: !Array, - * uncheckedUserAgentCbEls: !Array, - * reviewStatusCountDict: !ReportUiReviewStatusCountDict, - * }} ReportUiPageState - */ - -/** - * @typedef {!Object} ReportUiReviewStatusCountDict - */ - - -/* - * CLI args - */ - - -/** - * @typedef {{ - * optionNames: !Array, - * description: string, - * isRequired: ?boolean, - * type: ?string, - * defaultValue: ?*, - * exampleValue: ?string, - * }} CliOptionConfig - */ - - /* * CBT (CrossBrowserTesting.com) */ @@ -235,83 +160,3 @@ * commands: !Array, * }} CbtSeleniumInfoResponse */ - - -/* - * Resemble.js API externs - */ - - -/** - * @typedef {{ - * rawMisMatchPercentage: number, - * misMatchPercentage: string, - * diffBounds: !ResembleApiBoundingBox, - * analysisTime: number, - * getImageDataUrl: function(text: string): string, - * getBuffer: function(includeOriginal: boolean): !Buffer, - * }} ResembleApiComparisonResult - */ - -/** - * @typedef {{ - * top: number, - * left: number, - * bottom: number, - * right: number, - * }} ResembleApiBoundingBox - */ - - -/* - * ps-node API externs - */ - - -/** - * @typedef {{ - * pid: number, - * ppid: number, - * command: string, - * arguments: !Array, - * }} PsNodeProcess - */ - - -/* - * Node.js API - */ - - -/** - * @typedef {{ - * cwd: ?string, - * env: ?Object, - * argv0: ?string, - * stdio: ?Array, - * detached: ?boolean, - * uid: ?number, - * gid: ?number, - * shell: ?boolean, - * windowsVerbatimArguments: ?boolean, - * windowsHide: ?boolean, - * }} ChildProcessSpawnOptions - */ - -/** - * @typedef {{ - * status: number, - * signal: ?string, - * pid: number, - * }} ChildProcessSpawnResult - */ - - -/* - * Image cropping - */ - - -/** - * @typedef {{r: number, g: number, b: number, a: number}} RGBA - */ diff --git a/test/screenshot/infra/types/cli-types.js b/test/screenshot/infra/types/cli-types.js new file mode 100644 index 00000000000..71e5821eefa --- /dev/null +++ b/test/screenshot/infra/types/cli-types.js @@ -0,0 +1,26 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @typedef {{ + * optionNames: !Array, + * description: string, + * isRequired: ?boolean, + * type: ?string, + * defaultValue: ?*, + * exampleValue: ?string, + * }} CliOptionConfig + */ diff --git a/test/screenshot/infra/types/node-api-externs.js b/test/screenshot/infra/types/node-api-externs.js new file mode 100644 index 00000000000..081021d010f --- /dev/null +++ b/test/screenshot/infra/types/node-api-externs.js @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Node.js API + */ + + +/** + * @typedef {{ + * cwd: ?string, + * env: ?Object, + * argv0: ?string, + * stdio: ?Array, + * detached: ?boolean, + * uid: ?number, + * gid: ?number, + * shell: ?boolean, + * windowsVerbatimArguments: ?boolean, + * windowsHide: ?boolean, + * }} ChildProcessSpawnOptions + */ + +/** + * @typedef {{ + * status: number, + * signal: ?string, + * pid: number, + * }} ChildProcessSpawnResult + */ + + +/* + * Resemble.js API + */ + + +/** + * @typedef {{ + * rawMisMatchPercentage: number, + * misMatchPercentage: string, + * diffBounds: !ResembleApiBoundingBox, + * analysisTime: number, + * getImageDataUrl: function(text: string): string, + * getBuffer: function(includeOriginal: boolean): !Buffer, + * }} ResembleApiComparisonResult + */ + +/** + * @typedef {{ + * top: number, + * left: number, + * bottom: number, + * right: number, + * }} ResembleApiBoundingBox + */ + +/** + * @typedef {{ + * r: number, g: number, b: number, a: number + * }} RGBA + */ + + +/* + * ps-node API + */ + + +/** + * @typedef {{ + * pid: number, + * ppid: number, + * command: string, + * arguments: !Array, + * }} PsNodeProcess + */ diff --git a/test/screenshot/infra/types/report-ui-types.js b/test/screenshot/infra/types/report-ui-types.js new file mode 100644 index 00000000000..e4884d1fbb3 --- /dev/null +++ b/test/screenshot/infra/types/report-ui-types.js @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Report UI + */ + + +/** + * @typedef {{ + * checkedUserAgentCbEls: !Array, + * uncheckedUserAgentCbEls: !Array, + * unreviewedUserAgentCbEls: !Array, + * changelistDict: !ReportUiChangelistDict, + * reviewStatusCountDict: !ReportUiReviewStatusCountDict, + * }} ReportUiState + */ + +/** + * @typedef {{ + * changed: !ReportUiChangelistState, + * added: !ReportUiChangelistState, + * removed: !ReportUiChangelistState, + * }} ReportUiChangelistDict + */ + +/** + * @typedef {{ + * cbEl: !HTMLInputElement, + * countEl: !HTMLElement, + * reviewStatusEl: !HTMLElement, + * checkedUserAgentCbEls: !Array, + * uncheckedUserAgentCbEls: !Array, + * reviewStatusCountDict: !ReportUiReviewStatusCountDict, + * pageDict: !ReportUiPageDict, + * }} ReportUiChangelistState + */ + +/** + * @typedef {!Object} ReportUiPageDict + */ + +/** + * @typedef {{ + * cbEl: !HTMLInputElement, + * countEl: !HTMLElement, + * reviewStatusEl: !HTMLElement, + * checkedUserAgentCbEls: !Array, + * uncheckedUserAgentCbEls: !Array, + * reviewStatusCountDict: !ReportUiReviewStatusCountDict, + * }} ReportUiPageState + */ + +/** + * @typedef {!Object} ReportUiReviewStatusCountDict + */ diff --git a/test/screenshot/run.js b/test/screenshot/run.js index 31ec450d1bf..c9c371f367c 100644 --- a/test/screenshot/run.js +++ b/test/screenshot/run.js @@ -54,40 +54,43 @@ const COMMAND_MAP = { }, }; -async function run() { +async function runAsync() { const cli = new Cli(); const cmd = COMMAND_MAP[cli.command]; - if (cmd) { - const isOnline = await cli.isOnline(); - if (!isOnline) { - console.log('Offline mode!'); - } - - cmd().then( - (exitCode = 0) => { - if (exitCode !== 0) { - process.exit(exitCode); - } - }, - (err) => { - console.error(err); - process.exit(ExitCode.UNKNOWN_ERROR); - } - ); - } else { + if (!cmd) { console.error(`Error: Unknown command: '${cli.command}'`); process.exit(ExitCode.UNSUPPORTED_CLI_COMMAND); + return; + } + + const isOnline = await cli.checkIsOnline(); + if (!isOnline) { + console.log('Offline mode!'); } + + cmd().then( + (exitCode = ExitCode.OK) => { + if (exitCode !== ExitCode.OK) { + process.exit(exitCode); + } + }, + (err) => { + console.error(err); + process.exit(ExitCode.UNKNOWN_ERROR); + } + ); } const startTimeMs = new Date(); +// TODO(acdvorak): Create a centralized class to manage global exit handlers process.on('exit', () => { const elapsedTimeHuman = Duration.elapsed(startTimeMs, new Date()).toHumanShort(); console.log(`\nRun time: ${elapsedTimeHuman}\n`); }); +// TODO(acdvorak): Create a centralized class to manage global exit handlers process.on('unhandledRejection', (error) => { const message = [ 'UnhandledPromiseRejectionWarning: Unhandled promise rejection.', @@ -99,4 +102,4 @@ process.on('unhandledRejection', (error) => { process.exit(ExitCode.UNHANDLED_PROMISE_REJECTION); }); -run(); +runAsync(); diff --git a/test/screenshot/spec/mdc-drawer/classes/permanent.html b/test/screenshot/spec/mdc-drawer/classes/permanent.html index fc3bbba7455..18802a568b9 100644 --- a/test/screenshot/spec/mdc-drawer/classes/permanent.html +++ b/test/screenshot/spec/mdc-drawer/classes/permanent.html @@ -83,7 +83,7 @@
    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -92,7 +92,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -101,7 +101,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -110,7 +110,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    diff --git a/test/screenshot/spec/mdc-drawer/classes/persistent.html b/test/screenshot/spec/mdc-drawer/classes/persistent.html index 158a8d3e346..489c46611be 100644 --- a/test/screenshot/spec/mdc-drawer/classes/persistent.html +++ b/test/screenshot/spec/mdc-drawer/classes/persistent.html @@ -84,7 +84,7 @@

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -93,7 +93,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -102,7 +102,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -111,7 +111,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    diff --git a/test/screenshot/spec/mdc-drawer/classes/temporary.html b/test/screenshot/spec/mdc-drawer/classes/temporary.html index f5ed276b3af..f202c83fff4 100644 --- a/test/screenshot/spec/mdc-drawer/classes/temporary.html +++ b/test/screenshot/spec/mdc-drawer/classes/temporary.html @@ -84,7 +84,7 @@

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -93,7 +93,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -102,7 +102,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -111,7 +111,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    diff --git a/test/screenshot/spec/mdc-drawer/fixture.scss b/test/screenshot/spec/mdc-drawer/fixture.scss index 374aba1f664..2d3c98a23d6 100644 --- a/test/screenshot/spec/mdc-drawer/fixture.scss +++ b/test/screenshot/spec/mdc-drawer/fixture.scss @@ -30,6 +30,11 @@ $custom-drawer-color: $material-color-orange-900; flex-direction: column; } +.test-drawer-paragraph { + margin: 1em 0 0 0; + line-height: 1; +} + .custom-drawer--fill-color { @include mdc-drawer-fill-color($custom-drawer-color); } diff --git a/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html b/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html index 2008b347205..f4881967fa1 100644 --- a/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html +++ b/test/screenshot/spec/mdc-drawer/mixins/fill-color-accessible.html @@ -83,7 +83,7 @@

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -92,7 +92,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -101,7 +101,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -110,7 +110,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    diff --git a/test/screenshot/spec/mdc-drawer/mixins/fill-color.html b/test/screenshot/spec/mdc-drawer/mixins/fill-color.html index 152952e33b4..f76d51e3332 100644 --- a/test/screenshot/spec/mdc-drawer/mixins/fill-color.html +++ b/test/screenshot/spec/mdc-drawer/mixins/fill-color.html @@ -83,7 +83,7 @@

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -92,7 +92,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -101,7 +101,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -110,7 +110,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    diff --git a/test/screenshot/spec/mdc-drawer/mixins/ink-color.html b/test/screenshot/spec/mdc-drawer/mixins/ink-color.html index faf8d921d22..79cd1651e8f 100644 --- a/test/screenshot/spec/mdc-drawer/mixins/ink-color.html +++ b/test/screenshot/spec/mdc-drawer/mixins/ink-color.html @@ -83,7 +83,7 @@

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -92,7 +92,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -101,7 +101,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    @@ -110,7 +110,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
    anim id est laborum.

    -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
    enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    From 422375637ae67a216863e15134d6169e433ef923 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Thu, 19 Jul 2018 17:40:44 -0700 Subject: [PATCH 37/53] chore(infrastructure): Ensure Selenium tests are killed on Ctrl-C (#3150) --- test/screenshot/infra/lib/cbt-api.js | 7 +++-- test/screenshot/infra/lib/selenium-api.js | 30 +++++++++++-------- test/screenshot/infra/lib/user-agent-store.js | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/test/screenshot/infra/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js index 4100ab5c4f4..1bfc0baac80 100644 --- a/test/screenshot/infra/lib/cbt-api.js +++ b/test/screenshot/infra/lib/cbt-api.js @@ -386,11 +386,14 @@ https://crossbrowsertesting.com/account /** * @param {!Array} seleniumTestIds + * @param {boolean=} silent * @return {!Promise} */ - async killSeleniumTests(seleniumTestIds) { + async killSeleniumTests(seleniumTestIds, silent = false) { await Promise.all(seleniumTestIds.map((seleniumTestId) => { - console.log(`${colors.red('Killing')} zombie Selenium test ${colors.bold(seleniumTestId)}`); + if (!silent) { + console.log(`${colors.red('Killing')} zombie Selenium test ${colors.bold(seleniumTestId)}`); + } return this.sendRequest_('DELETE', `/selenium/${seleniumTestId}`); })); } diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 3033ebc3397..421d0a1d9f9 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -25,7 +25,7 @@ const mdcProto = require('../proto/mdc.pb').mdc.proto; const seleniumProto = require('../proto/selenium.pb').selenium.proto; const {Screenshot, TestFile, UserAgent} = mdcProto; -const {CaptureState} = Screenshot; +const {CaptureState, InclusionType} = Screenshot; const {BrowserVendorType, Navigator} = UserAgent; const {RawCapabilities} = seleniumProto; @@ -56,6 +56,7 @@ const CliStatuses = { GET: {name: 'Get', color: colors.bold.white}, CROP: {name: 'Crop', color: colors.white}, PASS: {name: 'Pass', color: colors.green}, + ADD: {name: 'Add', color: colors.bgGreen.black}, FAIL: {name: 'Fail', color: colors.red}, RETRY: {name: 'Retry', color: colors.magenta}, CAPTURED: {name: 'Captured', color: colors.bold.grey}, @@ -438,7 +439,12 @@ class SeleniumApi { this.logStatus_(CliStatuses.FAIL, message); } else { unchangedScreenshots.push(screenshot); - this.logStatus_(CliStatuses.PASS, message); + + if (screenshot.inclusion_type === InclusionType.ADD) { + this.logStatus_(CliStatuses.ADD, message); + } else { + this.logStatus_(CliStatuses.PASS, message); + } } } } @@ -617,19 +623,16 @@ class SeleniumApi { /** @private */ async killBrowsers_() { - if (this.isKilled_) { - return; - } - this.isKilled_ = true; - const ids = Array.from(this.seleniumSessionIds_); - this.seleniumSessionIds_.clear(); + const wasAlreadyKilled = this.isKilled_; - console.log('\n'); + if (!wasAlreadyKilled) { + console.log('\n'); + } - await this.cbtApi_.killSeleniumTests(ids); + this.isKilled_ = true; - console.log(`Killed ${ids.length} Selenium tests!`); + await this.cbtApi_.killSeleniumTests(ids, /* silent */ wasAlreadyKilled); // Give the HTTP requests a chance to complete before exiting await this.sleep_(Duration.seconds(4).toMillis()); @@ -658,9 +661,10 @@ class SeleniumApi { // https://stackoverflow.com/a/6774395/467582 const eraseCurrentLine = '\r' + String.fromCodePoint(27) + '[K'; const maxStatusWidth = Object.values(CliStatuses).map((status) => status.name.length).sort().reverse()[0]; - const colorStatus = status.color(status.name.toUpperCase().padStart(maxStatusWidth, ' ')); + const statusName = status.name.toUpperCase(); + const paddingSpaces = ''.padStart(maxStatusWidth - statusName.length, ' '); - console.log(eraseCurrentLine + colorStatus + ':', ...args); + console.log(eraseCurrentLine + paddingSpaces + status.color(statusName) + ':', ...args); if (process.env.TRAVIS === 'true') { return; diff --git a/test/screenshot/infra/lib/user-agent-store.js b/test/screenshot/infra/lib/user-agent-store.js index 32c04e9f9ef..ec7f2122b6b 100644 --- a/test/screenshot/infra/lib/user-agent-store.js +++ b/test/screenshot/infra/lib/user-agent-store.js @@ -82,7 +82,7 @@ Expected format: 'desktop_windows_chrome@latest'. const [, formFactorName, osVendorName, browserVendorName, browserVersionName] = matchArray; const getEnumKeysLowerCase = (enumeration) => { - return Object.keys(enumeration).map((key) => key.toLowerCase()); + return Object.keys(enumeration).filter((key) => key !== 'UNKNOWN').map((key) => key.toLowerCase()); }; // In proto3, the first enum value must always be 0. From 675e37b40cc97e3c40ec3578a32cd32e62c2957b Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Thu, 19 Jul 2018 23:07:54 -0700 Subject: [PATCH 38/53] chore(infrastructure): Post test progress to GitHub PR (#3151) ### What it does - Every 5 seconds, updates GitHub PR status check description with the test progress ### Example output #### 0% complete: ![1 - 0 percent](https://user-images.githubusercontent.com/409245/42985808-de097df0-8ba7-11e8-882e-26d7043b5522.png) #### 25% complete: ![2 - 25 percent](https://user-images.githubusercontent.com/409245/42985809-de27be82-8ba7-11e8-8da1-7729fe19297d.png) #### 100% complete: ![3 - complete](https://user-images.githubusercontent.com/409245/42985810-de44c43c-8ba7-11e8-9660-533b415e0a72.png) --- .travis.yml | 8 +-- test/screenshot/infra/commands/test.js | 4 +- test/screenshot/infra/lib/github-api.js | 59 ++++++++++++++++++----- test/screenshot/infra/lib/selenium-api.js | 35 +++++++++++++- 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index b52f6552228..4bb0670e392 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,12 +33,12 @@ matrix: env: - TEST_SUITE=screenshot script: npm run screenshot:test -- --no-fetch + install: + - rm -rf node_modules + - npm install + #- npm ls # Noisy output, but useful for debugging npm package dependency version issues before_install: # Source the script to run it in the same shell process. This ensures that any environment variables set by the # script are visible to subsequent Travis CLI commands. # https://superuser.com/a/176788/62792 - source test/screenshot/infra/commands/travis.sh -install: - - rm -rf node_modules - - npm install - #- npm ls # Noisy output, but useful for debugging npm package dependency version issues diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 8511b509c2d..6a6e7a0c141 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -39,14 +39,14 @@ module.exports = { return ExitCode.OK; } - await gitHubApi.setPullRequestStatus(reportData); + await gitHubApi.setPullRequestStatusAuto(reportData); try { await controller.uploadAllAssets(reportData); await controller.captureAllPages(reportData); await controller.compareAllScreenshots(reportData); await controller.generateReportPage(reportData); - await gitHubApi.setPullRequestStatus(reportData); + await gitHubApi.setPullRequestStatusAuto(reportData); } catch (err) { await gitHubApi.setPullRequestError(); throw err; diff --git a/test/screenshot/infra/lib/github-api.js b/test/screenshot/infra/lib/github-api.js index 9f07270d8d3..324175516e3 100644 --- a/test/screenshot/infra/lib/github-api.js +++ b/test/screenshot/infra/lib/github-api.js @@ -15,6 +15,7 @@ */ const octokit = require('@octokit/rest'); + const GitRepo = require('./git-repo'); class GitHubApi { @@ -41,6 +42,22 @@ class GitHubApi { type: 'oauth', token: token, }); + + const throttle = (fn, delay) => { + let lastCall = 0; + return (...args) => { + const now = (new Date).getTime(); + if (now - lastCall < delay) { + return; + } + lastCall = now; + return fn(...args); + }; + }; + + this.createStatusThrottled_ = throttle((...args) => { + return this.createStatusUnthrottled_(...args); + }, 5000); } /** @@ -56,17 +73,33 @@ class GitHubApi { }; } + /** + * @param {string} state + * @param {string} description + * @return {!Promise<*>} + */ + async setPullRequestStatusManual({state, description}) { + if (process.env.TRAVIS !== 'true') { + return; + } + + return await this.createStatusThrottled_({ + state, + targetUrl: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, + description, + }); + } + /** * @param {!mdc.proto.ReportData} reportData * @return {!Promise<*>} */ - async setPullRequestStatus(reportData) { - const meta = reportData.meta; - const prNumber = Number(process.env.TRAVIS_PULL_REQUEST); - if (!prNumber) { + async setPullRequestStatusAuto(reportData) { + if (process.env.TRAVIS !== 'true') { return; } + const meta = reportData.meta; const screenshots = reportData.screenshots; const numUnchanged = screenshots.unchanged_screenshot_list.length; const numChanged = @@ -90,22 +123,23 @@ class GitHubApi { targetUrl = meta.report_html_file.public_url; } else { - const numScreenshotsFormatted = screenshots.runnable_screenshot_list.length.toLocaleString(); + const runnableScreenshots = screenshots.runnable_screenshot_list; + const numTotal = runnableScreenshots.length; + state = GitHubApi.PullRequestState.PENDING; targetUrl = `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`; - description = `Running ${numScreenshotsFormatted} screenshot tests`; + description = `Running ${numTotal.toLocaleString()} screenshots...`; } - return await this.createStatus_({state, targetUrl, description}); + return await this.createStatusUnthrottled_({state, targetUrl, description}); } async setPullRequestError() { - const prNumber = Number(process.env.TRAVIS_PULL_REQUEST); - if (!prNumber) { + if (process.env.TRAVIS !== 'true') { return; } - return await this.createStatus_({ + return await this.createStatusUnthrottled_({ state: GitHubApi.PullRequestState.ERROR, targetUrl: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, description: 'Error running screenshot tests', @@ -119,11 +153,12 @@ class GitHubApi { * @return {!Promise<*>} * @private */ - async createStatus_({state, targetUrl, description = undefined}) { + async createStatusUnthrottled_({state, targetUrl, description = undefined}) { + const sha = process.env.TRAVIS_PULL_REQUEST_SHA || await this.gitRepo_.getFullCommitHash(); return await this.octokit_.repos.createStatus({ owner: 'material-components', repo: 'material-components-web', - sha: await this.gitRepo_.getFullCommitHash(process.env.TRAVIS_PULL_REQUEST_SHA), + sha, state, target_url: targetUrl, description, diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 421d0a1d9f9..a49be41804b 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -33,6 +33,7 @@ const CbtApi = require('./cbt-api'); const Cli = require('./cli'); const Constants = require('./constants'); const Duration = require('./duration'); +const GitHubApi = require('./github-api'); const ImageCropper = require('./image-cropper'); const ImageDiffer = require('./image-differ'); const LocalStorage = require('./local-storage'); @@ -79,6 +80,12 @@ class SeleniumApi { */ this.cli_ = new Cli(); + /** + * @type {!GitHubApi} + * @private + */ + this.gitHubApi_ = new GitHubApi(); + /** * @type {!ImageCropper} * @private @@ -115,6 +122,12 @@ class SeleniumApi { */ this.numCompleted_ = 0; + /** + * @type {number} + * @private + */ + this.numChanged_ = 0; + /** * @type {boolean} * @private @@ -436,6 +449,7 @@ class SeleniumApi { if (diffImageResult.has_changed) { changedScreenshots.push(screenshot); + this.numChanged_++; this.logStatus_(CliStatuses.FAIL, message); } else { unchangedScreenshots.push(screenshot); @@ -659,19 +673,36 @@ class SeleniumApi { } // https://stackoverflow.com/a/6774395/467582 - const eraseCurrentLine = '\r' + String.fromCodePoint(27) + '[K'; + const escape = String.fromCodePoint(27); + const eraseCurrentLine = `\r${escape}[K`; const maxStatusWidth = Object.values(CliStatuses).map((status) => status.name.length).sort().reverse()[0]; const statusName = status.name.toUpperCase(); const paddingSpaces = ''.padStart(maxStatusWidth - statusName.length, ' '); console.log(eraseCurrentLine + paddingSpaces + status.color(statusName) + ':', ...args); + const numDone = this.numCompleted_; + const strDone = numDone.toLocaleString(); + + const numTotal = numDone + this.numPending_; + const strTotal = numTotal.toLocaleString(); + + const numChanged = this.numChanged_; + const strChanged = numChanged.toLocaleString(); + + const numPercent = numTotal > 0 ? (100 * numDone / numTotal) : 0; + const strPercent = numPercent.toFixed(1); + if (process.env.TRAVIS === 'true') { + this.gitHubApi_.setPullRequestStatusManual({ + state: GitHubApi.PullRequestState.PENDING, + description: `${strDone} of ${strTotal} (${strPercent}%) - ${strChanged} diffs`, + }); return; } const pending = this.numPending_; - const completed = this.numCompleted_; + const completed = numDone; const total = pending + completed; const percent = (total === 0 ? 0 : (100 * completed / total).toFixed(1)); From 8512f4eaf992405239785a7f5af8204793420581 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Fri, 20 Jul 2018 11:33:50 -0700 Subject: [PATCH 39/53] chore(infrastructure): Add image anchors and deep links to report page (#3157) ### What it does - Adds a `#` link on the right side of every screenshot: - Links to that exact screenshot on the report page - Collapses all other screenshots if the hash link is present in the URL on page load - Disables fetching CBT devices in `--offline` mode - Changes the behavior of the "Collapse Images" button: - Only collapses images now - No longer expands/collapses other `

    ` elements ### Example output ![image](https://user-images.githubusercontent.com/409245/43018069-3d4c4cc4-8c0d-11e8-927b-4740581201cd.png) --- test/screenshot/infra/lib/report-builder.js | 4 ++- test/screenshot/report/_collection.hbs | 3 +++ test/screenshot/report/report.js | 30 ++++++++++++++------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/test/screenshot/infra/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js index da3524cf299..413f863a453 100644 --- a/test/screenshot/infra/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -129,7 +129,9 @@ class ReportBuilder { async initForCapture() { this.logger_.foldStart('screenshot.init', 'ReportBuilder#initForCapture()'); - await this.cbtApi_.fetchAvailableDevices(); + if (this.cli_.isOnline()) { + await this.cbtApi_.fetchAvailableDevices(); + } /** @type {!mdc.proto.ReportMeta} */ const reportMeta = await this.createReportMetaProto_(); diff --git a/test/screenshot/report/_collection.hbs b/test/screenshot/report/_collection.hbs index a7c8b4e4c60..bb037ce104e 100644 --- a/test/screenshot/report/_collection.hbs +++ b/test/screenshot/report/_collection.hbs @@ -103,6 +103,9 @@ ../html_file_path user_agent.alias }}{{/createApprovalStatusElement}} + #
    diff --git a/test/screenshot/report/report.js b/test/screenshot/report/report.js index 03a5ba7656e..20c6c3ccc00 100644 --- a/test/screenshot/report/report.js +++ b/test/screenshot/report/report.js @@ -21,6 +21,7 @@ window.mdc.reportUi = (() => { class ReportUi { constructor() { this.bindEventListeners_(); + this.collapseAllExceptDeepLink_(); this.fetchReportData_().then((reportData) => { /** @@ -116,6 +117,26 @@ window.mdc.reportUi = (() => { }); } + collapseAllExceptDeepLink_() { + const [, id] = (/^#(.+)$/.exec(location.hash || '') || []); + if (!id) { + return; + } + const deepLinkElem = document.getElementById(id); + if (!deepLinkElem) { + return; + } + const htmlFileDetailsElems = Array.from(document.querySelectorAll('details.report-html-file')); + htmlFileDetailsElems.forEach((htmlFileDetailsElem) => { + htmlFileDetailsElem.open = htmlFileDetailsElem.contains(deepLinkElem); + if (htmlFileDetailsElem.open) { + htmlFileDetailsElem.querySelectorAll('details.report-user-agent').forEach((userAgentDetailsElem) => { + userAgentDetailsElem.open = userAgentDetailsElem.contains(deepLinkElem); + }); + } + }); + } + collapseAll() { const allDetailsElems = Array.from(document.querySelectorAll('details')); allDetailsElems.forEach((detailsElem) => detailsElem.open = false); @@ -127,15 +148,6 @@ window.mdc.reportUi = (() => { } collapseImages() { - this.collapseNone(); - - const collectionDetailsElems = Array.from(document.querySelectorAll('.report-collection')); - collectionDetailsElems.forEach((detailsElem) => { - const hasScreenshots = Number(detailsElem.getAttribute('data-num-screenshots')) > 0; - const isComparable = !['skipped', 'unchanged'].includes(detailsElem.getAttribute('data-collection-type')); - detailsElem.open = hasScreenshots && isComparable; - }); - const userAgentDetailsElems = Array.from(document.querySelectorAll('.report-user-agent')); userAgentDetailsElems.forEach((detailsElem) => detailsElem.open = false); } From ce3f42842a796c90331d7fee1ae65d1a1b67ddd7 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Fri, 20 Jul 2018 20:07:36 -0700 Subject: [PATCH 40/53] chore(infrastructure): Fix CBT race condition with max parallels (#3166) ### What it does - Fixes a race condition in CBT requests: - If another user starts a test between the time we check for available VMs and the time we actually request a VM, an error is thrown by CBT. - Previously, this error would crash the entire test. - Now, we catch that error and retry the request after a short delay. - Captures browser console logs and prints them to stdout (only supported by Chrome ATM) - Prints partial stack traces for nested WebDriver errors (via `VError` lib) - Prints a warning message when `CbtApi` methods are called in `--offline` mode ### Example output #### Parallel execution limit: ![image](https://user-images.githubusercontent.com/409245/43031454-12bef8cc-8c57-11e8-9c7f-6932c5534282.png) #### Browser console log: ![image](https://user-images.githubusercontent.com/409245/43031488-c60ca88e-8c57-11e8-9eb4-ecb5c3a05fba.png) --- package-lock.json | 33 +++++++-- package.json | 1 + test/screenshot/infra/commands/test.js | 4 +- test/screenshot/infra/lib/cbt-api.js | 15 ++++ test/screenshot/infra/lib/cli.js | 11 ++- test/screenshot/infra/lib/selenium-api.js | 86 +++++++++++++++++++++-- test/screenshot/run.js | 4 +- 7 files changed, 137 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac713744181..6a4fe7b2f41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10313,6 +10313,15 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, + "requires": { + "extsprintf": "1.0.2" + } } } }, @@ -18498,12 +18507,28 @@ "dev": true }, "verror": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, "requires": { - "extsprintf": "1.0.2" + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "extsprintf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz", + "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=", + "dev": true + } } }, "vfile": { diff --git a/package.json b/package.json index 0c64707f607..f6a64c93120 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "to-slug-case": "^1.0.0", "useragent": "^2.3.0", "validate-commit-msg": "^2.6.1", + "verror": "^1.10.0", "webpack": "^3.0.0", "webpack-dev-server": "^2.4.3" }, diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 6a6e7a0c141..3479f6725a6 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -16,6 +16,8 @@ 'use strict'; +const VError = require('verror'); + const BuildCommand = require('./build'); const Controller = require('../lib/controller'); const GitHubApi = require('../lib/github-api'); @@ -49,7 +51,7 @@ module.exports = { await gitHubApi.setPullRequestStatusAuto(reportData); } catch (err) { await gitHubApi.setPullRequestError(); - throw err; + throw new VError(err, 'Failed running screenshot tests'); } return await controller.getTestExitCode(reportData); diff --git a/test/screenshot/infra/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js index 1bfc0baac80..d19b3905c59 100644 --- a/test/screenshot/infra/lib/cbt-api.js +++ b/test/screenshot/infra/lib/cbt-api.js @@ -26,6 +26,7 @@ const {FormFactorType, OsVendorType, BrowserVendorType, BrowserVersionType} = Us const {CbtAccount, CbtActiveTestCounts, CbtConcurrencyStats} = cbtProto; const {RawCapabilities} = seleniumProto; +const Cli = require('./cli'); const DiffBaseParser = require('./diff-base-parser'); const Duration = require('./duration'); @@ -40,6 +41,12 @@ let allBrowsersPromise; class CbtApi { constructor() { + /** + * @type {!Cli} + * @private + */ + this.cli_ = new Cli(); + /** * @type {!DiffBaseParser} * @private @@ -406,6 +413,14 @@ https://crossbrowsertesting.com/account * @private */ async sendRequest_(method, endpoint, body = undefined) { + if (this.cli_.isOffline()) { + console.warn( + `${colors.magenta('WARNING')}:`, + new Error('CbtApi#sendRequest_() should not be called in --offline mode') + ); + return []; + } + return request({ method, uri: `${REST_API_BASE_URL}${endpoint}`, diff --git a/test/screenshot/infra/lib/cli.js b/test/screenshot/infra/lib/cli.js index 17589d2a077..617d0a552cd 100644 --- a/test/screenshot/infra/lib/cli.js +++ b/test/screenshot/infra/lib/cli.js @@ -63,12 +63,11 @@ class Cli { * @return {!Promise} */ async checkIsOnline() { - if (typeof isOnlineCached !== 'boolean') { - if (this.offline) { - isOnlineCached = false; - } else { - isOnlineCached = await checkIsOnline({timeout: Duration.seconds(5).toMillis()}); - } + if (this.offline) { + return false; + } + if (typeof isOnlineCached === 'undefined') { + isOnlineCached = await checkIsOnline({timeout: Duration.seconds(5).toMillis()}); } return isOnlineCached; } diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index a49be41804b..e70803ae246 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -17,6 +17,7 @@ 'use strict'; const Jimp = require('jimp'); +const VError = require('verror'); const UserAgentParser = require('useragent'); const colors = require('colors/safe'); const path = require('path'); @@ -37,7 +38,7 @@ const GitHubApi = require('./github-api'); const ImageCropper = require('./image-cropper'); const ImageDiffer = require('./image-differ'); const LocalStorage = require('./local-storage'); -const {Browser, Builder, By, until} = require('selenium-webdriver'); +const {Browser, Builder, By, logging, until} = require('selenium-webdriver'); const {CBT_CONCURRENCY_POLL_INTERVAL_MS, CBT_CONCURRENCY_MAX_WAIT_MS, ExitCode} = Constants; const {SELENIUM_FONT_LOAD_WAIT_MS} = Constants; @@ -213,11 +214,12 @@ class SeleniumApi { try { changedScreenshots = (await this.driveBrowser_({reportData, userAgent, driver})).changedScreenshots; + await this.printBrowserConsoleLogs_(driver); logResult(CliStatuses.FINISHED); } catch (err) { - logResult(CliStatuses.FAILED, err); + logResult(CliStatuses.FAILED); await this.killBrowsers_(); - throw err; + throw new VError(err, 'Failed driving web browser'); } finally { logResult(CliStatuses.QUITTING); await driver.quit(); @@ -232,6 +234,26 @@ class SeleniumApi { } } + /** + * @param {!IWebDriver} driver + * @return {!Promise} + * @private + */ + async printBrowserConsoleLogs_(driver) { + const log = driver.manage().logs(); + + // Chrome is the only browser that supports logging as of 2018-07-20. + const logEntries = (await log.get(logging.Type.BROWSER).catch(() => [])).filter((logEntry) => { + // Ignore messages about missing favicon + return logEntry.message.indexOf('favicon.ico') === -1; + }); + + if (logEntries.length > 0) { + const messageColor = colors.bold.red('Browser console log:'); + console.log(`\n\n${messageColor}\n`, JSON.stringify(logEntries, null, 2), '\n'); + } + } + /** * @return {!Promise} * @private @@ -300,7 +322,7 @@ class SeleniumApi { this.logStatus_(CliStatuses.STARTING, `${userAgent.alias}...`); /** @type {!IWebDriver} */ - const driver = await driverBuilder.build(); + const driver = await this.buildWebDriverWithRetries_(driverBuilder); /** @type {!selenium.proto.RawCapabilities} */ const actualCapabilities = await this.getActualCapabilities_(driver); @@ -322,6 +344,44 @@ class SeleniumApi { return driver; } + /** + * @param {!Builder} driverBuilder + * @param {number=} startTimeMs + * @return {!Promise} + * @private + */ + async buildWebDriverWithRetries_(driverBuilder, startTimeMs = Date.now()) { + try { + return await driverBuilder.build(); + } catch (err) { + if (err.message.indexOf('maximum number of parallel') === -1) { + throw new VError(err, 'WebDriver instance could not be created'); + } + } + + /** @type {!cbt.proto.CbtConcurrencyStats} */ + const concurrencyStats = await this.cbtApi_.fetchConcurrencyStats(); + const max = concurrencyStats.max_concurrent_selenium_tests; + + // TODO(acdvorak): De-dupe this with getMaxParallelTests_() + const elapsedTimeMs = Date.now() - startTimeMs; + const elapsedTimeHuman = Duration.millis(elapsedTimeMs).toHumanShort(); + if (elapsedTimeMs > CBT_CONCURRENCY_MAX_WAIT_MS) { + throw new Error(`Timed out waiting for CBT resources to become available after ${elapsedTimeHuman}`); + } + + // TODO(acdvorak): De-dupe this with getMaxParallelTests_() + const waitTimeMs = CBT_CONCURRENCY_POLL_INTERVAL_MS; + const waitTimeHuman = Duration.millis(waitTimeMs).toHumanShort(); + this.logStatus_( + CliStatuses.WAITING, + `Parallel execution limit reached. ${max} tests are already running on CBT. Will retry in ${waitTimeHuman}...` + ); + await this.sleep_(waitTimeMs); + + return this.buildWebDriverWithRetries_(driverBuilder, startTimeMs); + } + /** * @param {!mdc.proto.ReportMeta} meta * @param {!mdc.proto.UserAgent} userAgent @@ -330,9 +390,17 @@ class SeleniumApi { */ async getDesiredCapabilities_({meta, userAgent}) { if (this.cli_.isOnline()) { - return await this.cbtApi_.getDesiredCapabilities({meta, userAgent}); + return this.cbtApi_.getDesiredCapabilities({meta, userAgent}); } + return this.createDesiredCapabilitiesOffline_({userAgent}); + } + /** + * @param {!mdc.proto.UserAgent} userAgent + * @return {!selenium.proto.RawCapabilities} + * @private + */ + createDesiredCapabilitiesOffline_({userAgent}) { const browserVendorMap = { [BrowserVendorType.CHROME]: Browser.CHROME, [BrowserVendorType.EDGE]: Browser.EDGE, @@ -431,6 +499,10 @@ class SeleniumApi { ]; for (const [isSmallComponent, screenshotQueue] of screenshotQueues) { + if (screenshotQueue.length === 0) { + continue; + } + await this.resizeWindow_({driver, isSmallComponent}); for (const screenshot of screenshotQueue) { @@ -637,6 +709,10 @@ class SeleniumApi { /** @private */ async killBrowsers_() { + if (this.cli_.isOffline()) { + return; + } + const ids = Array.from(this.seleniumSessionIds_); const wasAlreadyKilled = this.isKilled_; diff --git a/test/screenshot/run.js b/test/screenshot/run.js index c9c371f367c..d6e4d6a520a 100644 --- a/test/screenshot/run.js +++ b/test/screenshot/run.js @@ -16,6 +16,7 @@ 'use strict'; +const colors = require('colors'); const Cli = require('./infra/lib/cli'); const Duration = require('./infra/lib/duration'); const {ExitCode} = require('./infra/lib/constants'); @@ -76,7 +77,7 @@ async function runAsync() { } }, (err) => { - console.error(err); + console.error('\n\n' + colors.bold.red('ERROR:'), err); process.exit(ExitCode.UNKNOWN_ERROR); } ); @@ -97,6 +98,7 @@ process.on('unhandledRejection', (error) => { 'This error originated either by throwing inside of an async function without a catch block,', 'or by rejecting a promise which was not handled with .catch().', ].join(' '); + console.error('\n'); console.error(message); console.error(error); process.exit(ExitCode.UNHANDLED_PROMISE_REJECTION); From 73dc071808ff1b90c70a442d9cbc30a6f21a175c Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Fri, 20 Jul 2018 23:00:57 -0700 Subject: [PATCH 41/53] chore(infrastructure): Disable `git fetch` by default (#3168) ### What it does - Disables `git fetch` by default, but enables it IFF: 1. `--diff-base` looks like a remote branch or version tag (e.g., `origin/master` or `v0.37.1`); and 2. the user has not explicitly disabled fetching with the `--no-fetch` CLI flag - Swaps the icons for "Collapse All" and "Collapse None" buttons - Removes the blank space between the browser name and `#` deep link on the report page - Tweaks the wording of a few log messages ### Example output screenshot --- test/screenshot/infra/commands/test.js | 2 +- test/screenshot/infra/lib/cli.js | 4 ++-- test/screenshot/infra/lib/controller.js | 9 --------- test/screenshot/infra/lib/diff-base-parser.js | 12 ++++++++++-- test/screenshot/infra/lib/git-repo.js | 7 +++++++ test/screenshot/infra/lib/report-builder.js | 2 +- test/screenshot/infra/lib/selenium-api.js | 2 +- test/screenshot/report/_footer.hbs | 4 ++-- test/screenshot/report/report.scss | 2 +- 9 files changed, 25 insertions(+), 19 deletions(-) diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 3479f6725a6..2e54a75f5f3 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -51,7 +51,7 @@ module.exports = { await gitHubApi.setPullRequestStatusAuto(reportData); } catch (err) { await gitHubApi.setPullRequestError(); - throw new VError(err, 'Failed running screenshot tests'); + throw new VError(err, 'Failed to run screenshot tests'); } return await controller.getTestExitCode(reportData); diff --git a/test/screenshot/infra/lib/cli.js b/test/screenshot/infra/lib/cli.js index 617d0a552cd..557ea71139a 100644 --- a/test/screenshot/infra/lib/cli.js +++ b/test/screenshot/infra/lib/cli.js @@ -355,8 +355,8 @@ E.g.: '--browser=chrome,-mobile' is the same as '--browser=chrome --browser=-mob } /** @return {boolean} */ - get shouldFetch() { - return !this.args_['--no-fetch']; + get skipFetch() { + return this.args_['--no-fetch']; } /** @return {?string} */ diff --git a/test/screenshot/infra/lib/controller.js b/test/screenshot/infra/lib/controller.js index c592304bdf9..59b45108786 100644 --- a/test/screenshot/infra/lib/controller.js +++ b/test/screenshot/infra/lib/controller.js @@ -101,10 +101,6 @@ class Controller { */ async initForCapture() { const isOnline = this.cli_.isOnline(); - const shouldFetch = this.cli_.shouldFetch; - if (isOnline && shouldFetch) { - await this.gitRepo_.fetch(); - } if (isOnline) { await this.cbtApi_.killStalledSeleniumTests(); } @@ -115,11 +111,6 @@ class Controller { * @return {!Promise} */ async initForDemo() { - const isOnline = this.cli_.isOnline(); - const shouldFetch = this.cli_.shouldFetch; - if (isOnline && shouldFetch) { - await this.gitRepo_.fetch(); - } return this.reportBuilder_.initForDemo(); } diff --git a/test/screenshot/infra/lib/diff-base-parser.js b/test/screenshot/infra/lib/diff-base-parser.js index 4b1954199fb..446841bee58 100644 --- a/test/screenshot/infra/lib/diff-base-parser.js +++ b/test/screenshot/infra/lib/diff-base-parser.js @@ -65,7 +65,6 @@ class DiffBaseParser { } /** - * TODO(acdvorak): Move this method out of DiffBaseParser class - it doesn't belong here. * @param {string} rawDiffBase * @return {!Promise} */ @@ -88,7 +87,6 @@ class DiffBaseParser { } /** - * TODO(acdvorak): Move this method out of DiffBaseParser class - it doesn't belong here. * @param {string} rawDiffBase * @return {!Promise} * @private @@ -110,6 +108,16 @@ class DiffBaseParser { const [inputGoldenRef, inputGoldenPath] = rawDiffBase.split(':'); const goldenFilePath = inputGoldenPath || GOLDEN_JSON_RELATIVE_PATH; + + const isRemoteBranch = inputGoldenRef.startsWith('origin/'); + const isVersionTag = /^v[0-9.]+$/.test(inputGoldenRef); + const isFetchable = isRemoteBranch || isVersionTag; + const skipFetch = this.cli_.skipFetch; + const isOnline = this.cli_.isOnline(); + if (isFetchable && !skipFetch && isOnline) { + await this.gitRepo_.fetch(); + } + const fullGoldenRef = await this.gitRepo_.getFullSymbolicName(inputGoldenRef); // Diff against a specific git commit. diff --git a/test/screenshot/infra/lib/git-repo.js b/test/screenshot/infra/lib/git-repo.js index 8fe97882f25..bc5f1127ef4 100644 --- a/test/screenshot/infra/lib/git-repo.js +++ b/test/screenshot/infra/lib/git-repo.js @@ -21,6 +21,8 @@ const simpleGit = require('simple-git/promise'); const mdcProto = require('../proto/mdc.pb').mdc.proto; const {User} = mdcProto; +let hasFetched = false; + class GitRepo { constructor(workingDirPath = undefined) { /** @@ -49,6 +51,11 @@ class GitRepo { * @return {!Promise} */ async fetch(args = []) { + if (hasFetched) { + return; + } + hasFetched = true; + console.log('Fetching remote git commits...'); const prFetchRef = '+refs/pull/*/head:refs/remotes/origin/pr/*'; diff --git a/test/screenshot/infra/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js index 413f863a453..4c40801269d 100644 --- a/test/screenshot/infra/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -305,7 +305,7 @@ class ReportBuilder { */ async prefetchGoldenImages_(reportData) { // TODO(acdvorak): Figure out how to handle offline mode for prefetching and diffing - console.log('Prefetching golden images...'); + console.log('Fetching golden images...'); await Promise.all( reportData.screenshots.expected_screenshot_list.map((expectedScreenshot) => { return this.prefetchScreenshotImages_(expectedScreenshot); diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index e70803ae246..6ca034f508d 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -219,7 +219,7 @@ class SeleniumApi { } catch (err) { logResult(CliStatuses.FAILED); await this.killBrowsers_(); - throw new VError(err, 'Failed driving web browser'); + throw new VError(err, 'Failed to drive web browser'); } finally { logResult(CliStatuses.QUITTING); await driver.quit(); diff --git a/test/screenshot/report/_footer.hbs b/test/screenshot/report/_footer.hbs index b7869b6170c..6a28ef74723 100644 --- a/test/screenshot/report/_footer.hbs +++ b/test/screenshot/report/_footer.hbs @@ -46,10 +46,10 @@ Collapse:
    +``` + +> _NOTE:_ The extended FAB must contain label where as the icon is optional. The icon and label may be specified in whichever order is appropriate based on context. + ## Style Customization ### CSS Classes From 8ccd4b6ac51159f151b1954dc772db283954cda9 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 23 Jul 2018 15:58:15 -0700 Subject: [PATCH 44/53] chore(infrastructure): Report diffs against `master` in PRs (#3171) ### What it does * Diffs PRs against the branch they're merging into (usually `origin/master`) - Still diffs against the local PR branch as well - Diffs against the local PR branch are considered test failures and will fail the Travis build - Diffs against the merge target branch are assumed to be intentional, and do not fail the Travis build * Posts diff results for the target merge branch (usually `origin/master`) as a comment on GitHub PRs * Increases Travis `git clone --depth` from 50 (the default) to 200 - Ensures that large PRs clone enough commits to diff against `master` and calculate the commit distance since the last MDC release * Refactors `commands/*.js` to export classes instead of anonymous objects * Refactors CLI color handling ### Example output * Report page: https://storage.googleapis.com/mdc-web-screenshot-tests/travis/2018/07/23/00_10_15_695/report/report.html #### No Diffs: ![image](https://user-images.githubusercontent.com/409245/43100589-a340ae70-8e7a-11e8-901a-435661184922.png) ![GitHub PR comment screenshot - no diffs](https://user-images.githubusercontent.com/409245/43051546-65995fcc-8dd0-11e8-82b1-1a0d497a9a98.png) #### 28 Diffs: ![GitHub PR comment screenshot - 28 diffs](https://user-images.githubusercontent.com/409245/43051743-e66df1a6-8dd2-11e8-9722-5076083925c2.png) --- .travis.yml | 3 +- test/screenshot/infra/commands/approve.js | 11 +- test/screenshot/infra/commands/build.js | 49 +- test/screenshot/infra/commands/clean.js | 12 +- test/screenshot/infra/commands/demo.js | 16 +- test/screenshot/infra/commands/index.js | 11 +- test/screenshot/infra/commands/proto.js | 14 +- test/screenshot/infra/commands/serve.js | 11 +- test/screenshot/infra/commands/test.js | 459 +++++++++++++++++- test/screenshot/infra/lib/cbt-api.js | 6 +- test/screenshot/infra/lib/controller.js | 142 +----- test/screenshot/infra/lib/diff-base-parser.js | 97 +++- test/screenshot/infra/lib/git-repo.js | 49 +- test/screenshot/infra/lib/github-api.js | 46 +- test/screenshot/infra/lib/golden-io.js | 33 +- test/screenshot/infra/lib/local-storage.js | 61 ++- test/screenshot/infra/lib/report-builder.js | 62 ++- test/screenshot/infra/lib/report-writer.js | 3 +- test/screenshot/infra/lib/selenium-api.js | 6 +- test/screenshot/report/report.js | 6 +- test/screenshot/run.js | 52 +- 21 files changed, 847 insertions(+), 302 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4bb0670e392..c1779d7a86e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,9 +32,10 @@ matrix: - node_js: 8 env: - TEST_SUITE=screenshot + git: + depth: 200 script: npm run screenshot:test -- --no-fetch install: - - rm -rf node_modules - npm install #- npm ls # Noisy output, but useful for debugging npm package dependency version issues before_install: diff --git a/test/screenshot/infra/commands/approve.js b/test/screenshot/infra/commands/approve.js index 90309b732e0..36fda3e0917 100644 --- a/test/screenshot/infra/commands/approve.js +++ b/test/screenshot/infra/commands/approve.js @@ -18,10 +18,15 @@ const Controller = require('../lib/controller'); -module.exports = { +class ApproveCommand { + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { const controller = new Controller(); const reportData = await controller.initForApproval(); await controller.approveChanges(reportData); - }, -}; + } +} + +module.exports = ApproveCommand; diff --git a/test/screenshot/infra/commands/build.js b/test/screenshot/infra/commands/build.js index 0cc872fb394..6e15aacc5a0 100644 --- a/test/screenshot/infra/commands/build.js +++ b/test/screenshot/infra/commands/build.js @@ -27,13 +27,22 @@ const ProcessManager = require('../lib/process-manager'); const ProtoCommand = require('./proto'); const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); -const logger = new Logger(__filename); -const processManager = new ProcessManager(); +class BuildCommand { + constructor() { + this.logger_ = new Logger(__filename); + this.processManager_ = new ProcessManager(); -module.exports = { + this.cleanCommand_ = new CleanCommand(); + this.indexCommand_ = new IndexCommand(); + this.protoCommand_ = new ProtoCommand(); + } + + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { // Travis sometimes forgets to emit this - logger.foldEnd('install.npm'); + this.logger_.foldEnd('install.npm'); const shouldBuild = await this.shouldBuild_(); const shouldWatch = await this.shouldWatch_(); @@ -42,28 +51,28 @@ module.exports = { return; } - await CleanCommand.runAsync(); + await this.cleanCommand_.runAsync(); - logger.foldStart('screenshot.build', 'Compiling source files'); + this.logger_.foldStart('screenshot.build', 'Compiling source files'); this.buildProtoFiles_(shouldWatch); this.buildHtmlFiles_(shouldWatch); if (shouldWatch) { - processManager.spawnChildProcess('npm', ['run', 'screenshot:webpack', '--', '--watch']); + this.processManager_.spawnChildProcess('npm', ['run', 'screenshot:webpack', '--', '--watch']); } else { - processManager.spawnChildProcessSync('npm', ['run', 'screenshot:webpack']); + this.processManager_.spawnChildProcessSync('npm', ['run', 'screenshot:webpack']); } - logger.foldEnd('screenshot.build'); - }, + this.logger_.foldEnd('screenshot.build'); + } /** * @param {boolean} shouldWatch * @private */ buildProtoFiles_(shouldWatch) { - const buildRightNow = () => ProtoCommand.runAsync(); + const buildRightNow = () => this.protoCommand_.runAsync(); const buildDelayed = debounce(buildRightNow, 1000); if (!shouldWatch) { @@ -78,14 +87,14 @@ module.exports = { watcher.on('add', buildDelayed); watcher.on('change', buildDelayed); - }, + } /** * @param {boolean} shouldWatch * @private */ buildHtmlFiles_(shouldWatch) { - const buildRightNow = () => IndexCommand.runAsync(); + const buildRightNow = () => this.indexCommand_.runAsync(); const buildDelayed = debounce(buildRightNow, 1000); if (!shouldWatch) { @@ -101,7 +110,7 @@ module.exports = { watcher.on('add', buildDelayed); watcher.on('unlink', buildDelayed); - }, + } /** * @return {!Promise} @@ -121,7 +130,7 @@ module.exports = { } return true; - }, + } /** * @return {!Promise} @@ -130,7 +139,7 @@ module.exports = { async shouldWatch_() { const cli = new Cli(); return cli.watch; - }, + } /** * TODO(acvdorak): Store PID in local text file instead of scanning through running processes @@ -139,7 +148,7 @@ module.exports = { */ async getExistingProcessId_() { /** @type {!Array} */ - const allProcs = await processManager.getRunningProcessesInPwdAsync('node', 'build'); + const allProcs = await this.processManager_.getRunningProcessesInPwdAsync('node', 'build'); const buildProcs = allProcs.filter((proc) => { const [script, command] = proc.arguments; return ( @@ -149,5 +158,7 @@ module.exports = { }); return buildProcs.length > 0 ? buildProcs[0].pid : null; - }, -}; + } +} + +module.exports = BuildCommand; diff --git a/test/screenshot/infra/commands/clean.js b/test/screenshot/infra/commands/clean.js index e2de2343d25..fdc57785160 100644 --- a/test/screenshot/infra/commands/clean.js +++ b/test/screenshot/infra/commands/clean.js @@ -19,14 +19,20 @@ const del = require('del'); const mkdirp = require('mkdirp'); const path = require('path'); + const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); -module.exports = { +class CleanCommand { + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { const relativePathPatterns = ['out', '**/index.html'].map((filename) => { return path.join(TEST_DIR_RELATIVE_PATH, filename); }); await del(relativePathPatterns); mkdirp.sync(path.join(TEST_DIR_RELATIVE_PATH, 'out')); - }, -}; + } +} + +module.exports = CleanCommand; diff --git a/test/screenshot/infra/commands/demo.js b/test/screenshot/infra/commands/demo.js index 1242dd885b7..dabfc0dc9cf 100644 --- a/test/screenshot/infra/commands/demo.js +++ b/test/screenshot/infra/commands/demo.js @@ -19,11 +19,19 @@ const BuildCommand = require('./build'); const Controller = require('../lib/controller'); -module.exports = { +class DemoCommand { + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { - await BuildCommand.runAsync(); + const buildCommand = new BuildCommand(); const controller = new Controller(); + + await buildCommand.runAsync(); + const reportData = await controller.initForDemo(); await controller.uploadAllAssets(reportData); - }, -}; + } +} + +module.exports = DemoCommand; diff --git a/test/screenshot/infra/commands/index.js b/test/screenshot/infra/commands/index.js index d44542c080e..6aa24cab361 100644 --- a/test/screenshot/infra/commands/index.js +++ b/test/screenshot/infra/commands/index.js @@ -22,7 +22,10 @@ const LocalStorage = require('../lib/local-storage'); const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); // TODO(acdvorak): Clean up this entire file. It's gross. -module.exports = { +class IndexCommand { + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { const localStorage = new LocalStorage(); @@ -130,5 +133,7 @@ module.exports = { await walkDir(path.join(TEST_DIR_RELATIVE_PATH)); await localStorage.delete([fakeReportHtmlFilePath, fakeReportJsonFilePath]); - }, -}; + } +} + +module.exports = IndexCommand; diff --git a/test/screenshot/infra/commands/proto.js b/test/screenshot/infra/commands/proto.js index d7701f78588..fa2a22cbb9f 100644 --- a/test/screenshot/infra/commands/proto.js +++ b/test/screenshot/infra/commands/proto.js @@ -22,10 +22,12 @@ const path = require('path'); const ProcessManager = require('../lib/process-manager'); const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); -const processManager = new ProcessManager(); - -module.exports = { +class ProtoCommand { + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { + const processManager = new ProcessManager(); const protoFilePaths = glob.sync(path.join(TEST_DIR_RELATIVE_PATH, '**/*.proto')); const cmd = 'pbjs'; @@ -35,5 +37,7 @@ module.exports = { const jsFilePath = protoFilePath.replace(/.proto$/, '.pb.js'); processManager.spawnChildProcessSync(cmd, args.concat(`--out=${jsFilePath}`, protoFilePath)); } - }, -}; + } +} + +module.exports = ProtoCommand; diff --git a/test/screenshot/infra/commands/serve.js b/test/screenshot/infra/commands/serve.js index 4063635a5c9..000f90953be 100644 --- a/test/screenshot/infra/commands/serve.js +++ b/test/screenshot/infra/commands/serve.js @@ -24,7 +24,10 @@ const Cli = require('../lib/cli'); const {ExitCode} = require('../lib/constants'); const {TEST_DIR_RELATIVE_PATH} = require('../lib/constants'); -module.exports = { +class ServeCommand { + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { const cli = new Cli(); const {port} = cli; @@ -45,5 +48,7 @@ Local development server running on http://localhost:${port}/ ========================================================== `); }); - }, -}; + } +} + +module.exports = ServeCommand; diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 2e54a75f5f3..9a9574a32e9 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -18,42 +18,467 @@ const VError = require('verror'); +const mdcProto = require('../proto/mdc.pb').mdc.proto; +const GitRevision = mdcProto.GitRevision; + const BuildCommand = require('./build'); +const Cli = require('../lib/cli'); +const CliColor = require('../lib/logger').colors; +const DiffBaseParser = require('../lib/diff-base-parser'); +const Duration = require('../lib/duration'); const Controller = require('../lib/controller'); const GitHubApi = require('../lib/github-api'); +const ImageDiffer = require('../lib/image-differ'); const Logger = require('../lib/logger'); const {ExitCode} = require('../lib/constants'); -module.exports = { +// TODO(acdvorak): Refactor most of this class out into a separate file +class TestCommand { + constructor() { + this.cli_ = new Cli(); + this.diffBaseParser_ = new DiffBaseParser(); + this.gitHubApi_ = new GitHubApi(); + this.imageDiffer_ = new ImageDiffer(); + this.logger_ = new Logger(__filename); + } + + /** + * @return {!Promise} Process exit code. If no exit code is returned, `0` is assumed. + */ async runAsync() { - await BuildCommand.runAsync(); - const controller = new Controller(); - const gitHubApi = new GitHubApi(); - const logger = new Logger(__filename); + await this.build_(); - /** @type {!mdc.proto.ReportData} */ - const reportData = await controller.initForCapture(); + /** @type {!mdc.proto.DiffBase} */ + const snapshotDiffBase = await this.diffBaseParser_.parseGoldenDiffBase(); + const snapshotGitRev = snapshotDiffBase.git_revision; + + const isTravisPr = snapshotGitRev && snapshotGitRev.type === GitRevision.Type.TRAVIS_PR; + const isTestable = isTravisPr ? snapshotGitRev.pr_file_paths.length > 0 : true; - const {isTestable, prNumber} = controller.checkIsTestable(reportData); if (!isTestable) { - logger.warn(`PR #${prNumber} does not contain any testable source file changes.`); - logger.warn('Skipping screenshot tests.'); + this.logUntestablePr_(snapshotGitRev.pr_number); return ExitCode.OK; } - await gitHubApi.setPullRequestStatusAuto(reportData); + // TODO(acdvorak): Find a better word than "local" + /** @type {!mdc.proto.ReportData} */ + const localDiffReportData = await this.diffAgainstLocal_(snapshotDiffBase); + const localDiffExitCode = this.getExitCode_(localDiffReportData); + if (localDiffExitCode !== ExitCode.OK) { + this.logTestResults_(localDiffReportData); + return localDiffExitCode; + } + + if (isTravisPr) { + /** @type {!mdc.proto.ReportData} */ + const masterDiffReportData = await this.diffAgainstMaster_({localDiffReportData, snapshotGitRev}); + this.logTestResults_(localDiffReportData); + this.logTestResults_(masterDiffReportData); + } + + // Diffs against master shouldn't fail the Travis job. + return ExitCode.OK; + } + + async build_() { + const buildCommand = new BuildCommand(); + await buildCommand.runAsync(); + } + + /** + * @param {!mdc.proto.DiffBase} goldenDiffBase + * @return {!Promise} + * @private + */ + async diffAgainstLocal_(goldenDiffBase) { + const controller = new Controller(); + + /** @type {!mdc.proto.ReportData} */ + const reportData = await controller.initForCapture(goldenDiffBase); try { + await this.gitHubApi_.setPullRequestStatusAuto(reportData); await controller.uploadAllAssets(reportData); await controller.captureAllPages(reportData); - await controller.compareAllScreenshots(reportData); + + controller.populateMaps(reportData); + + await controller.uploadAllImages(reportData); await controller.generateReportPage(reportData); - await gitHubApi.setPullRequestStatusAuto(reportData); + + await this.gitHubApi_.setPullRequestStatusAuto(reportData); + + this.logComparisonResults_(reportData); } catch (err) { - await gitHubApi.setPullRequestError(); + await this.gitHubApi_.setPullRequestError(); throw new VError(err, 'Failed to run screenshot tests'); } - return await controller.getTestExitCode(reportData); - }, -}; + return reportData; + } + + /** + * TODO(acdvorak): Rename this method + * @param {!mdc.proto.DiffBase} goldenDiffBase + * @param {!Array} capturedScreenshots + * @return {!Promise} + * @private + */ + async diffAgainstMasterImpl_(goldenDiffBase, capturedScreenshots) { + const controller = new Controller(); + + /** @type {!mdc.proto.ReportData} */ + const reportData = await controller.initForCapture(goldenDiffBase); + + try { + await controller.uploadAllAssets(reportData); + await this.copyAndCompareScreenshots_({reportData, capturedScreenshots}); + + controller.populateMaps(reportData); + + await controller.uploadAllImages(reportData); + await controller.generateReportPage(reportData); + + this.logComparisonResults_(reportData); + } catch (err) { + await this.gitHubApi_.setPullRequestError(); + throw new VError(err, 'Failed to run screenshot tests'); + } + + return reportData; + } + + /** + * TODO(acdvorak): Rename this method + * @param {!mdc.proto.ReportData} localDiffReportData + * @param {!mdc.proto.GitRevision} snapshotGitRev + * @return {!Promise} + * @private + */ + async diffAgainstMaster_({localDiffReportData, snapshotGitRev}) { + const localScreenshots = localDiffReportData.screenshots; + + /** @type {!Array} */ + const capturedScreenshots = [].concat( + localScreenshots.changed_screenshot_list, + localScreenshots.added_screenshot_list, + localScreenshots.removed_screenshot_list, + localScreenshots.unchanged_screenshot_list, + ); + + /** @type {!mdc.proto.DiffBase} */ + const masterDiffBase = await this.diffBaseParser_.parseMasterDiffBase(); + + /** @type {!mdc.proto.ReportData} */ + const masterDiffReportData = await this.diffAgainstMasterImpl_(masterDiffBase, capturedScreenshots); + + masterDiffReportData.meta.start_time_iso_utc = localDiffReportData.meta.start_time_iso_utc; + masterDiffReportData.meta.end_time_iso_utc = new Date().toISOString(); + masterDiffReportData.meta.duration_ms = Duration.elapsed( + masterDiffReportData.meta.start_time_iso_utc, + masterDiffReportData.meta.end_time_iso_utc + ).toMillis(); + + const prNumber = snapshotGitRev.pr_number; + const comment = this.getPrComment_({masterDiffReportData, snapshotGitRev}); + await this.gitHubApi_.createPullRequestComment({prNumber, comment}); + + return masterDiffReportData; + } + + /** + * @param {!mdc.proto.ReportData} reportData + * @param {!Array} capturedScreenshots + * @return {!Promise} + * @private + */ + async copyAndCompareScreenshots_({reportData, capturedScreenshots}) { + const num = capturedScreenshots.length; + const plural = num === 1 ? '' : 's'; + this.logger_.foldStart('screenshot.compare_master', `Comparing ${num} screenshot${plural} to master`); + + const promises = []; + const screenshots = reportData.screenshots; + const masterScreenshots = screenshots.actual_screenshot_list; + + for (const masterScreenshot of masterScreenshots) { + for (const capturedScreenshot of capturedScreenshots) { + if (capturedScreenshot.html_file_path !== masterScreenshot.html_file_path || + capturedScreenshot.user_agent.alias !== masterScreenshot.user_agent.alias) { + continue; + } + promises.push(new Promise(async (resolve) => { + masterScreenshot.actual_html_file = capturedScreenshot.actual_html_file; + masterScreenshot.actual_image_file = capturedScreenshot.actual_image_file; + masterScreenshot.capture_state = capturedScreenshot.capture_state; + + /** @type {!mdc.proto.DiffImageResult} */ + const diffImageResult = await this.imageDiffer_.compareOneScreenshot({ + meta: reportData.meta, + screenshot: masterScreenshot, + }); + + masterScreenshot.diff_image_result = diffImageResult; + masterScreenshot.diff_image_file = diffImageResult.diff_image_file; + + if (diffImageResult.has_changed) { + reportData.screenshots.changed_screenshot_list.push(masterScreenshot); + } else { + reportData.screenshots.unchanged_screenshot_list.push(masterScreenshot); + } + reportData.screenshots.comparable_screenshot_list.push(masterScreenshot); + + resolve(); + })); + } + } + + await Promise.all(promises); + + this.logger_.foldEnd('screenshot.compare_master'); + } + + /** + * @param {!mdc.proto.ReportData} masterDiffReportData + * @param {!mdc.proto.GitRevision} snapshotGitRev + * @return {string} + * @private + */ + getPrComment_({masterDiffReportData, snapshotGitRev}) { + const reportPageUrl = masterDiffReportData.meta.report_html_file.public_url; + const masterScreenshots = masterDiffReportData.screenshots; + + const listMarkdown = [ + this.getChangelistMarkdown_( + 'Changed', masterScreenshots.changed_screenshot_list, masterScreenshots.changed_screenshot_page_map + ), + this.getChangelistMarkdown_( + 'Added', masterScreenshots.added_screenshot_list, masterScreenshots.added_screenshot_page_map + ), + this.getChangelistMarkdown_( + 'Removed', masterScreenshots.removed_screenshot_list, masterScreenshots.removed_screenshot_page_map + ), + ].filter((str) => Boolean(str)).join('\n\n'); + + let contentMarkdown; + + const numChanged = + masterScreenshots.changed_screenshot_list.length + + masterScreenshots.added_screenshot_list.length + + masterScreenshots.removed_screenshot_list.length; + + if (listMarkdown) { + contentMarkdown = ` +
    + ${numChanged} screenshot${numChanged === 1 ? '' : 's'} changed ⚠️ +
    + +${listMarkdown} + +
    +
    +`.trim(); + } else { + contentMarkdown = '### No diffs! 💯🎉'; + } + + return ` +🤖 Beep boop! + +### Screenshot test report + +Commit ${snapshotGitRev.commit} vs. \`master\`: + +* ${reportPageUrl} + +${contentMarkdown} +`.trim(); + } + + /** + * @param {string} verb + * @param {!Array} screenshotArray + * @param {!Object} screenshotMap + */ + getChangelistMarkdown_(verb, screenshotArray, screenshotMap) { + const numHtmlFiles = Object.keys(screenshotMap).length; + if (numHtmlFiles === 0) { + return null; + } + + const listItemMarkdown = Object.entries(screenshotMap).map(([htmlFilePath, screenshotList]) => { + const browserIconMarkup = this.getAllBrowserIcons_(screenshotList.screenshots); + const firstScreenshot = screenshotList.screenshots[0]; + const htmlFileUrl = (firstScreenshot.actual_html_file || firstScreenshot.expected_html_file).public_url; + + return ` +* [\`${htmlFilePath}\`](${htmlFileUrl}) ${browserIconMarkup} +`.trim(); + }).join('\n'); + + return ` +#### ${screenshotArray.length} ${verb}: + +${listItemMarkdown} +`; + } + + /** + * @param {!Array} screenshotArray + * @return {string} + * @private + */ + getAllBrowserIcons_(screenshotArray) { + return screenshotArray.map((screenshot) => { + return this.getOneBrowserIcon_(screenshot); + }).join(' '); + } + + /** + * @param screenshot + * @return {string} + * @private + */ + getOneBrowserIcon_(screenshot) { + const imgFile = screenshot.diff_image_file || screenshot.actual_image_file || screenshot.expected_image_file; + const linkUrl = imgFile.public_url; + + const untrimmed = ` + + + +`; + return untrimmed.trim().replace(/>\n *<'); + } + + /** + * @return {string} + * @private + */ + getCongratulatoryMarkdown_() { + return ` +### No diffs! 💯🎉 +`; + } + + /** + * @param {!mdc.proto.ReportData} reportData + * @return {!ExitCode|number} + */ + getExitCode_(reportData) { + // TODO(acdvorak): Store this directly in the proto so we don't have to recalculate it all over the place + const numChanges = + reportData.screenshots.changed_screenshot_list.length + + reportData.screenshots.added_screenshot_list.length + + reportData.screenshots.removed_screenshot_list.length; + + const isOnline = this.cli_.isOnline(); + if (isOnline && numChanges > 0) { + return ExitCode.CHANGES_FOUND; + } + return ExitCode.OK; + } + + /** + * @param {number} prNumber + * @private + */ + logUntestablePr_(prNumber) { + this.logger_.warn(` +${CliColor.underline(`PR #${prNumber}`)} does not contain any testable source file changes. + +${CliColor.bold.green('Skipping screenshot tests.')} +`); + } + + /** + * @param {!mdc.proto.ReportData} reportData + * @private + */ + logComparisonResults_(reportData) { + this.logger_.foldStart('screenshot.diff_results', 'Diff results'); + this.logComparisonResultSet_('Skipped', reportData.screenshots.skipped_screenshot_list); + this.logComparisonResultSet_('Unchanged', reportData.screenshots.unchanged_screenshot_list); + this.logComparisonResultSet_('Removed', reportData.screenshots.removed_screenshot_list); + this.logComparisonResultSet_('Added', reportData.screenshots.added_screenshot_list); + this.logComparisonResultSet_('Changed', reportData.screenshots.changed_screenshot_list); + this.logger_.foldEnd('screenshot.diff_results'); + } + + /** + * @param {!mdc.proto.ReportData} reportData + * @private + */ + logTestResults_(reportData) { + // TODO(acdvorak): Store this directly in the proto so we don't have to recalculate it all over the place + const numChanges = + reportData.screenshots.changed_screenshot_list.length + + reportData.screenshots.added_screenshot_list.length + + reportData.screenshots.removed_screenshot_list.length; + + const boldRed = CliColor.bold.red; + const boldGreen = CliColor.bold.green; + const color = numChanges === 0 ? boldGreen : boldRed; + + const goldenDisplayName = this.getDisplayName_(reportData.meta.golden_diff_base); + const snapshotDisplayName = this.getDisplayName_(reportData.meta.snapshot_diff_base); + + const changedMsg = `${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!`; + const reportPageUrl = reportData.meta.report_html_file.public_url; + + const headingPlain = 'Screenshot Test Results'; + const headingColor = CliColor.bold(headingPlain); + const headingUnderline = ''.padEnd(headingPlain.length, '='); + + this.logger_.log(` + +${headingColor} +${headingUnderline} + + - Golden: ${goldenDisplayName} + - Snapshot: ${snapshotDisplayName} + - Changes: ${color(changedMsg)} + - Report: ${color(reportPageUrl)} +`.trimRight()); + } + + /** + * @param {!mdc.proto.DiffBase} diffBase + * @return {string} + * @private + */ + getDisplayName_(diffBase) { + const gitRev = diffBase.git_revision; + if (gitRev) { + const commitShort = gitRev.commit.substr(0, 7); + if (gitRev.pr_number) { + return `PR #${gitRev.pr_number} (commit ${commitShort})`; + } + if (gitRev.tag) { + return `${gitRev.tag} (commit ${commitShort})`; + } + if (gitRev.branch) { + return `${gitRev.remote ? `${gitRev.remote}/${gitRev.branch}` : gitRev.branch} (commit ${commitShort})`; + } + return commitShort; + } + return diffBase.local_file_path || diffBase.public_url; + } + + /** + * @param {string} title + * @param {!Array} screenshots + * @private + */ + logComparisonResultSet_(title, screenshots) { + const num = screenshots.length; + console.log(`${title} ${num} screenshot${num === 1 ? '' : 's'}${num > 0 ? ':' : ''}`); + for (const screenshot of screenshots) { + console.log(` - ${screenshot.html_file_path} > ${screenshot.user_agent.alias}`); + } + console.log(''); + } +} + +module.exports = TestCommand; diff --git a/test/screenshot/infra/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js index d19b3905c59..a9b040e19c4 100644 --- a/test/screenshot/infra/lib/cbt-api.js +++ b/test/screenshot/infra/lib/cbt-api.js @@ -14,7 +14,6 @@ * limitations under the License. */ -const colors = require('colors'); const request = require('request-promise-native'); const mdcProto = require('../proto/mdc.pb').mdc.proto; @@ -27,6 +26,7 @@ const {CbtAccount, CbtActiveTestCounts, CbtConcurrencyStats} = cbtProto; const {RawCapabilities} = seleniumProto; const Cli = require('./cli'); +const CliColor = require('./logger').colors; const DiffBaseParser = require('./diff-base-parser'); const Duration = require('./duration'); @@ -399,7 +399,7 @@ https://crossbrowsertesting.com/account async killSeleniumTests(seleniumTestIds, silent = false) { await Promise.all(seleniumTestIds.map((seleniumTestId) => { if (!silent) { - console.log(`${colors.red('Killing')} zombie Selenium test ${colors.bold(seleniumTestId)}`); + console.log(`${CliColor.red('Killing')} zombie Selenium test ${CliColor.bold(seleniumTestId)}`); } return this.sendRequest_('DELETE', `/selenium/${seleniumTestId}`); })); @@ -415,7 +415,7 @@ https://crossbrowsertesting.com/account async sendRequest_(method, endpoint, body = undefined) { if (this.cli_.isOffline()) { console.warn( - `${colors.magenta('WARNING')}:`, + `${CliColor.magenta('WARNING')}:`, new Error('CbtApi#sendRequest_() should not be called in --offline mode') ); return []; diff --git a/test/screenshot/infra/lib/controller.js b/test/screenshot/infra/lib/controller.js index 6ca6742cdb6..edbe56eb669 100644 --- a/test/screenshot/infra/lib/controller.js +++ b/test/screenshot/infra/lib/controller.js @@ -16,9 +16,6 @@ 'use strict'; -const mdcProto = require('../proto/mdc.pb').mdc.proto; -const {GitRevision} = mdcProto; - const CbtApi = require('./cbt-api'); const Cli = require('./cli'); const CloudStorage = require('./cloud-storage'); @@ -29,7 +26,6 @@ const Logger = require('./logger'); const ReportBuilder = require('./report-builder'); const ReportWriter = require('./report-writer'); const SeleniumApi = require('./selenium-api'); -const {ExitCode} = require('./constants'); class Controller { constructor() { @@ -97,14 +93,15 @@ class Controller { } /** + * @param {!mdc.proto.DiffBase} goldenDiffBase * @return {!Promise} */ - async initForCapture() { + async initForCapture(goldenDiffBase) { const isOnline = this.cli_.isOnline(); if (isOnline) { await this.cbtApi_.killStalledSeleniumTests(); } - return this.reportBuilder_.initForCapture(); + return this.reportBuilder_.initForCapture(goldenDiffBase); } /** @@ -116,153 +113,62 @@ class Controller { /** * @param {!mdc.proto.ReportData} reportData - * @return {{isTestable: boolean, prNumber: ?number}} - */ - checkIsTestable(reportData) { - const goldenGitRevision = reportData.meta.golden_diff_base.git_revision; - const shouldSkipScreenshotTests = - goldenGitRevision && - goldenGitRevision.type === GitRevision.Type.TRAVIS_PR && - goldenGitRevision.pr_file_paths.length === 0; - - return { - isTestable: !shouldSkipScreenshotTests, - prNumber: goldenGitRevision ? goldenGitRevision.pr_number : null, - }; - } - - /** - * @param {!mdc.proto.ReportData} reportData - * @return {!Promise} */ async uploadAllAssets(reportData) { - this.logger_.foldStart('screenshot.upload', 'Controller#uploadAllAssets()'); + this.logger_.foldStart('screenshot.upload_assets', 'Controller#uploadAllAssets()'); await this.cloudStorage_.uploadAllAssets(reportData); - this.logger_.foldEnd('screenshot.upload'); - return reportData; + this.logger_.foldEnd('screenshot.upload_assets'); } /** * @param {!mdc.proto.ReportData} reportData - * @return {!Promise} */ async captureAllPages(reportData) { - this.logger_.foldStart('screenshot.capture', 'Controller#captureAllPages()'); - await this.seleniumApi_.captureAllPages(reportData); - await this.cloudStorage_.uploadAllScreenshots(reportData); - this.logger_.foldEnd('screenshot.capture'); - return reportData; - } + this.logger_.foldStart('screenshot.capture_images', 'Controller#captureAllPages()'); - /** - * @param {!mdc.proto.ReportData} reportData - * @return {!Promise} - */ - async compareAllScreenshots(reportData) { - this.logger_.foldStart('screenshot.compare', 'Controller#compareAllScreenshots()'); - - await this.reportBuilder_.populateScreenshotMaps(reportData.user_agents, reportData.screenshots); - await this.cloudStorage_.uploadAllDiffs(reportData); - - this.logComparisonResults_(reportData); + await this.seleniumApi_.captureAllPages(reportData); - // TODO(acdvorak): Where should this go? const meta = reportData.meta; meta.end_time_iso_utc = new Date().toISOString(); meta.duration_ms = Duration.elapsed(meta.start_time_iso_utc, meta.end_time_iso_utc).toMillis(); - this.logger_.foldEnd('screenshot.compare'); - - return reportData; + this.logger_.foldEnd('screenshot.capture_images'); } /** * @param {!mdc.proto.ReportData} reportData - * @return {!Promise} */ - async generateReportPage(reportData) { - this.logger_.foldStart('screenshot.report', 'Controller#generateReportPage()'); - - await this.reportWriter_.generateReportPage(reportData); - await this.cloudStorage_.uploadDiffReport(reportData); - - this.logger_.foldEnd('screenshot.report'); - this.logger_.log(''); - - // TODO(acdvorak): Store this directly in the proto so we don't have to recalculate it all over the place - const numChanges = - reportData.screenshots.changed_screenshot_list.length + - reportData.screenshots.added_screenshot_list.length + - reportData.screenshots.removed_screenshot_list.length; - - const boldRed = Logger.colors.bold.red; - const boldGreen = Logger.colors.bold.green; - - this.logger_.log('\n'); - if (numChanges > 0) { - this.logger_.error(boldRed(`${numChanges} screenshot${numChanges === 1 ? '' : 's'} changed!\n`)); - this.logger_.log('Diff report:', boldRed(reportData.meta.report_html_file.public_url)); - } else { - this.logger_.log(boldGreen('0 screenshots changed!\n')); - this.logger_.log('Diff report:', boldGreen(reportData.meta.report_html_file.public_url)); - } - - return reportData; + populateMaps(reportData) { + this.reportBuilder_.populateMaps(reportData.user_agents, reportData.screenshots); } /** * @param {!mdc.proto.ReportData} reportData - * @return {!Promise} */ - async getTestExitCode(reportData) { - // TODO(acdvorak): Store this directly in the proto so we don't have to recalculate it all over the place - const numChanges = - reportData.screenshots.changed_screenshot_list.length + - reportData.screenshots.added_screenshot_list.length + - reportData.screenshots.removed_screenshot_list.length; - - const isOnline = this.cli_.isOnline(); - if (isOnline && numChanges > 0) { - return ExitCode.CHANGES_FOUND; - } - return ExitCode.OK; + async uploadAllImages(reportData) { + this.logger_.foldStart('screenshot.upload_images', 'Controller#uploadAllImages()'); + await this.cloudStorage_.uploadAllScreenshots(reportData); + await this.cloudStorage_.uploadAllDiffs(reportData); + this.logger_.foldEnd('screenshot.upload_images'); } /** * @param {!mdc.proto.ReportData} reportData - * @return {!Promise} */ - async approveChanges(reportData) { - /** @type {!GoldenFile} */ - const newGoldenFile = await this.reportBuilder_.approveChanges(reportData); - await this.goldenIo_.writeToLocalFile(newGoldenFile); - return reportData; + async generateReportPage(reportData) { + this.logger_.foldStart('screenshot.generate_report', 'Controller#generateReportPage()'); + await this.reportWriter_.generateReportPage(reportData); + await this.cloudStorage_.uploadDiffReport(reportData); + this.logger_.foldEnd('screenshot.generate_report'); } /** * @param {!mdc.proto.ReportData} reportData - * @private */ - logComparisonResults_(reportData) { - console.log(''); - this.logComparisonResultSet_('Skipped', reportData.screenshots.skipped_screenshot_list); - this.logComparisonResultSet_('Unchanged', reportData.screenshots.unchanged_screenshot_list); - this.logComparisonResultSet_('Removed', reportData.screenshots.removed_screenshot_list); - this.logComparisonResultSet_('Added', reportData.screenshots.added_screenshot_list); - this.logComparisonResultSet_('Changed', reportData.screenshots.changed_screenshot_list); - } - - /** - * @param {string} title - * @param {!Array} screenshots - * @private - */ - logComparisonResultSet_(title, screenshots) { - console.log(`${title} ${screenshots.length} screenshot${screenshots.length === 1 ? '' : 's'}:`); - for (const screenshot of screenshots) { - console.log(` - ${screenshot.html_file_path} > ${screenshot.user_agent.alias}`); - } - console.log(''); + async approveChanges(reportData) { + /** @type {!GoldenFile} */ + const newGoldenFile = await this.reportBuilder_.approveChanges(reportData); + await this.goldenIo_.writeToLocalFile(newGoldenFile); } } diff --git a/test/screenshot/infra/lib/diff-base-parser.js b/test/screenshot/infra/lib/diff-base-parser.js index 446841bee58..be9d97b9f90 100644 --- a/test/screenshot/infra/lib/diff-base-parser.js +++ b/test/screenshot/infra/lib/diff-base-parser.js @@ -53,22 +53,39 @@ class DiffBaseParser { * @return {!Promise} */ async parseGoldenDiffBase() { - /** @type {?mdc.proto.GitRevision} */ - const travisGitRevision = await this.getTravisGitRevision(); - if (travisGitRevision) { - return DiffBase.create({ - type: DiffBase.Type.GIT_REVISION, - git_revision: travisGitRevision, - }); + return await this.getTravisDiffBase_() || await this.parseDiffBase(this.cli_.diffBase); + } + + /** + * @return {!Promise} + */ + async parseSnapshotDiffBase() { + return await this.getTravisDiffBase_() || await this.parseDiffBase('HEAD'); + } + + /** + * @return {!Promise} + */ + async parseMasterDiffBase() { + /** @type {!mdc.proto.DiffBase} */ + const goldenDiffBase = await this.parseGoldenDiffBase(); + const prNumber = goldenDiffBase.git_revision ? goldenDiffBase.git_revision.pr_number : null; + let baseBranch = 'origin/master'; + if (prNumber) { + if (process.env.TRAVIS_BRANCH) { + baseBranch = `origin/${process.env.TRAVIS_BRANCH}`; + } else { + baseBranch = await this.gitHubApi_.getPullRequestBaseBranch(prNumber); + } } - return this.parseDiffBase(); + return this.parseDiffBase(baseBranch); } /** * @param {string} rawDiffBase * @return {!Promise} */ - async parseDiffBase(rawDiffBase = this.cli_.diffBase) { + async parseDiffBase(rawDiffBase) { const isOnline = this.cli_.isOnline(); const isRealBranch = (branch) => Boolean(branch) && !['master', 'origin/master', 'HEAD'].includes(branch); @@ -91,7 +108,7 @@ class DiffBaseParser { * @return {!Promise} * @private */ - async parseDiffBase_(rawDiffBase = this.cli_.diffBase) { + async parseDiffBase_(rawDiffBase) { // Diff against a public `golden.json` URL. // E.g.: `--diff-base=https://storage.googleapis.com/.../golden.json` const isUrl = HTTP_URL_REGEX.test(rawDiffBase); @@ -145,6 +162,35 @@ class DiffBaseParser { return this.createLocalBranchDiffBase_(localRef, goldenFilePath); } + /** + * @return {!Promise} + * @private + */ + async getTravisDiffBase_() { + /** @type {?mdc.proto.GitRevision} */ + const travisGitRevision = await this.getTravisGitRevision(); + if (!travisGitRevision) { + return null; + } + + let generatedInputString; + if (travisGitRevision.pr_number) { + generatedInputString = `travis/pr/${travisGitRevision.pr_number}`; + } else if (travisGitRevision.tag) { + generatedInputString = `travis/tag/${travisGitRevision.tag}`; + } else if (travisGitRevision.branch) { + generatedInputString = `travis/branch/${travisGitRevision.branch}`; + } else { + generatedInputString = `travis/commit/${travisGitRevision.commit}`; + } + + return DiffBase.create({ + type: DiffBase.Type.GIT_REVISION, + input_string: generatedInputString, + git_revision: travisGitRevision, + }); + } + /** * @return {?Promise} */ @@ -165,6 +211,7 @@ class DiffBaseParser { author, branch: travisPrBranch || travisBranch, pr_number: travisPrNumber, + pr_file_paths: await this.getTestablePrFilePaths_(travisPrNumber), }); } @@ -195,6 +242,28 @@ class DiffBaseParser { return null; } + /** + * @param {number} prNumber + * @return {!Promise>} + * @private + */ + async getTestablePrFilePaths_(prNumber) { + /** @type {!Array} */ + const allPrFiles = await this.gitHubApi_.getPullRequestFiles(prNumber); + + return allPrFiles + .filter((prFile) => { + const isMarkdownFile = () => prFile.filename.endsWith('.md'); + const isDemosFile = () => prFile.filename.startsWith('demos/'); + const isDocsFile = () => prFile.filename.startsWith('docs/'); + const isUnitTestFile = () => prFile.filename.startsWith('test/unit/'); + const isIgnoredFile = isMarkdownFile() || isDemosFile() || isDocsFile() || isUnitTestFile(); + return !isIgnoredFile; + }) + .map((prFile) => prFile.filename) + ; + } + /** * @param {string} publicUrl * @return {!mdc.proto.DiffBase} @@ -233,9 +302,9 @@ class DiffBaseParser { return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, + input_string: `${commit}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format git_revision: GitRevision.create({ type: GitRevision.Type.COMMIT, - input_string: `${commit}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, author, @@ -258,9 +327,9 @@ class DiffBaseParser { return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, + input_string: `${remoteRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format git_revision: GitRevision.create({ type: GitRevision.Type.REMOTE_BRANCH, - input_string: `${remoteRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, author, @@ -282,9 +351,9 @@ class DiffBaseParser { return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, + input_string: `${tagRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format git_revision: GitRevision.create({ type: GitRevision.Type.REMOTE_TAG, - input_string: `${tagRef}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, author, @@ -306,9 +375,9 @@ class DiffBaseParser { return DiffBase.create({ type: DiffBase.Type.GIT_REVISION, + input_string: `${branch}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format git_revision: GitRevision.create({ type: GitRevision.Type.LOCAL_BRANCH, - input_string: `${branch}:${goldenJsonFilePath}`, // TODO(acdvorak): Document the ':' separator format golden_json_file_path: goldenJsonFilePath, commit, author, diff --git a/test/screenshot/infra/lib/git-repo.js b/test/screenshot/infra/lib/git-repo.js index bc5f1127ef4..da8a26d94d9 100644 --- a/test/screenshot/infra/lib/git-repo.js +++ b/test/screenshot/infra/lib/git-repo.js @@ -16,6 +16,7 @@ 'use strict'; +const VError = require('verror'); const simpleGit = require('simple-git/promise'); const mdcProto = require('../proto/mdc.pb').mdc.proto; @@ -65,7 +66,12 @@ class GitRepo { await this.exec_('raw', ['config', '--add', 'remote.origin.fetch', prFetchRef]); } - await this.repo_.fetch(['--tags', ...args]); + try { + await this.repo_.fetch(['--tags', ...args]); + } catch (err) { + const serialized = JSON.stringify(args); + throw new VError(err, `Failed to run GitRepo.fetch(${serialized})`); + } } /** @@ -106,21 +112,34 @@ class GitRepo { * @return {!Promise} */ async getFileAtRevision(filePath, revision = 'master') { - return this.repo_.show([`${revision}:${filePath}`]); + try { + return this.repo_.show([`${revision}:${filePath}`]); + } catch (err) { + const serialized = JSON.stringify(args); + throw new VError(err, `Failed to run GitRepo.getFileAtRevision(${serialized})`); + } } /** * @return {!Promise>} */ async getRemoteNames() { - return (await this.repo_.getRemotes()).map((remote) => remote.name); + try { + return (await this.repo_.getRemotes()).map((remote) => remote.name); + } catch (err) { + throw new VError(err, 'Failed to run GitRepo.getRemoteNames()'); + } } /** * @return {!Promise} */ async getStatus() { - return this.repo_.status(); + try { + return this.repo_.status(); + } catch (err) { + throw new VError(err, 'Failed to run GitRepo.getStatus()'); + } } /** @@ -128,8 +147,13 @@ class GitRepo { * @return {!Promise>} */ async getLog(args = []) { - const logEntries = await this.repo_.log([...args]); - return logEntries.all.concat(); // convert TypeScript ReadonlyArray to mutable Array + try { + const logEntries = await this.repo_.log([...args]); + return logEntries.all.concat(); // convert TypeScript ReadonlyArray to mutable Array + } catch (err) { + const serialized = JSON.stringify(args); + throw new VError(err, `Failed to run GitRepo.getLog(${serialized})`); + } } /** @@ -137,7 +161,11 @@ class GitRepo { * @return {!Promise>} */ async getIgnoredPaths(filePaths) { - return this.repo_.checkIgnore(filePaths); + try { + return this.repo_.checkIgnore(filePaths); + } catch (err) { + throw new VError(err, `Failed to run GitRepo.getIgnoredPaths(${filePaths.length} file paths)`); + } } /** @@ -161,7 +189,12 @@ class GitRepo { * @private */ async exec_(cmd, argList = []) { - return (await this.repo_[cmd](argList) || '').trim(); + try { + return (await this.repo_[cmd](argList) || '').trim(); + } catch (err) { + const serialized = JSON.stringify([cmd, ...argList]); + throw new VError(err, `Failed to run GitRepo.exec_(${serialized})`); + } } } diff --git a/test/screenshot/infra/lib/github-api.js b/test/screenshot/infra/lib/github-api.js index 324175516e3..a12397f8e9d 100644 --- a/test/screenshot/infra/lib/github-api.js +++ b/test/screenshot/infra/lib/github-api.js @@ -14,6 +14,7 @@ * limitations under the License. */ +const debounce = require('debounce'); const octokit = require('@octokit/rest'); const GitRepo = require('./git-repo'); @@ -55,9 +56,16 @@ class GitHubApi { }; }; - this.createStatusThrottled_ = throttle((...args) => { + const createStatusDebounced = debounce((...args) => { + return this.createStatusUnthrottled_(...args); + }, 2500); + const createStatusThrottled = throttle((...args) => { return this.createStatusUnthrottled_(...args); }, 5000); + this.createStatusThrottled_ = (...args) => { + createStatusDebounced(...args); + createStatusThrottled(...args); + }; } /** @@ -76,14 +84,13 @@ class GitHubApi { /** * @param {string} state * @param {string} description - * @return {!Promise<*>} */ - async setPullRequestStatusManual({state, description}) { + setPullRequestStatusManual({state, description}) { if (process.env.TRAVIS !== 'true') { return; } - return await this.createStatusThrottled_({ + this.createStatusThrottled_({ state, targetUrl: `https://travis-ci.org/material-components/material-components-web/jobs/${process.env.TRAVIS_JOB_ID}`, description, @@ -199,6 +206,37 @@ class GitHubApi { }); return fileResponse.data; } + + /** + * @param {number} prNumber + * @return {!Promise} + */ + async getPullRequestBaseBranch(prNumber) { + const prResponse = await this.octokit_.pullRequests.get({ + owner: 'material-components', + repo: 'material-components-web', + number: prNumber, + }); + if (!prResponse.data) { + const serialized = JSON.stringify(prResponse, null, 2); + throw new Error(`Unable to fetch data for GitHub PR #${prNumber}:\n${serialized}`); + } + return `origin/${prResponse.data.base.ref}`; + } + + /** + * @param {number} prNumber + * @param {string} comment + * @return {!Promise<*>} + */ + async createPullRequestComment({prNumber, comment}) { + return this.octokit_.issues.createComment({ + owner: 'material-components', + repo: 'material-components-web', + number: prNumber, + body: comment, + }); + } } module.exports = GitHubApi; diff --git a/test/screenshot/infra/lib/golden-io.js b/test/screenshot/infra/lib/golden-io.js index 35cf6b81244..d63b847ddad 100644 --- a/test/screenshot/infra/lib/golden-io.js +++ b/test/screenshot/infra/lib/golden-io.js @@ -69,29 +69,27 @@ class GoldenIo { /** * Parses the `golden.json` file specified by the `--diff-base` CLI arg. - * @param {string=} rawDiffBase + * @param {!mdc.proto.DiffBase} goldenDiffBase * @return {!Promise} */ - async readFromDiffBase(rawDiffBase = this.cli_.diffBase) { - if (!this.cachedGoldenJsonMap_[rawDiffBase]) { - const goldenJson = JSON.parse(await this.readFromDiffBase_(rawDiffBase)); - this.cachedGoldenJsonMap_[rawDiffBase] = new GoldenFile(goldenJson); + async readFromDiffBase(goldenDiffBase) { + const key = goldenDiffBase.input_string; + if (!this.cachedGoldenJsonMap_[key]) { + const goldenJson = JSON.parse(await this.readFromDiffBase_(goldenDiffBase)); + this.cachedGoldenJsonMap_[key] = new GoldenFile(goldenJson); } // Deep copy to avoid mutating shared state - return new GoldenFile(this.cachedGoldenJsonMap_[rawDiffBase].toJSON()); + return new GoldenFile(this.cachedGoldenJsonMap_[key].toJSON()); } /** - * @param {string} rawDiffBase + * @param {!mdc.proto.DiffBase} goldenDiffBase * @return {!Promise} * @private */ - async readFromDiffBase_(rawDiffBase) { - /** @type {!mdc.proto.DiffBase} */ - const parsedDiffBase = await this.diffBaseParser_.parseDiffBase(rawDiffBase); - - const publicUrl = parsedDiffBase.public_url; + async readFromDiffBase_(goldenDiffBase) { + const publicUrl = goldenDiffBase.public_url; if (publicUrl) { return request({ method: 'GET', @@ -99,19 +97,22 @@ class GoldenIo { }); } - const localFilePath = parsedDiffBase.local_file_path; + const localFilePath = goldenDiffBase.local_file_path; if (localFilePath) { return this.localStorage_.readTextFile(localFilePath); } - const rev = parsedDiffBase.git_revision; + const rev = goldenDiffBase.git_revision; if (rev) { return this.gitRepo_.getFileAtRevision(rev.golden_json_file_path, rev.commit); } - const serialized = JSON.stringify({parsedDiffBase, meta}, null, 2); + const serialized = JSON.stringify({goldenDiffBase}, null, 2); throw new Error( - `Unable to parse '--diff-base=${rawDiffBase}': Expected a URL, local file path, or git ref.\n${serialized}` + ` +Unable to parse '--diff-base=${goldenDiffBase.input_string}': Expected a URL, local file path, or git ref. +${serialized} +`.trim() ); } diff --git a/test/screenshot/infra/lib/local-storage.js b/test/screenshot/infra/lib/local-storage.js index 821d5c8e2e9..1f0e12ca6bf 100644 --- a/test/screenshot/infra/lib/local-storage.js +++ b/test/screenshot/infra/lib/local-storage.js @@ -16,6 +16,7 @@ 'use strict'; +const VError = require('verror'); const del = require('del'); const fs = require('mz/fs'); const fsx = require('fs-extra'); @@ -63,7 +64,11 @@ class LocalStorage { */ async getTestPageDestinationPaths(reportMeta) { const cwd = reportMeta.local_asset_base_dir; - return glob.sync('**/spec/mdc-*/**/*.html', {cwd, nodir: true, ignore: ['**/index.html']}); + try { + return glob.sync('**/spec/mdc-*/**/*.html', {cwd, nodir: true, ignore: ['**/index.html']}); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.getTestPageDestinationPaths(${cwd})`); + } } /** @@ -72,8 +77,13 @@ class LocalStorage { * @return {!Promise} */ async writeTextFile(filePath, fileContent) { - mkdirp.sync(path.dirname(filePath)); - await fs.writeFile(filePath, fileContent, {encoding: 'utf8'}); + try { + mkdirp.sync(path.dirname(filePath)); + await fs.writeFile(filePath, fileContent, {encoding: 'utf8'}); + } catch (err) { + const serialized = JSON.stringify({filePath, fileContent: fileContent.length + ' bytes'}); + throw new VError(err, `Failed to run LocalStorage.writeTextFile(${serialized})`); + } } /** @@ -83,8 +93,13 @@ class LocalStorage { * @return {!Promise} */ async writeBinaryFile(filePath, fileContent, encoding = null) { - mkdirp.sync(path.dirname(filePath)); - await fs.writeFile(filePath, fileContent, {encoding}); + try { + mkdirp.sync(path.dirname(filePath)); + await fs.writeFile(filePath, fileContent, {encoding}); + } catch (err) { + const serialized = JSON.stringify({filePath, fileContent: fileContent.length + ' bytes', encoding}); + throw new VError(err, `Failed to run LocalStorage.writeBinaryFile(${serialized})`); + } } /** @@ -92,7 +107,11 @@ class LocalStorage { * @return {!Promise} */ async readTextFile(filePath) { - return fs.readFile(filePath, {encoding: 'utf8'}); + try { + return fs.readFile(filePath, {encoding: 'utf8'}); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.readTextFile(${filePath})`); + } } /** @@ -100,7 +119,11 @@ class LocalStorage { * @return {!Promise} */ readTextFileSync(filePath) { - return fs.readFileSync(filePath, {encoding: 'utf8'}); + try { + return fs.readFileSync(filePath, {encoding: 'utf8'}); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.readTextFileSync(${filePath})`); + } } /** @@ -109,7 +132,11 @@ class LocalStorage { * @return {!Promise} */ async readBinaryFile(filePath, encoding = null) { - return fs.readFile(filePath, {encoding}); + try { + return fs.readFile(filePath, {encoding}); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.readBinaryFile(${filePath}, ${encoding})`); + } } /** @@ -142,7 +169,11 @@ class LocalStorage { * @return {!Promise} */ async copy(src, dest) { - return fsx.copy(src, dest); + try { + return fsx.copy(src, dest); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.copy(${src}, ${dest})`); + } } /** @@ -150,7 +181,11 @@ class LocalStorage { * @return {!Promise<*>} */ async delete(pathPatterns) { - return del(pathPatterns); + try { + return del(pathPatterns); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.delete(${pathPatterns} patterns)`); + } } /** @@ -158,7 +193,11 @@ class LocalStorage { * @return {!Promise} */ async exists(filePath) { - return fs.exists(filePath); + try { + return fs.exists(filePath); + } catch (err) { + throw new VError(err, `Failed to run LocalStorage.exists(${filePath})`); + } } /** diff --git a/test/screenshot/infra/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js index 4c40801269d..89fdd832856 100644 --- a/test/screenshot/infra/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -26,7 +26,7 @@ const path = require('path'); const serveIndex = require('serve-index'); const mdcProto = require('../proto/mdc.pb').mdc.proto; -const {Approvals, DiffImageResult, Dimensions, GitRevision, GitStatus, GoldenScreenshot, LibraryVersion} = mdcProto; +const {Approvals, DiffImageResult, Dimensions, GitStatus, GoldenScreenshot, LibraryVersion} = mdcProto; const {ReportData, ReportMeta, Screenshot, Screenshots, ScreenshotList, TestFile, User, UserAgents} = mdcProto; const {InclusionType, CaptureState} = Screenshot; @@ -118,15 +118,16 @@ class ReportBuilder { /** @type {!mdc.proto.ReportData} */ const reportData = ReportData.fromObject(require(runReportJsonFile.absolute_path)); reportData.approvals = Approvals.create(); - this.populateScreenshotMaps(reportData.user_agents, reportData.screenshots); + this.populateMaps(reportData.user_agents, reportData.screenshots); this.populateApprovals_(reportData); return reportData; } /** + * @param {!mdc.proto.DiffBase} goldenDiffBase * @return {!Promise} */ - async initForCapture() { + async initForCapture(goldenDiffBase) { this.logger_.foldStart('screenshot.init', 'ReportBuilder#initForCapture()'); if (this.cli_.isOnline()) { @@ -134,7 +135,7 @@ class ReportBuilder { } /** @type {!mdc.proto.ReportMeta} */ - const reportMeta = await this.createReportMetaProto_(); + const reportMeta = await this.createReportMetaProto_(goldenDiffBase); /** @type {!mdc.proto.UserAgents} */ const userAgents = await this.createUserAgentsProto_(); @@ -145,7 +146,7 @@ class ReportBuilder { await this.startTemporaryHttpServer_(reportMeta); } - const screenshots = await this.createScreenshotsProto_({reportMeta, userAgents}); + const screenshots = await this.createScreenshotsProto_({reportMeta, userAgents, goldenDiffBase}); const reportData = ReportData.create({ meta: reportMeta, @@ -175,7 +176,7 @@ class ReportBuilder { * @param {!mdc.proto.UserAgents} userAgents * @param {!mdc.proto.Screenshots} screenshots */ - populateScreenshotMaps(userAgents, screenshots) { + populateMaps(userAgents, screenshots) { // TODO(acdvorak): Figure out why the report page is randomly sorted. E.g.: // https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/04_49_09_427/report/report.html // https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/12/04_48_52_974/report/report.html @@ -380,10 +381,11 @@ class ReportBuilder { } /** + * @param {!mdc.proto.DiffBase} goldenDiffBase * @return {!Promise} * @private */ - async createReportMetaProto_() { + async createReportMetaProto_(goldenDiffBase) { const isOnline = this.cli_.isOnline(); // We only need to start up a local web server if the user is running in offline mode. @@ -415,30 +417,7 @@ class ReportBuilder { const gitStatus = GitStatus.fromObject(await this.gitRepo_.getStatus()); /** @type {!mdc.proto.DiffBase} */ - const goldenDiffBase = await this.diffBaseParser_.parseGoldenDiffBase(); - - /** @type {!mdc.proto.DiffBase} */ - const snapshotDiffBase = await this.diffBaseParser_.parseDiffBase('HEAD'); - - /** @type {!mdc.proto.GitRevision} */ - const goldenGitRevision = goldenDiffBase.git_revision; - - if (goldenGitRevision && goldenGitRevision.type === GitRevision.Type.TRAVIS_PR) { - /** @type {!Array} */ - const allPrFiles = await this.gitHubApi_.getPullRequestFiles(goldenGitRevision.pr_number); - - goldenGitRevision.pr_file_paths = allPrFiles - .filter((prFile) => { - const isMarkdownFile = () => prFile.filename.endsWith('.md'); - const isDemosFile = () => prFile.filename.startsWith('demos/'); - const isDocsFile = () => prFile.filename.startsWith('docs/'); - const isUnitTestFile = () => prFile.filename.startsWith('test/unit/'); - const isIgnoredFile = isMarkdownFile() || isDemosFile() || isDocsFile() || isUnitTestFile(); - return !isIgnoredFile; - }) - .map((prFile) => prFile.filename) - ; - } + const snapshotDiffBase = await this.diffBaseParser_.parseSnapshotDiffBase(); return ReportMeta.create({ start_time_iso_utc: new Date().toISOString(), @@ -539,7 +518,19 @@ class ReportBuilder { * @return {!Promise} */ async getCommitDistance_(mdcVersion) { - return (await this.gitRepo_.getLog([`v${mdcVersion}..HEAD`])).length; + try { + return (await this.gitRepo_.getLog([`v${mdcVersion}..HEAD`])).length; + } catch (err) { + // To save time, Travis CI only clones a certain number of commits. + // Unfortunately, if the user's PR branch has a lot of commits, `git log` will fail with an error like this: + // + // fatal: ambiguous argument 'v0.37.1..HEAD': unknown revision or path not in the working tree. + // Use '--' to separate paths from revisions, like this: + // 'git [...] -- [...]' + // + // Commit distance isn't critical information, so just return 0. + return 0; + } } /** @@ -559,12 +550,13 @@ class ReportBuilder { /** * @param {!mdc.proto.ReportMeta} reportMeta * @param {!mdc.proto.UserAgents} allUserAgents + * @param {!mdc.proto.DiffBase} goldenDiffBase * @return {!Promise} * @private */ - async createScreenshotsProto_({reportMeta, userAgents}) { + async createScreenshotsProto_({reportMeta, userAgents, goldenDiffBase}) { /** @type {!GoldenFile} */ - const goldenFile = await this.goldenIo_.readFromDiffBase(); + const goldenFile = await this.goldenIo_.readFromDiffBase(goldenDiffBase); /** @type {!Array} */ const expectedScreenshots = await this.getExpectedScreenshots_(goldenFile); @@ -596,7 +588,7 @@ class ReportBuilder { this.setAllStates_(screenshots.removed_screenshot_list, InclusionType.REMOVE, CaptureState.SKIPPED); this.setAllStates_(screenshots.comparable_screenshot_list, InclusionType.COMPARE, CaptureState.QUEUED); - this.populateScreenshotMaps(userAgents, screenshots); + this.populateMaps(userAgents, screenshots); return screenshots; } diff --git a/test/screenshot/infra/lib/report-writer.js b/test/screenshot/infra/lib/report-writer.js index 4ace08732bd..8c7d3e6e6e1 100644 --- a/test/screenshot/infra/lib/report-writer.js +++ b/test/screenshot/infra/lib/report-writer.js @@ -417,8 +417,6 @@ class ReportWriter { return new Handlebars.SafeString(`${diffBase.public_url}`); } - const rev = diffBase.git_revision; - if (diffBase.local_file_path) { const localFilePathMarkup = diffBase.is_default_local_file ? `${diffBase.local_file_path}` @@ -427,6 +425,7 @@ class ReportWriter { return new Handlebars.SafeString(`${localFilePathMarkup} (local file)`); } + const rev = diffBase.git_revision; if (rev) { const prMarkup = rev.pr_number ? `(PR #${rev.pr_number})` diff --git a/test/screenshot/infra/lib/selenium-api.js b/test/screenshot/infra/lib/selenium-api.js index 6ca034f508d..9608ba94b29 100644 --- a/test/screenshot/infra/lib/selenium-api.js +++ b/test/screenshot/infra/lib/selenium-api.js @@ -724,8 +724,10 @@ class SeleniumApi { await this.cbtApi_.killSeleniumTests(ids, /* silent */ wasAlreadyKilled); - // Give the HTTP requests a chance to complete before exiting - await this.sleep_(Duration.seconds(4).toMillis()); + if (!wasAlreadyKilled && ids.length > 0) { + console.log('\nWaiting for CBT cancellation requests to complete...'); + await this.sleep_(Duration.seconds(4).toMillis()); + } } /** diff --git a/test/screenshot/report/report.js b/test/screenshot/report/report.js index 20c6c3ccc00..81d6d977219 100644 --- a/test/screenshot/report/report.js +++ b/test/screenshot/report/report.js @@ -127,14 +127,16 @@ window.mdc.reportUi = (() => { return; } const htmlFileDetailsElems = Array.from(document.querySelectorAll('details.report-html-file')); - htmlFileDetailsElems.forEach((htmlFileDetailsElem) => { + for (const htmlFileDetailsElem of htmlFileDetailsElems) { htmlFileDetailsElem.open = htmlFileDetailsElem.contains(deepLinkElem); if (htmlFileDetailsElem.open) { htmlFileDetailsElem.querySelectorAll('details.report-user-agent').forEach((userAgentDetailsElem) => { userAgentDetailsElem.open = userAgentDetailsElem.contains(deepLinkElem); }); + htmlFileDetailsElem.parentElement.closest('details').open = true; + break; } - }); + } } collapseAll() { diff --git a/test/screenshot/run.js b/test/screenshot/run.js index d6e4d6a520a..300a135526f 100644 --- a/test/screenshot/run.js +++ b/test/screenshot/run.js @@ -16,68 +16,62 @@ 'use strict'; -const colors = require('colors'); const Cli = require('./infra/lib/cli'); +const CliColor = require('./infra/lib/logger').colors; const Duration = require('./infra/lib/duration'); const {ExitCode} = require('./infra/lib/constants'); -const COMMAND_MAP = { - async approve() { - return require('./infra/commands/approve').runAsync(); +const COMMANDS = { + get approve() { + return require('./infra/commands/approve'); }, - - async build() { - return require('./infra/commands/build').runAsync(); + get build() { + return require('./infra/commands/build'); }, - - async clean() { - return require('./infra/commands/clean').runAsync(); + get clean() { + return require('./infra/commands/clean'); }, - - async demo() { - return require('./infra/commands/demo').runAsync(); + get demo() { + return require('./infra/commands/demo'); }, - - async index() { - return require('./infra/commands/index').runAsync(); + get index() { + return require('./infra/commands/index'); }, - - async proto() { - return require('./infra/commands/proto').runAsync(); + get proto() { + return require('./infra/commands/proto'); }, - - async serve() { - return require('./infra/commands/serve').runAsync(); + get serve() { + return require('./infra/commands/serve'); }, - - async test() { - return require('./infra/commands/test').runAsync(); + get test() { + return require('./infra/commands/test'); }, }; async function runAsync() { const cli = new Cli(); - const cmd = COMMAND_MAP[cli.command]; + const CmdClass = COMMANDS[cli.command]; - if (!cmd) { + if (!CmdClass) { console.error(`Error: Unknown command: '${cli.command}'`); process.exit(ExitCode.UNSUPPORTED_CLI_COMMAND); return; } + const cmd = new CmdClass(); const isOnline = await cli.checkIsOnline(); if (!isOnline) { console.log('Offline mode!'); } - cmd().then( + cmd.runAsync().then( (exitCode = ExitCode.OK) => { if (exitCode !== ExitCode.OK) { process.exit(exitCode); } }, (err) => { - console.error('\n\n' + colors.bold.red('ERROR:'), err); + console.error('\n\n' + CliColor.bold.red('ERROR:'), err); process.exit(ExitCode.UNKNOWN_ERROR); } ); From 36e27557bb3ed444c2eba1e1d7fd1095e33c5887 Mon Sep 17 00:00:00 2001 From: Bonnie Zhou Date: Mon, 23 Jul 2018 18:10:54 -0700 Subject: [PATCH 45/53] refactor(chips): Register handlers in component instead of foundation (#3146) BREAKING CHANGE: `MDCChip`/`MDCChipSet` registerEventHandler adapter methods were removed, and corresponding handlers were made public in `MDCChipFoundation`/`MDCChipSetFoundation`. --- packages/mdc-chips/README.md | 8 +- packages/mdc-chips/chip-set/adapter.js | 14 -- packages/mdc-chips/chip-set/foundation.js | 25 +--- packages/mdc-chips/chip-set/index.js | 35 +++-- packages/mdc-chips/chip/adapter.js | 28 ---- packages/mdc-chips/chip/foundation.js | 30 ----- packages/mdc-chips/chip/index.js | 55 ++++++-- .../mdc-chips/mdc-chip-set.foundation.test.js | 80 ++--------- test/unit/mdc-chips/mdc-chip-set.test.js | 20 --- .../mdc-chips/mdc-chip.foundation.test.js | 127 ++++++------------ test/unit/mdc-chips/mdc-chip.test.js | 51 ------- 11 files changed, 123 insertions(+), 350 deletions(-) diff --git a/packages/mdc-chips/README.md b/packages/mdc-chips/README.md index 3f4a8cf0a6b..082aa387e63 100644 --- a/packages/mdc-chips/README.md +++ b/packages/mdc-chips/README.md @@ -248,10 +248,6 @@ Method Signature | Description `addClassToLeadingIcon(className: string) => void` | Adds a class to the leading icon element `removeClassFromLeadingIcon(className: string) => void` | Removes a class from the leading icon element `eventTargetHasClass(target: EventTarget, className: string) => boolean` | Returns true if target has className, false otherwise -`registerEventHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the root element -`deregisterEventHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the root element -`registerTrailingIconInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the trailing icon element -`deregisterTrailingIconInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the trailing icon element `notifyInteraction() => void` | Emits a custom event `MDCChip:interaction` denoting the chip has been interacted with `notifyTrailingIconInteraction() => void` | Emits a custom event `MDCChip:trailingIconInteraction` denoting the chip's trailing icon has been interacted with `notifyRemoval() => void` | Emits a custom event `MDCChip:removal` denoting the chip will be removed @@ -267,8 +263,6 @@ Method Signature | Description Method Signature | Description --- | --- `hasClass(className: string) => boolean` | Returns whether the chip set element has the given class -`registerInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event handler on the root element for a given event -`deregisterInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event handler on the root element for a given event `removeChip(chip: MDCChip) => void` | Removes the chip object from the chip set ### Foundations: `MDCChipFoundation` and `MDCChipSetFoundation` @@ -292,3 +286,5 @@ Method Signature | Description --- | --- `select(chipFoundation: MDCChipFoundation) => void` | Selects the given chip `deselect(chipFoundation: MDCChipFoundation) => void` | Deselects the given chip +`handleChipInteraction(evt: Event) => void` | Handles a custom `MDCChip:interaction` event on the root element +`handleChipRemoval(evt: Event) => void` | Handles a custom `MDCChip:removal` event on the root element diff --git a/packages/mdc-chips/chip-set/adapter.js b/packages/mdc-chips/chip-set/adapter.js index fe7ac7238d6..6c4f2e11f02 100644 --- a/packages/mdc-chips/chip-set/adapter.js +++ b/packages/mdc-chips/chip-set/adapter.js @@ -38,20 +38,6 @@ class MDCChipSetAdapter { */ hasClass(className) {} - /** - * Registers an event handler on the root element for a given event. - * @param {string} evtType - * @param {function(!MDCChipInteractionEventType): undefined} handler - */ - registerInteractionHandler(evtType, handler) {} - - /** - * Deregisters an event handler on the root element for a given event. - * @param {string} evtType - * @param {function(!MDCChipInteractionEventType): undefined} handler - */ - deregisterInteractionHandler(evtType, handler) {} - /** * Removes the chip object from the chip set. * @param {!Object} chip diff --git a/packages/mdc-chips/chip-set/foundation.js b/packages/mdc-chips/chip-set/foundation.js index b3f745dff87..c0955bf8ae1 100644 --- a/packages/mdc-chips/chip-set/foundation.js +++ b/packages/mdc-chips/chip-set/foundation.js @@ -44,8 +44,6 @@ class MDCChipSetFoundation extends MDCFoundation { static get defaultAdapter() { return /** @type {!MDCChipSetAdapter} */ ({ hasClass: () => {}, - registerInteractionHandler: () => {}, - deregisterInteractionHandler: () => {}, removeChip: () => {}, }); } @@ -61,25 +59,6 @@ class MDCChipSetFoundation extends MDCFoundation { * @private {!Array} */ this.selectedChips_ = []; - - /** @private {function(!MDCChipInteractionEventType): undefined} */ - this.chipInteractionHandler_ = (evt) => this.handleChipInteraction_(evt); - /** @private {function(!MDCChipInteractionEventType): undefined} */ - this.chipRemovalHandler_ = (evt) => this.handleChipRemoval_(evt); - } - - init() { - this.adapter_.registerInteractionHandler( - MDCChipFoundation.strings.INTERACTION_EVENT, this.chipInteractionHandler_); - this.adapter_.registerInteractionHandler( - MDCChipFoundation.strings.REMOVAL_EVENT, this.chipRemovalHandler_); - } - - destroy() { - this.adapter_.deregisterInteractionHandler( - MDCChipFoundation.strings.INTERACTION_EVENT, this.chipInteractionHandler_); - this.adapter_.deregisterInteractionHandler( - MDCChipFoundation.strings.REMOVAL_EVENT, this.chipRemovalHandler_); } /** @@ -119,7 +98,7 @@ class MDCChipSetFoundation extends MDCFoundation { * @param {!MDCChipInteractionEventType} evt * @private */ - handleChipInteraction_(evt) { + handleChipInteraction(evt) { const chipFoundation = evt.detail.chip.foundation; if (this.adapter_.hasClass(cssClasses.CHOICE) || this.adapter_.hasClass(cssClasses.FILTER)) { if (chipFoundation.isSelected()) { @@ -135,7 +114,7 @@ class MDCChipSetFoundation extends MDCFoundation { * @param {!MDCChipInteractionEventType} evt * @private */ - handleChipRemoval_(evt) { + handleChipRemoval(evt) { const {chip} = evt.detail; this.deselect(chip.foundation); this.adapter_.removeChip(chip); diff --git a/packages/mdc-chips/chip-set/index.js b/packages/mdc-chips/chip-set/index.js index eb74ab4b169..78f9eca4e4d 100644 --- a/packages/mdc-chips/chip-set/index.js +++ b/packages/mdc-chips/chip-set/index.js @@ -19,7 +19,7 @@ import MDCComponent from '@material/base/component'; import MDCChipSetAdapter from './adapter'; import MDCChipSetFoundation from './foundation'; -import {MDCChip} from '../chip/index'; +import {MDCChip, MDCChipFoundation} from '../chip/index'; /** * @extends {MDCComponent} @@ -36,6 +36,11 @@ class MDCChipSet extends MDCComponent { this.chips; /** @type {(function(!Element): !MDCChip)} */ this.chipFactory_; + + /** @private {?function(?Event): undefined} */ + this.handleChipInteraction_; + /** @private {?function(?Event): undefined} */ + this.handleChipRemoval_; } /** @@ -55,18 +60,32 @@ class MDCChipSet extends MDCComponent { this.chips = this.instantiateChips_(this.chipFactory_); } - destroy() { - this.chips.forEach((chip) => { - chip.destroy(); - }); - } - initialSyncWithDOM() { this.chips.forEach((chip) => { if (chip.isSelected()) { this.foundation_.select(chip.foundation); } }); + + this.handleChipInteraction_ = (evt) => this.foundation_.handleChipInteraction(evt); + this.handleChipRemoval_ = (evt) => this.foundation_.handleChipRemoval(evt); + this.root_.addEventListener( + MDCChipFoundation.strings.INTERACTION_EVENT, this.handleChipInteraction_); + this.root_.addEventListener( + MDCChipFoundation.strings.REMOVAL_EVENT, this.handleChipRemoval_); + } + + destroy() { + this.chips.forEach((chip) => { + chip.destroy(); + }); + + this.root_.removeEventListener( + MDCChipFoundation.strings.INTERACTION_EVENT, this.handleChipInteraction_); + this.root_.removeEventListener( + MDCChipFoundation.strings.REMOVAL_EVENT, this.handleChipRemoval_); + + super.destroy(); } /** @@ -83,8 +102,6 @@ class MDCChipSet extends MDCComponent { getDefaultFoundation() { return new MDCChipSetFoundation(/** @type {!MDCChipSetAdapter} */ (Object.assign({ hasClass: (className) => this.root_.classList.contains(className), - registerInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), removeChip: (chip) => { const index = this.chips.indexOf(chip); this.chips.splice(index, 1); diff --git a/packages/mdc-chips/chip/adapter.js b/packages/mdc-chips/chip/adapter.js index d78d2b21e6a..997db72eff3 100644 --- a/packages/mdc-chips/chip/adapter.js +++ b/packages/mdc-chips/chip/adapter.js @@ -67,34 +67,6 @@ class MDCChipAdapter { */ eventTargetHasClass(target, className) {} - /** - * Registers an event listener on the root element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler - */ - registerEventHandler(evtType, handler) {} - - /** - * Deregisters an event listener on the root element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler - */ - deregisterEventHandler(evtType, handler) {} - - /** - * Registers an event listener on the trailing icon element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler - */ - registerTrailingIconInteractionHandler(evtType, handler) {} - - /** - * Deregisters an event listener on the trailing icon element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler - */ - deregisterTrailingIconInteractionHandler(evtType, handler) {} - /** * Emits a custom "MDCChip:interaction" event denoting the chip has been * interacted with (typically on click or keydown). diff --git a/packages/mdc-chips/chip/foundation.js b/packages/mdc-chips/chip/foundation.js index 4f5823a6f20..4535db25509 100644 --- a/packages/mdc-chips/chip/foundation.js +++ b/packages/mdc-chips/chip/foundation.js @@ -48,10 +48,6 @@ class MDCChipFoundation extends MDCFoundation { addClassToLeadingIcon: () => {}, removeClassFromLeadingIcon: () => {}, eventTargetHasClass: () => {}, - registerEventHandler: () => {}, - deregisterEventHandler: () => {}, - registerTrailingIconInteractionHandler: () => {}, - deregisterTrailingIconInteractionHandler: () => {}, notifyInteraction: () => {}, notifyTrailingIconInteraction: () => {}, notifyRemoval: () => {}, @@ -71,32 +67,6 @@ class MDCChipFoundation extends MDCFoundation { * @private {boolean} * */ this.shouldRemoveOnTrailingIconClick_ = true; - /** @private {function(!Event): undefined} */ - this.interactionHandler_ = (evt) => this.handleInteraction(evt); - /** @private {function(!Event): undefined} */ - this.transitionEndHandler_ = (evt) => this.handleTransitionEnd(evt); - /** @private {function(!Event): undefined} */ - this.trailingIconInteractionHandler_ = (evt) => this.handleTrailingIconInteraction(evt); - } - - init() { - ['click', 'keydown'].forEach((evtType) => { - this.adapter_.registerEventHandler(evtType, this.interactionHandler_); - }); - this.adapter_.registerEventHandler('transitionend', this.transitionEndHandler_); - ['click', 'keydown', 'touchstart', 'pointerdown', 'mousedown'].forEach((evtType) => { - this.adapter_.registerTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_); - }); - } - - destroy() { - ['click', 'keydown'].forEach((evtType) => { - this.adapter_.deregisterEventHandler(evtType, this.interactionHandler_); - }); - this.adapter_.deregisterEventHandler('transitionend', this.transitionEndHandler_); - ['click', 'keydown', 'touchstart', 'pointerdown', 'mousedown'].forEach((evtType) => { - this.adapter_.deregisterTrailingIconInteractionHandler(evtType, this.trailingIconInteractionHandler_); - }); } /** diff --git a/packages/mdc-chips/chip/index.js b/packages/mdc-chips/chip/index.js index 9ba742af05f..78de50ab80f 100644 --- a/packages/mdc-chips/chip/index.js +++ b/packages/mdc-chips/chip/index.js @@ -22,6 +22,8 @@ import MDCChipAdapter from './adapter'; import {MDCChipFoundation} from './foundation'; import {strings} from './constants'; +const INTERACTION_EVENTS = ['click', 'keydown']; + /** * @extends {MDCComponent} * @final @@ -35,8 +37,17 @@ class MDCChip extends MDCComponent { /** @private {?Element} */ this.leadingIcon_; + /** @private {?Element} */ + this.trailingIcon_; /** @private {!MDCRipple} */ this.ripple_; + + /** @private {?function(?Event): undefined} */ + this.handleInteraction_; + /** @private {?function(!Event): undefined} */ + this.handleTransitionEnd_; + /** @private {function(!Event): undefined} */ + this.handleTrailingIconInteraction_; } /** @@ -49,6 +60,7 @@ class MDCChip extends MDCComponent { initialize() { this.leadingIcon_ = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); + this.trailingIcon_ = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR); // Adjust ripple size for chips with animated growing width. This applies when filter chips without // a leading icon are selected, and a leading checkmark will cause the chip width to expand. @@ -69,8 +81,37 @@ class MDCChip extends MDCComponent { } } + initialSyncWithDOM() { + this.handleInteraction_ = (evt) => this.foundation_.handleInteraction(evt); + this.handleTransitionEnd_ = (evt) => this.foundation_.handleTransitionEnd(evt); + this.handleTrailingIconInteraction_ = (evt) => this.foundation_.handleTrailingIconInteraction(evt); + + INTERACTION_EVENTS.forEach((evtType) => { + this.root_.addEventListener(evtType, this.handleInteraction_); + }); + this.root_.addEventListener('transitionend', this.handleTransitionEnd_); + + if (this.trailingIcon_) { + INTERACTION_EVENTS.forEach((evtType) => { + this.trailingIcon_.addEventListener(evtType, this.handleTrailingIconInteraction_); + }); + } + } + destroy() { this.ripple_.destroy(); + + INTERACTION_EVENTS.forEach((evtType) => { + this.root_.removeEventListener(evtType, this.handleInteraction_); + }); + this.root_.removeEventListener('transitionend', this.handleTransitionEnd_); + + if (this.trailingIcon_) { + INTERACTION_EVENTS.forEach((evtType) => { + this.trailingIcon_.removeEventListener(evtType, this.handleTrailingIconInteraction_); + }); + } + super.destroy(); } @@ -131,20 +172,6 @@ class MDCChip extends MDCComponent { } }, eventTargetHasClass: (target, className) => target.classList.contains(className), - registerEventHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterEventHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - registerTrailingIconInteractionHandler: (evtType, handler) => { - const trailingIconEl = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR); - if (trailingIconEl) { - trailingIconEl.addEventListener(evtType, handler); - } - }, - deregisterTrailingIconInteractionHandler: (evtType, handler) => { - const trailingIconEl = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR); - if (trailingIconEl) { - trailingIconEl.removeEventListener(evtType, handler); - } - }, notifyInteraction: () => this.emit(strings.INTERACTION_EVENT, {chip: this}, true /* shouldBubble */), notifyTrailingIconInteraction: () => this.emit( strings.TRAILING_ICON_INTERACTION_EVENT, {chip: this}, true /* shouldBubble */), diff --git a/test/unit/mdc-chips/mdc-chip-set.foundation.test.js b/test/unit/mdc-chips/mdc-chip-set.foundation.test.js index 4ef7e085317..5e9ec9d3361 100644 --- a/test/unit/mdc-chips/mdc-chip-set.foundation.test.js +++ b/test/unit/mdc-chips/mdc-chip-set.foundation.test.js @@ -34,7 +34,7 @@ test('exports cssClasses', () => { test('defaultAdapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCChipSetFoundation, [ - 'hasClass', 'registerInteractionHandler', 'deregisterInteractionHandler', 'removeChip', + 'hasClass', 'removeChip', ]); }); @@ -56,35 +56,13 @@ const setupTest = () => { return {foundation, mockAdapter, chipA, chipB}; }; -test('#init adds event listeners', () => { - const {foundation, mockAdapter} = setupTest(); - foundation.init(); - - td.verify(mockAdapter.registerInteractionHandler('MDCChip:interaction', td.matchers.isA(Function))); -}); - -test('#destroy removes event listeners', () => { - const {foundation, mockAdapter} = setupTest(); - foundation.destroy(); - - td.verify(mockAdapter.deregisterInteractionHandler('MDCChip:interaction', td.matchers.isA(Function))); -}); - -test('in choice chips, on custom MDCChip:interaction event selects chip if no chips are selected', () => { +test('in choice chips, #handleChipInteraction selects chip if no chips are selected', () => { const {foundation, mockAdapter, chipA} = setupTest(); - let chipInteractionHandler; - td.when(mockAdapter.registerInteractionHandler('MDCChip:interaction', td.matchers.isA(Function))) - .thenDo((evtType, handler) => { - chipInteractionHandler = handler; - }); td.when(mockAdapter.hasClass(cssClasses.CHOICE)).thenReturn(true); - td.when(chipA.foundation.isSelected()).thenReturn(false); assert.equal(foundation.selectedChips_.length, 0); - foundation.init(); - - chipInteractionHandler({ + foundation.handleChipInteraction({ detail: { chip: chipA, }, @@ -93,23 +71,15 @@ test('in choice chips, on custom MDCChip:interaction event selects chip if no ch assert.equal(foundation.selectedChips_.length, 1); }); -test('in choice chips, on custom MDCChip:interaction event deselects chip if another chip is selected', () => { +test('in choice chips, #handleChipInteraction deselects chip if another chip is selected', () => { const {foundation, mockAdapter, chipA, chipB} = setupTest(); - let chipInteractionHandler; - td.when(mockAdapter.registerInteractionHandler('MDCChip:interaction', td.matchers.isA(Function))) - .thenDo((evtType, handler) => { - chipInteractionHandler = handler; - }); td.when(mockAdapter.hasClass(cssClasses.CHOICE)).thenReturn(true); - foundation.select(chipB.foundation); td.when(chipA.foundation.isSelected()).thenReturn(false); td.when(chipB.foundation.isSelected()).thenReturn(true); assert.equal(foundation.selectedChips_.length, 1); - foundation.init(); - - chipInteractionHandler({ + foundation.handleChipInteraction({ detail: { chip: chipA, }, @@ -119,22 +89,14 @@ test('in choice chips, on custom MDCChip:interaction event deselects chip if ano assert.equal(foundation.selectedChips_.length, 1); }); -test('in filter chips, on custom MDCChip:interaction event selects multiple chips', () => { +test('in filter chips, #handleChipInteraction selects multiple chips', () => { const {foundation, mockAdapter, chipA, chipB} = setupTest(); - let chipInteractionHandler; - td.when(mockAdapter.registerInteractionHandler('MDCChip:interaction', td.matchers.isA(Function))) - .thenDo((evtType, handler) => { - chipInteractionHandler = handler; - }); td.when(mockAdapter.hasClass(cssClasses.FILTER)).thenReturn(true); - td.when(chipA.foundation.isSelected()).thenReturn(false); td.when(chipB.foundation.isSelected()).thenReturn(false); assert.equal(foundation.selectedChips_.length, 0); - foundation.init(); - - chipInteractionHandler({ + foundation.handleChipInteraction({ detail: { chip: chipA, }, @@ -142,7 +104,7 @@ test('in filter chips, on custom MDCChip:interaction event selects multiple chip td.verify(chipA.foundation.setSelected(true)); assert.equal(foundation.selectedChips_.length, 1); - chipInteractionHandler({ + foundation.handleChipInteraction({ detail: { chip: chipB, }, @@ -151,24 +113,16 @@ test('in filter chips, on custom MDCChip:interaction event selects multiple chip assert.equal(foundation.selectedChips_.length, 2); }); -test('in filter chips, on custom MDCChip:interaction event deselects selected chips', () => { +test('in filter chips, #handleChipInteraction event deselects selected chips', () => { const {foundation, mockAdapter, chipA, chipB} = setupTest(); - let chipInteractionHandler; - td.when(mockAdapter.registerInteractionHandler('MDCChip:interaction', td.matchers.isA(Function))) - .thenDo((evtType, handler) => { - chipInteractionHandler = handler; - }); td.when(mockAdapter.hasClass(cssClasses.FILTER)).thenReturn(true); - foundation.select(chipA.foundation); foundation.select(chipB.foundation); td.when(chipA.foundation.isSelected()).thenReturn(true); td.when(chipB.foundation.isSelected()).thenReturn(true); assert.equal(foundation.selectedChips_.length, 2); - foundation.init(); - - chipInteractionHandler({ + foundation.handleChipInteraction({ detail: { chip: chipB, }, @@ -176,7 +130,7 @@ test('in filter chips, on custom MDCChip:interaction event deselects selected ch td.verify(chipB.foundation.setSelected(false)); assert.equal(foundation.selectedChips_.length, 1); - chipInteractionHandler({ + foundation.handleChipInteraction({ detail: { chip: chipA, }, @@ -185,16 +139,10 @@ test('in filter chips, on custom MDCChip:interaction event deselects selected ch assert.equal(foundation.selectedChips_.length, 0); }); -test('on custom MDCChip:removal event removes chip', () => { +test('#handleChipRemoval removes chip', () => { const {foundation, mockAdapter, chipA} = setupTest(); - let chipRemovalHandler; - td.when(mockAdapter.registerInteractionHandler('MDCChip:removal', td.matchers.isA(Function))) - .thenDo((evtType, handler) => { - chipRemovalHandler = handler; - }); - - foundation.init(); - chipRemovalHandler({ + + foundation.handleChipRemoval({ detail: { chip: chipA, }, diff --git a/test/unit/mdc-chips/mdc-chip-set.test.js b/test/unit/mdc-chips/mdc-chip-set.test.js index 93d17c3b852..bb402f2607c 100644 --- a/test/unit/mdc-chips/mdc-chip-set.test.js +++ b/test/unit/mdc-chips/mdc-chip-set.test.js @@ -15,7 +15,6 @@ */ import bel from 'bel'; -import domEvents from 'dom-events'; import {assert} from 'chai'; import td from 'testdouble'; @@ -112,25 +111,6 @@ test('#adapter.hasClass returns true if class is set on chip set element', () => assert.isTrue(component.getDefaultFoundation().adapter_.hasClass('foo')); }); -test('#adapter.registerInteractionHandler adds a handler to the root element for a given event', () => { - const {root, component} = setupTest(); - const handler = td.func('eventHandler'); - - component.getDefaultFoundation().adapter_.registerInteractionHandler('click', handler); - domEvents.emit(root, 'click'); - td.verify(handler(td.matchers.anything())); -}); - -test('#adapter.deregisterInteractionHandler removes a handler from the root element for a given event', () => { - const {root, component} = setupTest(); - const handler = td.func('eventHandler'); - - root.addEventListener('click', handler); - component.getDefaultFoundation().adapter_.deregisterInteractionHandler('click', handler); - domEvents.emit(root, 'click'); - td.verify(handler(td.matchers.anything()), {times: 0}); -}); - test('#adapter.removeChip removes the chip object from the chip set', () => { const root = getFixture(); const component = new MDCChipSet(root, undefined, (el) => new FakeChip(el)); diff --git a/test/unit/mdc-chips/mdc-chip.foundation.test.js b/test/unit/mdc-chips/mdc-chip.foundation.test.js index a8fec52df7b..a31a2c627ce 100644 --- a/test/unit/mdc-chips/mdc-chip.foundation.test.js +++ b/test/unit/mdc-chips/mdc-chip.foundation.test.js @@ -17,7 +17,7 @@ import {assert} from 'chai'; import td from 'testdouble'; -import {verifyDefaultAdapter, captureHandlers} from '../helpers/foundation'; +import {verifyDefaultAdapter} from '../helpers/foundation'; import {createMockRaf} from '../helpers/raf'; import {setupFoundationTest} from '../helpers/setup'; import {MDCChipFoundation} from '../../../packages/mdc-chips/chip/foundation'; @@ -37,9 +37,7 @@ test('exports cssClasses', () => { test('defaultAdapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCChipFoundation, [ 'addClass', 'removeClass', 'hasClass', 'addClassToLeadingIcon', - 'removeClassFromLeadingIcon', 'eventTargetHasClass', 'registerEventHandler', - 'deregisterEventHandler', 'registerTrailingIconInteractionHandler', - 'deregisterTrailingIconInteractionHandler', 'notifyInteraction', + 'removeClassFromLeadingIcon', 'eventTargetHasClass', 'notifyInteraction', 'notifyTrailingIconInteraction', 'notifyRemoval', 'getComputedStyleValue', 'setStyleProperty', ]); @@ -47,34 +45,6 @@ test('defaultAdapter returns a complete adapter implementation', () => { const setupTest = () => setupFoundationTest(MDCChipFoundation); -test('#init adds event listeners', () => { - const {foundation, mockAdapter} = setupTest(); - foundation.init(); - - td.verify(mockAdapter.registerEventHandler('click', td.matchers.isA(Function))); - td.verify(mockAdapter.registerEventHandler('keydown', td.matchers.isA(Function))); - td.verify(mockAdapter.registerEventHandler('transitionend', td.matchers.isA(Function))); - td.verify(mockAdapter.registerTrailingIconInteractionHandler('click', td.matchers.isA(Function))); - td.verify(mockAdapter.registerTrailingIconInteractionHandler('keydown', td.matchers.isA(Function))); - td.verify(mockAdapter.registerTrailingIconInteractionHandler('touchstart', td.matchers.isA(Function))); - td.verify(mockAdapter.registerTrailingIconInteractionHandler('pointerdown', td.matchers.isA(Function))); - td.verify(mockAdapter.registerTrailingIconInteractionHandler('mousedown', td.matchers.isA(Function))); -}); - -test('#destroy removes event listeners', () => { - const {foundation, mockAdapter} = setupTest(); - foundation.destroy(); - - td.verify(mockAdapter.deregisterEventHandler('click', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterEventHandler('keydown', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterEventHandler('transitionend', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('click', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('keydown', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('touchstart', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('pointerdown', td.matchers.isA(Function))); - td.verify(mockAdapter.deregisterTrailingIconInteractionHandler('mousedown', td.matchers.isA(Function))); -}); - test('#isSelected returns true if mdc-chip--selected class is present', () => { const {foundation, mockAdapter} = setupTest(); td.when(mockAdapter.hasClass(cssClasses.SELECTED)).thenReturn(true); @@ -105,22 +75,19 @@ test(`#beginExit adds ${cssClasses.CHIP_EXIT} class`, () => { td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT)); }); -test('on click, emit custom event', () => { +test('#handleInteraction emits custom event on click', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'click', }; - foundation.init(); - handlers.click(mockEvt); + foundation.handleInteraction(mockEvt); td.verify(mockAdapter.notifyInteraction()); }); -test('on chip width transition end, notify removal of chip', () => { +test('#handleTransitionEnd notifies removal of chip on width transition end', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'transitionend', target: {}, @@ -128,16 +95,14 @@ test('on chip width transition end, notify removal of chip', () => { }; td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.CHIP_EXIT)).thenReturn(true); - foundation.init(); - handlers.transitionend(mockEvt); + foundation.handleTransitionEnd(mockEvt); td.verify(mockAdapter.notifyRemoval()); }); -test('on chip opacity transition end, animate width if chip is exiting', () => { +test('#handleTransitionEnd animates width if chip is exiting on chip opacity transition end', () => { const {foundation, mockAdapter} = setupTest(); const raf = createMockRaf(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'transitionend', target: {}, @@ -146,8 +111,7 @@ test('on chip opacity transition end, animate width if chip is exiting', () => { td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.CHIP_EXIT)).thenReturn(true); td.when(mockAdapter.getComputedStyleValue('width')).thenReturn('100px'); - foundation.init(); - handlers.transitionend(mockEvt); + foundation.handleTransitionEnd(mockEvt); raf.flush(); td.verify(mockAdapter.setStyleProperty('width', '100px')); @@ -158,10 +122,9 @@ test('on chip opacity transition end, animate width if chip is exiting', () => { td.verify(mockAdapter.setStyleProperty('width', '0')); }); -test(`on leading icon opacity transition end, add ${cssClasses.HIDDEN_LEADING_ICON}` + - 'class to leading icon if chip is selected', () => { +test(`#handleTransitionEnd adds ${cssClasses.HIDDEN_LEADING_ICON} class to leading icon ` + + 'on leading icon opacity transition end, if chip is selected', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'transitionend', target: {}, @@ -170,15 +133,14 @@ test(`on leading icon opacity transition end, add ${cssClasses.HIDDEN_LEADING_IC td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.LEADING_ICON)).thenReturn(true); td.when(mockAdapter.hasClass(cssClasses.SELECTED)).thenReturn(true); - foundation.init(); - handlers.transitionend(mockEvt); + foundation.handleTransitionEnd(mockEvt); td.verify(mockAdapter.addClassToLeadingIcon(cssClasses.HIDDEN_LEADING_ICON)); }); -test('on leading icon opacity transition end, do nothing if chip is not selected', () => { +test('#handleTransitionEnd does nothing on leading icon opacity transition end,' + + 'if chip is not selected', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'transitionend', target: {}, @@ -187,16 +149,14 @@ test('on leading icon opacity transition end, do nothing if chip is not selected td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.LEADING_ICON)).thenReturn(true); td.when(mockAdapter.hasClass(cssClasses.SELECTED)).thenReturn(false); - foundation.init(); - handlers.transitionend(mockEvt); + foundation.handleTransitionEnd(mockEvt); td.verify(mockAdapter.addClassToLeadingIcon(cssClasses.HIDDEN_LEADING_ICON), {times: 0}); }); -test(`on checkmark opacity transition end, remove ${cssClasses.HIDDEN_LEADING_ICON}` + - 'class from leading icon if chip is not selected', () => { +test(`#handleTransitionEnd removes ${cssClasses.HIDDEN_LEADING_ICON} class from leading icon ` + + 'on checkmark opacity transition end, if chip is not selected', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'transitionend', target: {}, @@ -205,15 +165,13 @@ test(`on checkmark opacity transition end, remove ${cssClasses.HIDDEN_LEADING_IC td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.CHECKMARK)).thenReturn(true); td.when(mockAdapter.hasClass(cssClasses.SELECTED)).thenReturn(false); - foundation.init(); - handlers.transitionend(mockEvt); + foundation.handleTransitionEnd(mockEvt); td.verify(mockAdapter.removeClassFromLeadingIcon(cssClasses.HIDDEN_LEADING_ICON)); }); -test('on checkmark opacity transition end, do nothing if chip is selected', () => { +test('#handleTransitionEnd does nothing on checkmark opacity transition end, if chip is selected', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerEventHandler'); const mockEvt = { type: 'transitionend', target: {}, @@ -222,58 +180,49 @@ test('on checkmark opacity transition end, do nothing if chip is selected', () = td.when(mockAdapter.eventTargetHasClass(mockEvt.target, cssClasses.CHECKMARK)).thenReturn(true); td.when(mockAdapter.hasClass(cssClasses.SELECTED)).thenReturn(true); - foundation.init(); - handlers.transitionend(mockEvt); + foundation.handleTransitionEnd(mockEvt); td.verify(mockAdapter.removeClassFromLeadingIcon(cssClasses.HIDDEN_LEADING_ICON), {times: 0}); }); -test('on click in trailing icon, emit custom event', () => { +test('#handleTrailingIconInteraction emits custom event on click in trailing icon', () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerTrailingIconInteractionHandler'); const mockEvt = { type: 'click', stopPropagation: td.func('stopPropagation'), }; - foundation.init(); - handlers.click(mockEvt); - + foundation.handleTrailingIconInteraction(mockEvt); td.verify(mockAdapter.notifyTrailingIconInteraction()); td.verify(mockEvt.stopPropagation()); }); -test(`on click in trailing icon, add ${cssClasses.CHIP_EXIT} class by default`, () => { +test(`#handleTrailingIconInteraction adds ${cssClasses.CHIP_EXIT} class by default on click in trailing icon`, () => { const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerTrailingIconInteractionHandler'); const mockEvt = { type: 'click', stopPropagation: td.func('stopPropagation'), }; - foundation.init(); - handlers.click(mockEvt); + foundation.handleTrailingIconInteraction(mockEvt); assert.isTrue(foundation.getShouldRemoveOnTrailingIconClick()); td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT)); td.verify(mockEvt.stopPropagation()); }); -test(`on click in trailing icon, do not add ${cssClasses.CHIP_EXIT} class if shouldRemoveOnTrailingIconClick_ is false`, - () => { - const {foundation, mockAdapter} = setupTest(); - const handlers = captureHandlers(mockAdapter, 'registerTrailingIconInteractionHandler'); - const mockEvt = { - type: 'click', - stopPropagation: td.func('stopPropagation'), - }; - - foundation.init(); - foundation.setShouldRemoveOnTrailingIconClick(false); - handlers.click(mockEvt); - - assert.isFalse(foundation.getShouldRemoveOnTrailingIconClick()); - td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT), {times: 0}); - td.verify(mockEvt.stopPropagation()); - } -); +test(`#handleTrailingIconInteraction does not add ${cssClasses.CHIP_EXIT} class on click in trailing icon ` + + 'if shouldRemoveOnTrailingIconClick_ is false', () => { + const {foundation, mockAdapter} = setupTest(); + const mockEvt = { + type: 'click', + stopPropagation: td.func('stopPropagation'), + }; + + foundation.setShouldRemoveOnTrailingIconClick(false); + foundation.handleTrailingIconInteraction(mockEvt); + + assert.isFalse(foundation.getShouldRemoveOnTrailingIconClick()); + td.verify(mockAdapter.addClass(cssClasses.CHIP_EXIT), {times: 0}); + td.verify(mockEvt.stopPropagation()); +}); diff --git a/test/unit/mdc-chips/mdc-chip.test.js b/test/unit/mdc-chips/mdc-chip.test.js index 1b5298609e1..9d3b0bce73d 100644 --- a/test/unit/mdc-chips/mdc-chip.test.js +++ b/test/unit/mdc-chips/mdc-chip.test.js @@ -17,7 +17,6 @@ import bel from 'bel'; import {assert} from 'chai'; import td from 'testdouble'; -import domEvents from 'dom-events'; import {MDCRipple} from '../../../packages/mdc-ripple'; import {MDCChip, MDCChipFoundation} from '../../../packages/mdc-chips/chip'; @@ -106,56 +105,6 @@ test('adapter#eventTargetHasClass returns true if given element has class', () = assert.isTrue(component.getDefaultFoundation().adapter_.eventTargetHasClass(mockEventTarget, 'foo')); }); -test('#adapter.registerEventHandler adds event listener for a given event to the root element', () => { - const {root, component} = setupTest(); - const handler = td.func('click handler'); - component.getDefaultFoundation().adapter_.registerEventHandler('click', handler); - domEvents.emit(root, 'click'); - - td.verify(handler(td.matchers.anything())); -}); - -test('#adapter.deregisterEventHandler removes event listener for a given event from the root element', () => { - const {root, component} = setupTest(); - const handler = td.func('click handler'); - - root.addEventListener('click', handler); - component.getDefaultFoundation().adapter_.deregisterEventHandler('click', handler); - domEvents.emit(root, 'click'); - - td.verify(handler(td.matchers.anything()), {times: 0}); -}); - -test('#adapter.registerTrailingIconInteractionHandler adds event listener for a given event to the trailing' + -'icon element', () => { - const {root, component} = setupTest(); - const icon = bel` - cancel - `; - root.appendChild(icon); - const handler = td.func('click handler'); - component.getDefaultFoundation().adapter_.registerTrailingIconInteractionHandler('click', handler); - domEvents.emit(icon, 'click'); - - td.verify(handler(td.matchers.anything())); -}); - -test('#adapter.deregisterTrailingIconInteractionHandler removes event listener for a given event from the trailing ' + -'icon element', () => { - const {root, component} = setupTest(); - const icon = bel` - cancel - `; - root.appendChild(icon); - const handler = td.func('click handler'); - - icon.addEventListener('click', handler); - component.getDefaultFoundation().adapter_.deregisterTrailingIconInteractionHandler('click', handler); - domEvents.emit(icon, 'click'); - - td.verify(handler(td.matchers.anything()), {times: 0}); -}); - test('#adapter.notifyInteraction emits ' + MDCChipFoundation.strings.INTERACTION_EVENT, () => { const {component} = setupTest(); const handler = td.func('interaction handler'); From bc500f247c9c0dcaaa81f9c3a9f93de22ca83480 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Mon, 23 Jul 2018 20:21:41 -0700 Subject: [PATCH 46/53] chore(infrastructure): Always print report page URLs (#3181) - Fixes 2 bugs introduced in #3171: - Test runs with zero diffs would not print the report page URL to the console - Test duration calculation always returned `0 milliseconds` for diffs against `master` - Also removes buggy `commit_offset` field: - It only works with non-bug-fix releases. It would fail on `v0.36.1`, for example. --- test/screenshot/infra/commands/test.js | 28 +++++++++++++-------- test/screenshot/infra/lib/report-builder.js | 21 ---------------- test/screenshot/infra/proto/mdc.pb.js | 25 +----------------- test/screenshot/infra/proto/mdc.proto | 1 - test/screenshot/report/_metadata.hbs | 3 --- 5 files changed, 18 insertions(+), 60 deletions(-) diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 9a9574a32e9..d50eb1ac9ac 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -74,6 +74,8 @@ class TestCommand { const masterDiffReportData = await this.diffAgainstMaster_({localDiffReportData, snapshotGitRev}); this.logTestResults_(localDiffReportData); this.logTestResults_(masterDiffReportData); + } else { + this.logTestResults_(localDiffReportData); } // Diffs against master shouldn't fail the Travis job. @@ -121,10 +123,11 @@ class TestCommand { * TODO(acdvorak): Rename this method * @param {!mdc.proto.DiffBase} goldenDiffBase * @param {!Array} capturedScreenshots + * @param {string} startTimeIsoUtc * @return {!Promise} * @private */ - async diffAgainstMasterImpl_(goldenDiffBase, capturedScreenshots) { + async diffAgainstMasterImpl_({goldenDiffBase, capturedScreenshots, startTimeIsoUtc}) { const controller = new Controller(); /** @type {!mdc.proto.ReportData} */ @@ -132,7 +135,7 @@ class TestCommand { try { await controller.uploadAllAssets(reportData); - await this.copyAndCompareScreenshots_({reportData, capturedScreenshots}); + await this.copyAndCompareScreenshots_({reportData, capturedScreenshots, startTimeIsoUtc}); controller.populateMaps(reportData); @@ -170,14 +173,11 @@ class TestCommand { const masterDiffBase = await this.diffBaseParser_.parseMasterDiffBase(); /** @type {!mdc.proto.ReportData} */ - const masterDiffReportData = await this.diffAgainstMasterImpl_(masterDiffBase, capturedScreenshots); - - masterDiffReportData.meta.start_time_iso_utc = localDiffReportData.meta.start_time_iso_utc; - masterDiffReportData.meta.end_time_iso_utc = new Date().toISOString(); - masterDiffReportData.meta.duration_ms = Duration.elapsed( - masterDiffReportData.meta.start_time_iso_utc, - masterDiffReportData.meta.end_time_iso_utc - ).toMillis(); + const masterDiffReportData = await this.diffAgainstMasterImpl_({ + goldenDiffBase: masterDiffBase, + capturedScreenshots, + startTimeIsoUtc: localDiffReportData.meta.start_time_iso_utc, + }); const prNumber = snapshotGitRev.pr_number; const comment = this.getPrComment_({masterDiffReportData, snapshotGitRev}); @@ -189,10 +189,11 @@ class TestCommand { /** * @param {!mdc.proto.ReportData} reportData * @param {!Array} capturedScreenshots + * @param {string} startTimeIsoUtc * @return {!Promise} * @private */ - async copyAndCompareScreenshots_({reportData, capturedScreenshots}) { + async copyAndCompareScreenshots_({reportData, capturedScreenshots, startTimeIsoUtc}) { const num = capturedScreenshots.length; const plural = num === 1 ? '' : 's'; this.logger_.foldStart('screenshot.compare_master', `Comparing ${num} screenshot${plural} to master`); @@ -235,6 +236,11 @@ class TestCommand { await Promise.all(promises); + const endTimeIsoUtc = new Date().toISOString(); + reportData.meta.start_time_iso_utc = startTimeIsoUtc; + reportData.meta.end_time_iso_utc = endTimeIsoUtc; + reportData.meta.duration_ms = Duration.elapsed(startTimeIsoUtc, endTimeIsoUtc).toMillis(); + this.logger_.foldEnd('screenshot.compare_master'); } diff --git a/test/screenshot/infra/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js index 89fdd832856..3671f4cd129 100644 --- a/test/screenshot/infra/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -454,7 +454,6 @@ class ReportBuilder { }), mdc_version: LibraryVersion.create({ version_string: mdcVersionString, - commit_offset: await this.getCommitDistance_(mdcVersionString), }), }); } @@ -513,26 +512,6 @@ class ReportBuilder { return stdOut[0].trim().replace(/^v/, ''); // `node --version` returns "v8.11.0", so we strip the leading 'v' } - /** - * @param {string} mdcVersion - * @return {!Promise} - */ - async getCommitDistance_(mdcVersion) { - try { - return (await this.gitRepo_.getLog([`v${mdcVersion}..HEAD`])).length; - } catch (err) { - // To save time, Travis CI only clones a certain number of commits. - // Unfortunately, if the user's PR branch has a lot of commits, `git log` will fail with an error like this: - // - // fatal: ambiguous argument 'v0.37.1..HEAD': unknown revision or path not in the working tree. - // Use '--' to separate paths from revisions, like this: - // 'git [...] -- [...]' - // - // Commit distance isn't critical information, so just return 0. - return 0; - } - } - /** * @return {!Promise} * @private diff --git a/test/screenshot/infra/proto/mdc.pb.js b/test/screenshot/infra/proto/mdc.pb.js index 1f1e2cffbaa..680c85befba 100644 --- a/test/screenshot/infra/proto/mdc.pb.js +++ b/test/screenshot/infra/proto/mdc.pb.js @@ -2120,7 +2120,6 @@ $root.mdc = (function() { * @memberof mdc.proto * @interface ILibraryVersion * @property {string|null} [version_string] LibraryVersion version_string - * @property {number|null} [commit_offset] LibraryVersion commit_offset */ /** @@ -2146,14 +2145,6 @@ $root.mdc = (function() { */ LibraryVersion.prototype.version_string = ""; - /** - * LibraryVersion commit_offset. - * @member {number} commit_offset - * @memberof mdc.proto.LibraryVersion - * @instance - */ - LibraryVersion.prototype.commit_offset = 0; - /** * Creates a new LibraryVersion instance using the specified properties. * @function create @@ -2180,8 +2171,6 @@ $root.mdc = (function() { writer = $Writer.create(); if (message.version_string != null && message.hasOwnProperty("version_string")) writer.uint32(/* id 1, wireType 2 =*/10).string(message.version_string); - if (message.commit_offset != null && message.hasOwnProperty("commit_offset")) - writer.uint32(/* id 2, wireType 0 =*/16).int32(message.commit_offset); return writer; }; @@ -2219,9 +2208,6 @@ $root.mdc = (function() { case 1: message.version_string = reader.string(); break; - case 2: - message.commit_offset = reader.int32(); - break; default: reader.skipType(tag & 7); break; @@ -2260,9 +2246,6 @@ $root.mdc = (function() { if (message.version_string != null && message.hasOwnProperty("version_string")) if (!$util.isString(message.version_string)) return "version_string: string expected"; - if (message.commit_offset != null && message.hasOwnProperty("commit_offset")) - if (!$util.isInteger(message.commit_offset)) - return "commit_offset: integer expected"; return null; }; @@ -2280,8 +2263,6 @@ $root.mdc = (function() { var message = new $root.mdc.proto.LibraryVersion(); if (object.version_string != null) message.version_string = String(object.version_string); - if (object.commit_offset != null) - message.commit_offset = object.commit_offset | 0; return message; }; @@ -2298,14 +2279,10 @@ $root.mdc = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) { + if (options.defaults) object.version_string = ""; - object.commit_offset = 0; - } if (message.version_string != null && message.hasOwnProperty("version_string")) object.version_string = message.version_string; - if (message.commit_offset != null && message.hasOwnProperty("commit_offset")) - object.commit_offset = message.commit_offset; return object; }; diff --git a/test/screenshot/infra/proto/mdc.proto b/test/screenshot/infra/proto/mdc.proto index b06d502ba3c..131859bf75d 100644 --- a/test/screenshot/infra/proto/mdc.proto +++ b/test/screenshot/infra/proto/mdc.proto @@ -109,7 +109,6 @@ message User { message LibraryVersion { string version_string = 1; - int32 commit_offset = 2; } message UserAgents { diff --git a/test/screenshot/report/_metadata.hbs b/test/screenshot/report/_metadata.hbs index 650ec7cae23..910a05056e9 100644 --- a/test/screenshot/report/_metadata.hbs +++ b/test/screenshot/report/_metadata.hbs @@ -53,9 +53,6 @@ MDC Version: {{meta.mdc_version.version_string}} - {{#if meta.mdc_version.commit_offset}} - - {{/if}} From ce8e724b0476d2a8a27cc3f5264da30eb02991d1 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 24 Jul 2018 13:22:54 -0700 Subject: [PATCH 47/53] chore(infrastructure): Increase SL timeout and disable N-1/N+1 browsers (#3182) ### What it does * Changes the timeout for individual test cases on Sauce Labs from 2 seconds to 10 seconds * Disables unnecessary browsers: Chrome N+1 (dev), Chrome N-1, and Firefox N-1 - Our SL account only allows `5` concurrent tests - Unit tests now run in `4` browsers instead of `7`: * Chrome latest (desktop) * Firefox latest * IE 11 * Safari (mobile) - Enables [extended debugging](https://wiki.saucelabs.com/pages/viewpage.action?pageId=70072943) (capturing browser console logs) in Chrome and Firefox AFAIK the only way to verify this PR is to run it repeatedly on Travis and look for the errors listed below. Refs #3191 ### Why This PR attempts to work around two specific types of flakes I've seen repeatedly on Sauce Labs: #### Timeouts on individual test cases that use `requestAnimationFrame()` or `setTimeout()` ``` IE 11.0.0 (Windows 8.1.0.0) MDCSliderFoundation - pointer events on touchstart takes RTL into account when computing the slider's value using the X coordinate of the event FAILED Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. IE 11.0.0 (Windows 8.1.0.0) MDCSliderFoundation - pointer events on touchstart adds the mdc-slider--active class to the root element FAILED Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. ........ IE 11.0.0 (Windows 8.1.0.0) MDCSliderFoundation - pointer events on body touchmove updates the slider's value based on the X coordinate of the event FAILED Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. . IE 11.0.0 (Windows 8.1.0.0) MDCSliderFoundation - pointer events on body touchmove notifies discrete slider pin value marker to change value FAILED Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. IE 11.0.0 (Windows 8.1.0.0) MDCSliderFoundation - pointer events on body touchend removes the mdc-slider--active class from the component FAILED Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. ``` #### Concurrency limits Our SL account is limited to 5 concurrent tests. We constantly blow past that limit because `karma.conf.js` tests _7_ browsers on every single commit. This, in turn, causes subsequent SL requests to enqueue, which can lead to test suite timeouts: ``` WARN [launcher]: Safari on SauceLabs have not captured in 240000 ms, killing. INFO [launcher]: Trying to start Safari on SauceLabs again (1/2). ... WARN [launcher]: Safari on SauceLabs have not captured in 240000 ms, killing. INFO [launcher]: Trying to start Safari on SauceLabs again (2/2). ``` [SL logs](https://saucelabs.com/beta/tests/696708e0fd284bc6b2a03ce6a8decb4f): ![image](https://user-images.githubusercontent.com/409245/43157525-29256050-8f32-11e8-8e65-fb6e83fe6930.png) ### Possible root causes of flaky unit tests on SL Most likely: * **Concurrency limit**: We have a free OSS account on SL, which gives us 5 concurrent tests. We currently run unit tests in 7 browsers, including Chrome N+1, Chrome N-1, and Firefox N-1. * **Traffic congestion**: SL might get overloaded during peak hours and queue our tests for a long time Less likely, but possible: * Buggy implementation and/or usage of mock `requestAnimationFrame()` in [`test/unit/helpers/raf.js`](https://github.com/material-components/material-components-web/blob/bc500f247c9c0dcaaa81f9c3a9f93de22ca83480/test/unit/helpers/raf.js#L32) * Buggy implementation and/or usage of `lolex` (mock `setTimeout()`) ### Other Sauce Labs errors This one connected OK, but kept sending the `GET title` command repeatedly and then gave up. [SL logs](https://saucelabs.com/beta/tests/cd8e1a1b7c414217b140126c52f068d1): ![image](https://user-images.githubusercontent.com/409245/43158106-ae96c89a-8f33-11e8-93b8-954edb71c4ea.png) --- karma.conf.js | 74 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 47e1c0f023b..d409833517c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -21,42 +21,66 @@ const USING_TRAVISCI = Boolean(process.env.TRAVIS); const USING_SL = Boolean(process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY); const SL_LAUNCHERS = { + /* + * Chrome (desktop) + */ + 'sl-chrome-stable': { base: 'SauceLabs', browserName: 'chrome', version: 'latest', platform: 'macOS 10.12', + extendedDebugging: true, }, - 'sl-chrome-beta': { - base: 'SauceLabs', - browserName: 'chrome', - version: 'dev', - platform: 'macOS 10.12', - }, - 'sl-chrome-previous': { - base: 'SauceLabs', - browserName: 'chrome', - version: 'latest-1', - platform: 'macOS 10.12', - }, + // 'sl-chrome-beta': { + // base: 'SauceLabs', + // browserName: 'chrome', + // version: 'dev', + // platform: 'macOS 10.12', + // extendedDebugging: true, + // }, + // 'sl-chrome-previous': { + // base: 'SauceLabs', + // browserName: 'chrome', + // version: 'latest-1', + // platform: 'macOS 10.12', + // extendedDebugging: true, + // }, + + /* + * Firefox + */ + 'sl-firefox-stable': { base: 'SauceLabs', browserName: 'firefox', version: 'latest', platform: 'Windows 10', + extendedDebugging: true, }, - 'sl-firefox-previous': { - base: 'SauceLabs', - browserName: 'firefox', - version: 'latest-1', - platform: 'Windows 10', - }, + // 'sl-firefox-previous': { + // base: 'SauceLabs', + // browserName: 'firefox', + // version: 'latest-1', + // platform: 'Windows 10', + // extendedDebugging: true, + // }, + + /* + * IE + */ + 'sl-ie': { base: 'SauceLabs', browserName: 'internet explorer', version: '11', platform: 'Windows 8.1', }, + + /* + * Edge + */ + // TODO(sgomes): Re-enable Edge and Safari after Sauce Labs problems are fixed. // 'sl-edge': { // base: 'SauceLabs', @@ -64,6 +88,11 @@ const SL_LAUNCHERS = { // version: 'latest', // platform: 'Windows 10', // }, + + /* + * Safari (desktop) + */ + // 'sl-safari-stable': { // base: 'SauceLabs', // browserName: 'safari', @@ -76,6 +105,11 @@ const SL_LAUNCHERS = { // version: '9.0', // platform: 'OS X 10.11', // }, + + /* + * Safari (mobile) + */ + 'sl-ios-safari-latest': { base: 'SauceLabs', deviceName: 'iPhone Simulator', @@ -126,6 +160,10 @@ module.exports = function(config) { mocha: { reporter: 'html', ui: 'qunit', + + // Number of milliseconds to wait for an individual `test(...)` function to complete. + // The default is 2000. + timeout: 10000, }, }, From 2f37d01181094dc4e1f4a8da5f46b8de72bac3b7 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 24 Jul 2018 20:24:55 -0700 Subject: [PATCH 48/53] chore(infrastructure) Add stack traces and format error messages (#3203) ### What it does - Adds stack traces to error messages (Node doesn't yet support `async` stack traces natively) - Formats and error messages so they're actually _readable_ (including nested `VError` exceptions) ### Example output #### Before: ![image](https://user-images.githubusercontent.com/409245/43177681-42e034a6-8f7e-11e8-8030-b4b40a840138.png) #### After: ![image](https://user-images.githubusercontent.com/409245/43177701-60246e4c-8f7e-11e8-9d11-0c4eef692e53.png) --- test/screenshot/infra/commands/test.js | 7 +- test/screenshot/infra/lib/cbt-api.js | 61 ++++++--- test/screenshot/infra/lib/controller.js | 25 ++-- test/screenshot/infra/lib/github-api.js | 107 ++++++++++----- test/screenshot/infra/lib/local-storage.js | 18 +-- test/screenshot/infra/lib/stacktrace.js | 145 +++++++++++++++++++++ test/screenshot/run.js | 3 +- 7 files changed, 288 insertions(+), 78 deletions(-) create mode 100644 test/screenshot/infra/lib/stacktrace.js diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index d50eb1ac9ac..2264237dd7d 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -24,12 +24,13 @@ const GitRevision = mdcProto.GitRevision; const BuildCommand = require('./build'); const Cli = require('../lib/cli'); const CliColor = require('../lib/logger').colors; +const Controller = require('../lib/controller'); const DiffBaseParser = require('../lib/diff-base-parser'); const Duration = require('../lib/duration'); -const Controller = require('../lib/controller'); const GitHubApi = require('../lib/github-api'); const ImageDiffer = require('../lib/image-differ'); const Logger = require('../lib/logger'); +const getStackTrace = require('../lib/stacktrace')('TestCommand'); const {ExitCode} = require('../lib/constants'); // TODO(acdvorak): Refactor most of this class out into a separate file @@ -113,7 +114,7 @@ class TestCommand { this.logComparisonResults_(reportData); } catch (err) { await this.gitHubApi_.setPullRequestError(); - throw new VError(err, 'Failed to run screenshot tests'); + throw new VError(err, getStackTrace('diffAgainstLocal_')); } return reportData; @@ -145,7 +146,7 @@ class TestCommand { this.logComparisonResults_(reportData); } catch (err) { await this.gitHubApi_.setPullRequestError(); - throw new VError(err, 'Failed to run screenshot tests'); + throw new VError(err, getStackTrace('diffAgainstMasterImpl_')); } return reportData; diff --git a/test/screenshot/infra/lib/cbt-api.js b/test/screenshot/infra/lib/cbt-api.js index a9b040e19c4..7e0213f81a9 100644 --- a/test/screenshot/infra/lib/cbt-api.js +++ b/test/screenshot/infra/lib/cbt-api.js @@ -14,6 +14,7 @@ * limitations under the License. */ +const VError = require('verror'); const request = require('request-promise-native'); const mdcProto = require('../proto/mdc.pb').mdc.proto; @@ -29,6 +30,7 @@ const Cli = require('./cli'); const CliColor = require('./logger').colors; const DiffBaseParser = require('./diff-base-parser'); const Duration = require('./duration'); +const getStackTrace = require('./stacktrace')('CbtApi'); const MDC_CBT_USERNAME = process.env.MDC_CBT_USERNAME; const MDC_CBT_AUTHKEY = process.env.MDC_CBT_AUTHKEY; @@ -98,8 +100,8 @@ https://crossbrowsertesting.com/account */ async fetchConcurrencyStats() { const [accountJson, activesJson] = await Promise.all([ - this.sendRequest_('GET', '/account'), - this.sendRequest_('GET', '/account/activeTestCounts'), + this.sendRequest_(getStackTrace('fetchConcurrencyStats'), 'GET', '/account'), + this.sendRequest_(getStackTrace('fetchConcurrencyStats'), 'GET', '/account/activeTestCounts'), ]); const account = CbtAccount.fromObject(accountJson); @@ -126,7 +128,8 @@ https://crossbrowsertesting.com/account console.log('Fetching browsers from CBT...'); - allBrowsersPromise = this.sendRequest_('GET', '/selenium/browsers'); + const stackTrace = getStackTrace('fetchAvailableDevices'); + allBrowsersPromise = this.sendRequest_(stackTrace, 'GET', '/selenium/browsers'); return allBrowsersPromise; } @@ -137,7 +140,8 @@ https://crossbrowsertesting.com/account * @return {!Promise} */ async setTestScore({seleniumSessionId, changedScreenshots}) { - await this.sendRequest_('PUT', `/selenium/${seleniumSessionId}`, { + const stackTrace = getStackTrace('fetchAvailableDevices'); + await this.sendRequest_(stackTrace, 'PUT', `/selenium/${seleniumSessionId}`, { action: 'set_score', score: changedScreenshots.length === 0 ? 'pass' : 'fail', }); @@ -363,13 +367,17 @@ https://crossbrowsertesting.com/account // NOTE: This only returns Selenium tests running on the authenticated CBT user's account. // It does NOT return Selenium tests running under other users. /** @type {!CbtSeleniumListResponse} */ - const listResponse = await this.sendRequest_('GET', '/selenium?active=true&num=100'); + const listResponse = await this.sendRequest_( + getStackTrace('killStalledSeleniumTests'), + 'GET', '/selenium?active=true&num=100' + ); const activeSeleniumTestIds = listResponse.selenium.map((test) => test.selenium_test_id); /** @type {!Array} */ const infoResponses = await Promise.all(activeSeleniumTestIds.map((seleniumTestId) => { - return this.sendRequest_('GET', `/selenium/${seleniumTestId}`); + const infoStackTrace = getStackTrace('killStalledSeleniumTests'); + return this.sendRequest_(infoStackTrace, 'GET', `/selenium/${seleniumTestId}`); })); const stalledSeleniumTestIds = []; @@ -397,22 +405,31 @@ https://crossbrowsertesting.com/account * @return {!Promise} */ async killSeleniumTests(seleniumTestIds, silent = false) { - await Promise.all(seleniumTestIds.map((seleniumTestId) => { + await Promise.all(seleniumTestIds.map(async (seleniumTestId) => { if (!silent) { - console.log(`${CliColor.red('Killing')} zombie Selenium test ${CliColor.bold(seleniumTestId)}`); + console.log(`${CliColor.magenta('Killing')} stalled Selenium test ${CliColor.bold(seleniumTestId)}`); } - return this.sendRequest_('DELETE', `/selenium/${seleniumTestId}`); + const stackTrace = getStackTrace('killSeleniumTests'); + return await this.sendRequest_(stackTrace, 'DELETE', `/selenium/${seleniumTestId}`).catch((err) => { + if (!silent) { + console.warn(`${CliColor.red('Failed')} to kill stalled Selenium test ${CliColor.bold(seleniumTestId)}:`); + console.warn(err); + } + }); })); } /** + * @param {string} stackTrace * @param {string} method * @param {string} endpoint * @param {!Object=} body * @return {!Promise|!Array<*>>} * @private */ - async sendRequest_(method, endpoint, body = undefined) { + async sendRequest_(stackTrace, method, endpoint, body = undefined) { + const uri = `${REST_API_BASE_URL}${endpoint}`; + if (this.cli_.isOffline()) { console.warn( `${CliColor.magenta('WARNING')}:`, @@ -421,16 +438,20 @@ https://crossbrowsertesting.com/account return []; } - return request({ - method, - uri: `${REST_API_BASE_URL}${endpoint}`, - auth: { - username: MDC_CBT_USERNAME, - password: MDC_CBT_AUTHKEY, - }, - body, - json: true, // Automatically stringify the request body and parse the response body as JSON - }); + try { + return await request({ + method, + uri, + auth: { + username: MDC_CBT_USERNAME, + password: MDC_CBT_AUTHKEY, + }, + body, + json: true, // Automatically stringify the request body and parse the response body as JSON + }); + } catch (err) { + throw new VError(err, `CBT API request failed: ${method} ${uri}:\n${stackTrace}`); + } } } diff --git a/test/screenshot/infra/lib/controller.js b/test/screenshot/infra/lib/controller.js index edbe56eb669..098e43f3544 100644 --- a/test/screenshot/infra/lib/controller.js +++ b/test/screenshot/infra/lib/controller.js @@ -16,16 +16,18 @@ 'use strict'; +const VError = require('verror'); + const CbtApi = require('./cbt-api'); const Cli = require('./cli'); const CloudStorage = require('./cloud-storage'); const Duration = require('./duration'); -const GitRepo = require('./git-repo'); const GoldenIo = require('./golden-io'); const Logger = require('./logger'); const ReportBuilder = require('./report-builder'); const ReportWriter = require('./report-writer'); const SeleniumApi = require('./selenium-api'); +const getStackTrace = require('./stacktrace')('Controller'); class Controller { constructor() { @@ -47,12 +49,6 @@ class Controller { */ this.cloudStorage_ = new CloudStorage(); - /** - * @type {!GitRepo} - * @private - */ - this.gitRepo_ = new GitRepo(); - /** * @type {!GoldenIo} * @private @@ -89,7 +85,7 @@ class Controller { */ async initForApproval() { const runReportJsonUrl = this.cli_.runReportJsonUrl; - return this.reportBuilder_.initForApproval({runReportJsonUrl}); + return await this.reportBuilder_.initForApproval({runReportJsonUrl}); } /** @@ -101,14 +97,14 @@ class Controller { if (isOnline) { await this.cbtApi_.killStalledSeleniumTests(); } - return this.reportBuilder_.initForCapture(goldenDiffBase); + return await this.reportBuilder_.initForCapture(goldenDiffBase); } /** * @return {!Promise} */ async initForDemo() { - return this.reportBuilder_.initForDemo(); + return await this.reportBuilder_.initForDemo(); } /** @@ -126,7 +122,14 @@ class Controller { async captureAllPages(reportData) { this.logger_.foldStart('screenshot.capture_images', 'Controller#captureAllPages()'); - await this.seleniumApi_.captureAllPages(reportData); + let stackTrace; + + try { + stackTrace = getStackTrace('captureAllPages'); + await this.seleniumApi_.captureAllPages(reportData); + } catch (err) { + throw new VError(err, stackTrace); + } const meta = reportData.meta; meta.end_time_iso_utc = new Date().toISOString(); diff --git a/test/screenshot/infra/lib/github-api.js b/test/screenshot/infra/lib/github-api.js index a12397f8e9d..0a9d490a61c 100644 --- a/test/screenshot/infra/lib/github-api.js +++ b/test/screenshot/infra/lib/github-api.js @@ -14,10 +14,12 @@ * limitations under the License. */ +const VError = require('verror'); const debounce = require('debounce'); const octokit = require('@octokit/rest'); const GitRepo = require('./git-repo'); +const getStackTrace = require('./stacktrace')('GitHubApi'); class GitHubApi { constructor() { @@ -162,15 +164,22 @@ class GitHubApi { */ async createStatusUnthrottled_({state, targetUrl, description = undefined}) { const sha = process.env.TRAVIS_PULL_REQUEST_SHA || await this.gitRepo_.getFullCommitHash(); - return await this.octokit_.repos.createStatus({ - owner: 'material-components', - repo: 'material-components-web', - sha, - state, - target_url: targetUrl, - description, - context: 'screenshot-test/butter-bot', - }); + let stackTrace; + + try { + stackTrace = getStackTrace('createStatusUnthrottled_'); + return await this.octokit_.repos.createStatus({ + owner: 'material-components', + repo: 'material-components-web', + sha, + state, + target_url: targetUrl, + description, + context: 'screenshot-test/butter-bot', + }); + } catch (err) { + throw new VError(err, `Failed to set commit status:\n${stackTrace}`); + } } /** @@ -180,13 +189,21 @@ class GitHubApi { async getPullRequestNumber(branch = undefined) { branch = branch || await this.gitRepo_.getBranchName(); - const allPRs = await this.octokit_.pullRequests.getAll({ - owner: 'material-components', - repo: 'material-components-web', - per_page: 100, - }); + let allPrsResponse; + let stackTrace; - const filteredPRs = allPRs.data.filter((pr) => pr.head.ref === branch); + try { + stackTrace = getStackTrace('getPullRequestNumber'); + allPrsResponse = await this.octokit_.pullRequests.getAll({ + owner: 'material-components', + repo: 'material-components-web', + per_page: 100, + }); + } catch (err) { + throw new VError(err, `Failed to get pull request number for branch "${branch}":\n${stackTrace}`); + } + + const filteredPRs = allPrsResponse.data.filter((pr) => pr.head.ref === branch); const pr = filteredPRs[0]; return pr ? pr.number : null; @@ -198,12 +215,21 @@ class GitHubApi { */ async getPullRequestFiles(prNumber) { /** @type {!github.proto.PullRequestFileResponse} */ - const fileResponse = await this.octokit_.pullRequests.getFiles({ - owner: 'material-components', - repo: 'material-components-web', - number: prNumber, - per_page: 300, - }); + let fileResponse; + let stackTrace; + + try { + stackTrace = getStackTrace('getPullRequestFiles'); + fileResponse = await this.octokit_.pullRequests.getFiles({ + owner: 'material-components', + repo: 'material-components-web', + number: prNumber, + per_page: 300, + }); + } catch (err) { + throw new VError(err, `Failed to get file list for PR #${prNumber}:\n${stackTrace}`); + } + return fileResponse.data; } @@ -212,15 +238,25 @@ class GitHubApi { * @return {!Promise} */ async getPullRequestBaseBranch(prNumber) { - const prResponse = await this.octokit_.pullRequests.get({ - owner: 'material-components', - repo: 'material-components-web', - number: prNumber, - }); + let prResponse; + let stackTrace; + + try { + stackTrace = getStackTrace('getPullRequestBaseBranch'); + prResponse = await this.octokit_.pullRequests.get({ + owner: 'material-components', + repo: 'material-components-web', + number: prNumber, + }); + } catch (err) { + throw new VError(err, `Failed to get the base branch for PR #${prNumber}:\n${stackTrace}`); + } + if (!prResponse.data) { const serialized = JSON.stringify(prResponse, null, 2); throw new Error(`Unable to fetch data for GitHub PR #${prNumber}:\n${serialized}`); } + return `origin/${prResponse.data.base.ref}`; } @@ -230,12 +266,19 @@ class GitHubApi { * @return {!Promise<*>} */ async createPullRequestComment({prNumber, comment}) { - return this.octokit_.issues.createComment({ - owner: 'material-components', - repo: 'material-components-web', - number: prNumber, - body: comment, - }); + let stackTrace; + + try { + stackTrace = getStackTrace('createPullRequestComment'); + return await this.octokit_.issues.createComment({ + owner: 'material-components', + repo: 'material-components-web', + number: prNumber, + body: comment, + }); + } catch (err) { + throw new VError(err, `Failed to create comment on PR #${prNumber}:\n${stackTrace}`); + } } } diff --git a/test/screenshot/infra/lib/local-storage.js b/test/screenshot/infra/lib/local-storage.js index 1f0e12ca6bf..2c6fc255839 100644 --- a/test/screenshot/infra/lib/local-storage.js +++ b/test/screenshot/infra/lib/local-storage.js @@ -108,7 +108,7 @@ class LocalStorage { */ async readTextFile(filePath) { try { - return fs.readFile(filePath, {encoding: 'utf8'}); + return await fs.readFile(filePath, {encoding: 'utf8'}); } catch (err) { throw new VError(err, `Failed to run LocalStorage.readTextFile(${filePath})`); } @@ -116,14 +116,10 @@ class LocalStorage { /** * @param {string} filePath - * @return {!Promise} + * @return {string} */ readTextFileSync(filePath) { - try { - return fs.readFileSync(filePath, {encoding: 'utf8'}); - } catch (err) { - throw new VError(err, `Failed to run LocalStorage.readTextFileSync(${filePath})`); - } + return fs.readFileSync(filePath, {encoding: 'utf8'}); } /** @@ -133,7 +129,7 @@ class LocalStorage { */ async readBinaryFile(filePath, encoding = null) { try { - return fs.readFile(filePath, {encoding}); + return await fs.readFile(filePath, {encoding}); } catch (err) { throw new VError(err, `Failed to run LocalStorage.readBinaryFile(${filePath}, ${encoding})`); } @@ -170,7 +166,7 @@ class LocalStorage { */ async copy(src, dest) { try { - return fsx.copy(src, dest); + return await fsx.copy(src, dest); } catch (err) { throw new VError(err, `Failed to run LocalStorage.copy(${src}, ${dest})`); } @@ -182,7 +178,7 @@ class LocalStorage { */ async delete(pathPatterns) { try { - return del(pathPatterns); + return await del(pathPatterns); } catch (err) { throw new VError(err, `Failed to run LocalStorage.delete(${pathPatterns} patterns)`); } @@ -194,7 +190,7 @@ class LocalStorage { */ async exists(filePath) { try { - return fs.exists(filePath); + return await fs.exists(filePath); } catch (err) { throw new VError(err, `Failed to run LocalStorage.exists(${filePath})`); } diff --git a/test/screenshot/infra/lib/stacktrace.js b/test/screenshot/infra/lib/stacktrace.js new file mode 100644 index 00000000000..fbfe5741d1b --- /dev/null +++ b/test/screenshot/infra/lib/stacktrace.js @@ -0,0 +1,145 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {!CliColor} */ +const colors = require('colors'); + +/** + * @param {string} className + * @return {function(methodName: string): string} + */ +module.exports = function(className) { + /** + * @param {string} methodName + * @return {string} + */ + function getStackTrace(methodName) { + const fullStack = new Error(`${className}.${methodName}()`).stack; + // Remove THIS function from the stack trace because it's not useful + return fullStack.split('\n').filter((line, index) => index !== 1).join('\n'); + } + + return getStackTrace; +}; + +module.exports.formatError = formatError; + +/** + * @param {!Error|!VError|*} err + * @return {string} + */ +function formatError(err) { + return formatErrorInternal(err) + .replace(/^([^\n]+)/, (fullMatch, line) => colors.bold.red(line)) + ; +} + +/** + * @param {!Error|!VError|*} err + * @return {string} + */ +function formatErrorInternal(err) { + const parentStr = stringifyError(err); + if (err.jse_cause) { + const childStr = formatError(err.jse_cause); + return `${childStr}\n\n${colors.italic('called from:')}\n\n${parentStr}`; + } + return parentStr; +} + +/** + * @param {!Error|!VError|*} err + * @return {string} + */ +function stringifyError(err) { + if (err.toString !== Error.prototype.toString) { + return sanitizeErrorString(err.toString()); + } + + const lines = [err.code, err.message, err.stack].filter((str) => Boolean(str)); + return sanitizeErrorString(lines.join('\n')); +} + +/** + * @param {string} errorStr + * @return {string} + */ +function sanitizeErrorString(errorStr) { + const lines = errorStr.replace(/^((VError|Error|):\s*)+/i, '').split('\n'); + if (lines[1] && lines[1].includes(lines[0].replace(/\(\):?$/, ''))) { + lines.splice(0, 1); + } + return lines + .map((line) => { + let formatted = line; + formatted = formatClassMethod(formatted); + formatted = formatNamedFunction(formatted); + formatted = formatAnonymousFunction(formatted); + return formatted; + }) + .join('\n') + .replace(/^ +at +/, '') + ; +} + +/** + * @param {string} errorLine + * @return {string} + */ +function formatClassMethod(errorLine) { + return errorLine + .replace(/^( +)(at) (\w+)\.(\w+)(.+)$/, (fullMatch, leadingSpaces, atPrefix, className, methodName, rest) => { + if (className === 'process' && methodName === '_tickCallback') { + return colors.dim(fullMatch); + } + rest = formatFileNameAndLineNumber(rest); + return `${leadingSpaces}${atPrefix} ${colors.underline(className)}.${colors.bold(methodName)}${rest}`; + }) + ; +} + +/** + * @param {string} errorLine + * @return {string} + */ +function formatNamedFunction(errorLine) { + return errorLine + .replace(/^( +)(at) (\w+)([^.].+)$/, (fullMatch, leadingSpaces, atPrefix, functionName, rest) => { + rest = formatFileNameAndLineNumber(rest); + return `${leadingSpaces}${atPrefix} ${colors.bold(functionName)}${rest}`; + }) + ; +} + +/** + * @param {string} errorLine + * @return {string} + */ +function formatAnonymousFunction(errorLine) { + return errorLine.replace(/^ +at .*$/, (fullMatch) => { + return colors.dim(fullMatch); + }); +} + +/** + * @param {string} errorLine + * @return {string} + */ +function formatFileNameAndLineNumber(errorLine) { + return errorLine.replace(/\/([^/]+\.\w+):(\d+):(\d+)(\).*)$/, (fullMatch, fileName, lineNumber, colNumber, rest) => { + return `/${colors.underline(fileName)}:${colors.bold(lineNumber)}:${colNumber}${rest}`; + }); +} diff --git a/test/screenshot/run.js b/test/screenshot/run.js index 300a135526f..d5cee2e99fa 100644 --- a/test/screenshot/run.js +++ b/test/screenshot/run.js @@ -20,6 +20,7 @@ const Cli = require('./infra/lib/cli'); const CliColor = require('./infra/lib/logger').colors; const Duration = require('./infra/lib/duration'); const {ExitCode} = require('./infra/lib/constants'); +const {formatError} = require('./infra/lib/stacktrace'); const COMMANDS = { get approve() { @@ -71,7 +72,7 @@ async function runAsync() { } }, (err) => { - console.error('\n\n' + CliColor.bold.red('ERROR:'), err); + console.error('\n\n' + CliColor.bold.red('ERROR:'), formatError(err)); process.exit(ExitCode.UNKNOWN_ERROR); } ); From 226d332d97e82671d202657389eb34e365f9cb3b Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 24 Jul 2018 23:31:14 -0700 Subject: [PATCH 49/53] chore(infrastructure): Build external PRs (#3205) ### What it does - Runs `npm run build` on external PRs - Screenshot tests no longer exit before building - Only runs `test/screenshot/infra/commands/travis.sh` on `TEST_SUITE=screenshot` tests - Unit tests will no longer fail fast, and will no longer print a meaningful error message - Skips extracting API credentials and installing/authorizing `gcloud` if required env vars are missing --- .travis.yml | 10 ++-- test/screenshot/infra/commands/test.js | 42 +++++++++++---- test/screenshot/infra/commands/travis.sh | 67 +++++++++++++++--------- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/.travis.yml b/.travis.yml index c1779d7a86e..9cd98363fc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,11 +35,11 @@ matrix: git: depth: 200 script: npm run screenshot:test -- --no-fetch + before_install: + # Source the script to run it in the same shell process. This ensures that any environment variables set by the + # script are visible to subsequent Travis CLI commands. + # https://superuser.com/a/176788/62792 + - source test/screenshot/infra/commands/travis.sh install: - npm install #- npm ls # Noisy output, but useful for debugging npm package dependency version issues -before_install: - # Source the script to run it in the same shell process. This ensures that any environment variables set by the - # script are visible to subsequent Travis CLI commands. - # https://superuser.com/a/176788/62792 - - source test/screenshot/infra/commands/travis.sh diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 2264237dd7d..591ef012e7a 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -49,6 +49,11 @@ class TestCommand { async runAsync() { await this.build_(); + if (this.isExternalPr_()) { + this.logExternalPr_(); + return ExitCode.OK; + } + /** @type {!mdc.proto.DiffBase} */ const snapshotDiffBase = await this.diffBaseParser_.parseGoldenDiffBase(); const snapshotGitRev = snapshotDiffBase.git_revision; @@ -359,16 +364,6 @@ ${listItemMarkdown} return untrimmed.trim().replace(/>\n *<'); } - /** - * @return {string} - * @private - */ - getCongratulatoryMarkdown_() { - return ` -### No diffs! 💯🎉 -`; - } - /** * @param {!mdc.proto.ReportData} reportData * @return {!ExitCode|number} @@ -387,12 +382,39 @@ ${listItemMarkdown} return ExitCode.OK; } + /** + * @return {boolean} + * @private + */ + isExternalPr_() { + return Boolean( + process.env.TRAVIS_PULL_REQUEST_SLUG && + !process.env.TRAVIS_PULL_REQUEST_SLUG.startsWith('material-components/') + ); + } + + /** + * @private + */ + logExternalPr_() { + this.logger_.warn(` + +${CliColor.bold.red('Screenshot tests are not supported on external PRs for security reasons.')} + +See ${CliColor.underline('https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions')} +for more information. + +${CliColor.bold.red('Skipping screenshot tests.')} +`); + } + /** * @param {number} prNumber * @private */ logUntestablePr_(prNumber) { this.logger_.warn(` + ${CliColor.underline(`PR #${prNumber}`)} does not contain any testable source file changes. ${CliColor.bold.green('Skipping screenshot tests.')} diff --git a/test/screenshot/infra/commands/travis.sh b/test/screenshot/infra/commands/travis.sh index c7b6c338d3d..a91db094439 100755 --- a/test/screenshot/infra/commands/travis.sh +++ b/test/screenshot/infra/commands/travis.sh @@ -1,22 +1,14 @@ #!/usr/bin/env bash -function print_stderr() { +function print_to_stderr() { echo "$@" >&2 } function log_error() { - print_stderr -e "\033[31m\033[1m$@\033[0m" -} - -function exit_if_external_pr() { - if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then - echo - log_error "ERROR: $TEST_SUITE tests are not supported on external PRs for security reasons." - print_stderr - print_stderr "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions" - print_stderr - log_error "Skipping $TEST_SUITE tests." - exit 10 + if [[ $# -gt 0 ]]; then + print_to_stderr -e "\033[31m\033[1m$@\033[0m" + else + print_to_stderr fi } @@ -26,17 +18,27 @@ function print_travis_env_vars() { echo } -function extract_api_credentials() { - openssl aes-256-cbc -K $encrypted_eead2343bb54_key -iv $encrypted_eead2343bb54_iv \ +function maybe_extract_api_credentials() { + if [[ -z "$encrypted_eead2343bb54_key" ]] || [[ -z "$encrypted_eead2343bb54_iv" ]]; then + log_error + log_error "Missing decryption keys for API credentials." + log_error + + if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then + log_error "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions for more information" + log_error + fi + + return + fi + + openssl aes-256-cbc -K "$encrypted_eead2343bb54_key" -iv "$encrypted_eead2343bb54_iv" \ -in test/screenshot/infra/auth/travis.tar.enc -out test/screenshot/infra/auth/travis.tar -d tar -xf test/screenshot/infra/auth/travis.tar -C test/screenshot/infra/auth/ } -function install_google_cloud_sdk() { - export PATH=$PATH:$HOME/google-cloud-sdk/bin - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - +function install_and_authorize_gcloud_sdk() { which gcloud 2>&1 > /dev/null if [[ $? == 0 ]]; then @@ -61,11 +63,26 @@ function install_google_cloud_sdk() { gcloud components update gsutil } -if [[ "$TEST_SUITE" == 'unit' ]]; then - exit_if_external_pr -elif [[ "$TEST_SUITE" == 'screenshot' ]]; then - exit_if_external_pr +function maybe_install_gcloud_sdk() { + export PATH=$PATH:$HOME/google-cloud-sdk/bin + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + if [[ -f test/screenshot/infra/auth/gcs.json ]]; then + install_and_authorize_gcloud_sdk + else + log_error + log_error "Missing Google Cloud credentials file: test/screenshot/infra/auth/gcs.json" + log_error + + if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then + log_error "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions for more information" + log_error + fi + fi +} + +if [[ "$TEST_SUITE" == 'screenshot' ]]; then print_travis_env_vars - extract_api_credentials - install_google_cloud_sdk + maybe_extract_api_credentials + maybe_install_gcloud_sdk fi From 23ed05fc7da3e834745763bc7199bb6803b578a5 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 24 Jul 2018 23:31:14 -0700 Subject: [PATCH 50/53] chore(infrastructure): Build external PRs (#3205) - Runs `npm run build` on external PRs - Screenshot tests no longer exit before building - Only runs `test/screenshot/infra/commands/travis.sh` on `TEST_SUITE=screenshot` tests - Unit tests will no longer fail fast, and will no longer print a meaningful error message - Skips extracting API credentials and installing/authorizing `gcloud` if required env vars are missing Fixes #3189 --- .travis.yml | 10 ++-- test/screenshot/infra/commands/test.js | 42 +++++++++++---- test/screenshot/infra/commands/travis.sh | 67 +++++++++++++++--------- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/.travis.yml b/.travis.yml index c1779d7a86e..9cd98363fc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,11 +35,11 @@ matrix: git: depth: 200 script: npm run screenshot:test -- --no-fetch + before_install: + # Source the script to run it in the same shell process. This ensures that any environment variables set by the + # script are visible to subsequent Travis CLI commands. + # https://superuser.com/a/176788/62792 + - source test/screenshot/infra/commands/travis.sh install: - npm install #- npm ls # Noisy output, but useful for debugging npm package dependency version issues -before_install: - # Source the script to run it in the same shell process. This ensures that any environment variables set by the - # script are visible to subsequent Travis CLI commands. - # https://superuser.com/a/176788/62792 - - source test/screenshot/infra/commands/travis.sh diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index 2264237dd7d..591ef012e7a 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -49,6 +49,11 @@ class TestCommand { async runAsync() { await this.build_(); + if (this.isExternalPr_()) { + this.logExternalPr_(); + return ExitCode.OK; + } + /** @type {!mdc.proto.DiffBase} */ const snapshotDiffBase = await this.diffBaseParser_.parseGoldenDiffBase(); const snapshotGitRev = snapshotDiffBase.git_revision; @@ -359,16 +364,6 @@ ${listItemMarkdown} return untrimmed.trim().replace(/>\n *<'); } - /** - * @return {string} - * @private - */ - getCongratulatoryMarkdown_() { - return ` -### No diffs! 💯🎉 -`; - } - /** * @param {!mdc.proto.ReportData} reportData * @return {!ExitCode|number} @@ -387,12 +382,39 @@ ${listItemMarkdown} return ExitCode.OK; } + /** + * @return {boolean} + * @private + */ + isExternalPr_() { + return Boolean( + process.env.TRAVIS_PULL_REQUEST_SLUG && + !process.env.TRAVIS_PULL_REQUEST_SLUG.startsWith('material-components/') + ); + } + + /** + * @private + */ + logExternalPr_() { + this.logger_.warn(` + +${CliColor.bold.red('Screenshot tests are not supported on external PRs for security reasons.')} + +See ${CliColor.underline('https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions')} +for more information. + +${CliColor.bold.red('Skipping screenshot tests.')} +`); + } + /** * @param {number} prNumber * @private */ logUntestablePr_(prNumber) { this.logger_.warn(` + ${CliColor.underline(`PR #${prNumber}`)} does not contain any testable source file changes. ${CliColor.bold.green('Skipping screenshot tests.')} diff --git a/test/screenshot/infra/commands/travis.sh b/test/screenshot/infra/commands/travis.sh index c7b6c338d3d..a91db094439 100755 --- a/test/screenshot/infra/commands/travis.sh +++ b/test/screenshot/infra/commands/travis.sh @@ -1,22 +1,14 @@ #!/usr/bin/env bash -function print_stderr() { +function print_to_stderr() { echo "$@" >&2 } function log_error() { - print_stderr -e "\033[31m\033[1m$@\033[0m" -} - -function exit_if_external_pr() { - if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then - echo - log_error "ERROR: $TEST_SUITE tests are not supported on external PRs for security reasons." - print_stderr - print_stderr "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions" - print_stderr - log_error "Skipping $TEST_SUITE tests." - exit 10 + if [[ $# -gt 0 ]]; then + print_to_stderr -e "\033[31m\033[1m$@\033[0m" + else + print_to_stderr fi } @@ -26,17 +18,27 @@ function print_travis_env_vars() { echo } -function extract_api_credentials() { - openssl aes-256-cbc -K $encrypted_eead2343bb54_key -iv $encrypted_eead2343bb54_iv \ +function maybe_extract_api_credentials() { + if [[ -z "$encrypted_eead2343bb54_key" ]] || [[ -z "$encrypted_eead2343bb54_iv" ]]; then + log_error + log_error "Missing decryption keys for API credentials." + log_error + + if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then + log_error "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions for more information" + log_error + fi + + return + fi + + openssl aes-256-cbc -K "$encrypted_eead2343bb54_key" -iv "$encrypted_eead2343bb54_iv" \ -in test/screenshot/infra/auth/travis.tar.enc -out test/screenshot/infra/auth/travis.tar -d tar -xf test/screenshot/infra/auth/travis.tar -C test/screenshot/infra/auth/ } -function install_google_cloud_sdk() { - export PATH=$PATH:$HOME/google-cloud-sdk/bin - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - +function install_and_authorize_gcloud_sdk() { which gcloud 2>&1 > /dev/null if [[ $? == 0 ]]; then @@ -61,11 +63,26 @@ function install_google_cloud_sdk() { gcloud components update gsutil } -if [[ "$TEST_SUITE" == 'unit' ]]; then - exit_if_external_pr -elif [[ "$TEST_SUITE" == 'screenshot' ]]; then - exit_if_external_pr +function maybe_install_gcloud_sdk() { + export PATH=$PATH:$HOME/google-cloud-sdk/bin + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + if [[ -f test/screenshot/infra/auth/gcs.json ]]; then + install_and_authorize_gcloud_sdk + else + log_error + log_error "Missing Google Cloud credentials file: test/screenshot/infra/auth/gcs.json" + log_error + + if [[ -n "$TRAVIS_PULL_REQUEST_SLUG" ]] && [[ ! "$TRAVIS_PULL_REQUEST_SLUG" =~ ^material-components/ ]]; then + log_error "See https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions for more information" + log_error + fi + fi +} + +if [[ "$TEST_SUITE" == 'screenshot' ]]; then print_travis_env_vars - extract_api_credentials - install_google_cloud_sdk + maybe_extract_api_credentials + maybe_install_gcloud_sdk fi From 66d03ca0f5e298f0a6ccfffb29c2458802579e8d Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Tue, 24 Jul 2018 23:52:59 -0700 Subject: [PATCH 51/53] docs(infrastructure): Add section on creating new screenshot test pages (#3199) - Adds "Creating new screenshot tests" section - Adds instructions to run `gcloud auth login` Fixes #3185 --- test/screenshot/README.md | 103 +++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 17 deletions(-) diff --git a/test/screenshot/README.md b/test/screenshot/README.md index b3b39c16764..be4eefa961d 100644 --- a/test/screenshot/README.md +++ b/test/screenshot/README.md @@ -6,6 +6,9 @@ Prevent visual regressions by running screenshot tests on every PR. ### API credentials +CBT credentials can be found on the [CrossBrowserTesting.com > Account](https://crossbrowsertesting.com/account) page. \ +Your `Authkey` is listed under the `User Profile` section. + Add the following to your `~/.bash_profile` or `~/.bashrc` file: ```bash @@ -13,12 +16,17 @@ export MDC_CBT_USERNAME='you@example.com' export MDC_CBT_AUTHKEY='example' ``` -Credentials can be found here: +Make the env vars available to existing terminal(s): + +```bash +[[ -f ~/.bash_profile ]] && source ~/.bash_profile || source ~/.bashrc +``` -* [CrossBrowserTesting.com > Account](https://crossbrowsertesting.com/account) \ - `Authkey` is listed under the `User Profile` section -* [Google Cloud Console > IAM & admin > Service accounts](https://console.cloud.google.com/iam-admin/serviceaccounts?project=material-components-web) \ - Click the `︙` icon on the right side of the service account, then choose `Create key` +Then authorize your GCP account: + +```bash +gcloud auth login +``` ### Test your changes @@ -45,6 +53,18 @@ https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/14/03_37 ## Basic usage +### Local dev server + +The deprecated `npm run dev` command has been replaced by: + +```bash +npm start +``` + +Open http://localhost:8080/ in your browser to view the test pages. + +Source files are automatically recompiled when they change. + ### Updating "golden" screenshots On the @@ -98,18 +118,6 @@ These options are treated as regular expressions, so partial matches are possibl See `test/screenshot/browser.json` for the full list of supported browsers. -### Local dev server - -The deprecated `npm run dev` command has been replaced by: - -```bash -npm start -``` - -Open http://localhost:8080/ in your browser to view the test pages. - -Source files are automatically recompiled when they change. - ## Advanced usage Use `--help` to see all available CLI options: @@ -124,6 +132,67 @@ npm run screenshot:test -- --help **IMPORTANT:** Note the `--` between the script name and its arguments. This is required by `npm`. +### Creating new screenshot tests + +The easiest way to create new screenshot tests is to copy an existing test page or directory, and modify it for your +component. + +For example, to create tests for `mdc-radio`, start by copying and renaming the `mdc-checkbox` tests: + +```bash +cp -r test/screenshot/spec/mdc-{checkbox,radio} +sed -i '' 's/-checkbox/-radio/g' test/screenshot/spec/mdc-radio/*.* test/screenshot/spec/mdc-radio/*/*.* +vim ... +``` + +#### Component sizes + +There are two types of components: + +1. **Large** fullpage components (dialog, drawer, top app bar, etc.) +2. **Small** widget components (button, checkbox, linear progress, etc.) + +Test pages for **small** components must have a `test-main--mobile-viewport` class on the `
    ` element: + +```html +
    +``` + +This class ensures that all components on the page fit inside an "average" mobile viewport without scrolling. +This is necessary because most browsers' WebDriver implementations do not support taking screenshots of the entire +`document`. + +Test pages for **large** components, however, must _not_ use the `--mobile-viewport` class: + +```html +
    +``` + +For **small** components, you also need to specify the dimensions of the `test-cell--FOO` class in your component's +`fixture.scss` file: + +```css +.test-cell--button { + width: 171px; + height: 71px; +} +``` + +The dimensions should be large enough to fit all variants of your component, with an extra ~`10px` or so of wiggle room. +This prevents noisy diffs in the event that your component's `height` or `margin` changes unexpectedly. + +#### CSS classes + +CSS Class | Description +--- | --- +`test-main` | Mandatory. Wraps all page content. +`test-main--mobile-viewport` | Mandatory (**small** components only). Ensures that all page content fits in a mobile viewport. +`test-cell--` | Mandatory (**small** components only). Sets the dimensions of cells in the grid. +`custom---` | Mandatory (mixin test pages only). Calls a single Sass theme mixin. + +\* _`` is the name of the component, minus the `mdc-` prefix. E.g.: `radio`, `top-app-bar`._ \ +\* _`` is the name of the Sass mixin, minus the `mdc--` prefix. E.g.: `container-fill-color`._ + ### Public demos ```bash From ca1528ccd10a5edaa8a31d91996a1e47d3211387 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Wed, 25 Jul 2018 01:20:40 -0700 Subject: [PATCH 52/53] chore(infrastructure): Run unit tests for external PRs in Chrome headless (#3206) Fixes #2815 https://travis-ci.org/material-components/material-components-web/jobs/407944719 ![image](https://user-images.githubusercontent.com/409245/43186623-17d7fca0-8fa4-11e8-991f-b08a01f330fc.png) --- karma.conf.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index d409833517c..169f0f63105 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -21,6 +21,16 @@ const USING_TRAVISCI = Boolean(process.env.TRAVIS); const USING_SL = Boolean(process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY); const SL_LAUNCHERS = { + /* + * Chrome (headless) + */ + + 'ChromeHeadlessNoSandbox': { + base: 'ChromeHeadless', + // See https://github.com/travis-ci/travis-ci/issues/8836#issuecomment-348248951 + flags: ['--no-sandbox'], + }, + /* * Chrome (desktop) */ @@ -208,5 +218,5 @@ module.exports = function(config) { }; function determineBrowsers() { - return USING_SL ? Object.keys(SL_LAUNCHERS) : ['Chrome']; + return USING_SL ? Object.keys(SL_LAUNCHERS) : ['ChromeHeadlessNoSandbox']; } From 42c87295712b7539ae1985c57496d4403b803c51 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Wed, 25 Jul 2018 02:04:22 -0700 Subject: [PATCH 53/53] chore(infrastructure): Colorize `npm start` output (#3207) ### What it does - Improves console output of `npm start` and `npm run screenshot:serve` commands: - Makes the top and bottom `===` bars green, and makes their length dynamic based on port number length - Bolds/underlines the URL ### Example output ![image](https://user-images.githubusercontent.com/409245/43190163-4443462e-8fad-11e8-9336-01447cceda33.png) --- test/screenshot/infra/commands/serve.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/screenshot/infra/commands/serve.js b/test/screenshot/infra/commands/serve.js index 000f90953be..0762a80c25a 100644 --- a/test/screenshot/infra/commands/serve.js +++ b/test/screenshot/infra/commands/serve.js @@ -16,6 +16,8 @@ 'use strict'; +/** @type {!CliColor} */ +const colors = require('colors'); const detectPort = require('detect-port'); const express = require('express'); const serveIndex = require('serve-index'); @@ -42,11 +44,16 @@ class ServeCommand { app.use('/', express.static(TEST_DIR_RELATIVE_PATH), serveIndex(TEST_DIR_RELATIVE_PATH)); app.listen(port, () => { - console.log(` -========================================================== -Local development server running on http://localhost:${port}/ -========================================================== -`); + const urlPlain = `http://localhost:${port}/`; + const urlColor = colors.bold.underline(urlPlain); + const noticePlain = `Local development server running on ${urlPlain}`; + const noticeColor = `Local development server running on ${urlColor}`; + const borderColor = colors.green(''.padStart(noticePlain.length, '=')); + console.log((` +${borderColor} +${noticeColor} +${borderColor} +`)); }); } }