diff --git a/.eslintignore b/.eslintignore index 747746059..72c56284a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -22,3 +22,6 @@ /build /material /site + +# Gemini reports +/gemini-report diff --git a/.gemini.yml b/.githooks/pre-commit/branch.sh old mode 100644 new mode 100755 similarity index 78% rename from .gemini.yml rename to .githooks/pre-commit/branch.sh index 2a068b35b..1ca662f87 --- a/.gemini.yml +++ b/.githooks/pre-commit/branch.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # Copyright (c) 2016-2017 Martin Donath # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,11 +20,15 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -rootUrl: http://localhost:8000/ -gridUrl: http://localhost:4444/wd/hub +# Determine current branch +BRANCH=`git rev-parse --abbrev-ref HEAD` +echo -n "Hook[pre-commit]: Checking branch..." -# Browsers to run tests on -browsers: - chrome: - desiredCapabilities: - browserName: chrome +# If we're on master, abort commit +if [[ "$BRANCH" == "master" ]]; then + echo "Commits on master are only allowed via Pull Requests. Aborting." + exit 1 +fi + +# We're good +exit 0 diff --git a/.gitignore b/.gitignore index c6cfbd09f..999388163 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ /MANIFEST /site +# Gemini reports +/gemini-report + # Distribution files /dist /mkdocs_material.egg-info diff --git a/Gulpfile.babel.js b/Gulpfile.babel.js index 5fefcc60a..64c581b88 100755 --- a/Gulpfile.babel.js +++ b/Gulpfile.babel.js @@ -35,7 +35,10 @@ const config = { src: "src/assets", /* Source directory for assets */ build: "material/assets" /* Target directory for assets */ }, - lib: "lib", /* Libraries */ + lib: "lib", /* Libraries and tasks */ + tests: { + visual: "tests/visual" /* Visual regression tests */ + }, views: { src: "src", /* Source directory for views */ build: "material" /* Target directory for views */ @@ -228,12 +231,11 @@ gulp.task("assets:clean", [ * Minify views */ gulp.task("views:build", (args.revision ? [ - "assets:images:build", - "assets:stylesheets:build", - "assets:javascripts:build" + "assets:build" ] : []).concat(args.clean ? [ "views:clean" -] : []), load("views/build")) +] : []), + load("views/build")) /* * Clean views @@ -270,11 +272,19 @@ gulp.task("mkdocs:serve", * Tests * ------------------------------------------------------------------------- */ +/* + * Run visual tests + */ +gulp.task("tests:visual:run", [ + // "assets:build", + // "views:build" +], load("tests/visual/run")) + /* * Start karma test runner */ gulp.task("tests:unit:watch", - load("tests/unit/watch")) + () => {}) /* ---------------------------------------------------------------------------- * Interface @@ -286,9 +296,9 @@ gulp.task("tests:unit:watch", gulp.task("build", [ "assets:build", "views:build" -].concat(args.mkdocs - ? "mkdocs:build" - : [])) +].concat(args.mkdocs ? [ + "mkdocs:build" +] : [])) /* * Clean assets and documentation diff --git a/lib/servers/selenium.js b/lib/servers/selenium.js index f8df471c6..641237612 100644 --- a/lib/servers/selenium.js +++ b/lib/servers/selenium.js @@ -52,10 +52,6 @@ export const start = done => { } } - /* Register signal handler for all relevant events */ - for (const signal of ["SIGTERM", "SIGINT", "exit"]) - process.on(signal, stop) - /* Remember process handle */ server = server || proc done() @@ -66,3 +62,10 @@ export const stop = () => { if (server) server.kill() } + +/* ---------------------------------------------------------------------------- + * Signals + * ------------------------------------------------------------------------- */ + +for (const signal of ["SIGTERM", "SIGINT", "exit"]) + process.on(signal, stop) diff --git a/lib/tasks/assets/javascripts/lint.js b/lib/tasks/assets/javascripts/lint.js index ffcd5a06b..f71250ecb 100644 --- a/lib/tasks/assets/javascripts/lint.js +++ b/lib/tasks/assets/javascripts/lint.js @@ -23,6 +23,7 @@ import path from "path" import through from "through2" import util from "gulp-util" + import { CLIEngine } from "eslint" /* ---------------------------------------------------------------------------- diff --git a/lib/tasks/assets/stylesheets/build.js b/lib/tasks/assets/stylesheets/build.js index 2f78f1843..622001788 100644 --- a/lib/tasks/assets/stylesheets/build.js +++ b/lib/tasks/assets/stylesheets/build.js @@ -25,6 +25,7 @@ import gulpif from "gulp-if" import mincss from "gulp-cssnano" import mqpacker from "css-mqpacker" import postcss from "gulp-postcss" +import pseudoclasses from "postcss-pseudo-classes" import rev from "gulp-rev" import sass from "gulp-sass" import sourcemaps from "gulp-sourcemaps" @@ -54,7 +55,9 @@ export default (gulp, config, args) => { postcss([ autoprefixer(), mqpacker - ])) + ].concat(!args.optimize ? [ + pseudoclasses() + ] : []))) /* Minify sources */ .pipe(gulpif(args.optimize, mincss())) diff --git a/lib/tasks/mkdocs/serve.js b/lib/tasks/mkdocs/serve.js index 102c2c8b9..14ace0aad 100644 --- a/lib/tasks/mkdocs/serve.js +++ b/lib/tasks/mkdocs/serve.js @@ -39,7 +39,7 @@ export default () => { server.kill() /* Spawn MkDocs server */ - server = child.spawn("mkdocs", ["serve", "-a", "0.0.0.0:8000"], { + server = child.spawn("mkdocs", ["serve", "--dev-addr", "0.0.0.0:8000"], { stdio: "inherit" }) } diff --git a/lib/tasks/tests/visual/run.js b/lib/tasks/tests/visual/run.js new file mode 100644 index 000000000..4cc8d2810 --- /dev/null +++ b/lib/tasks/tests/visual/run.js @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-2017 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import child from "child_process" +import path from "path" +import * as selenium from "~/lib/servers/selenium" + +import Gemini from "gemini" + +/* ---------------------------------------------------------------------------- + * Task: start test runner + * ------------------------------------------------------------------------- */ + +/* MkDocs server */ +let server = null + +/* ---------------------------------------------------------------------------- + * Task: start test runner + * ------------------------------------------------------------------------- */ + +export default (gulp, config) => { + return () => { + + /* Start MkDocs server */ + return new Promise(resolve => { + server = child.spawn("mkdocs", [ + "serve", "--dev-addr", "127.0.0.1:8000" + ], { + cwd: config.tests.visual, + stdio: [process.stdin, process.stdout, "pipe"] + }) + + /* Wait for MkDocs server and resolve promise */ + server.stderr.on("data", data => { + if (data.toString().match("Serving")) { + server.stderr.removeAllListeners("data") + resolve() + } + }) + + /* Start Selenium */ + }).then(() => { + return new Promise(resolve => { + selenium.start(() => resolve()) + + /* Start Gemini test runner depending on environment */ + }).then(() => { + const gemini = require(path.join( + process.cwd(), `${config.tests.visual}/.gemini-local.json`)) + return new Gemini(gemini).test("tests/visual/suites", { + reporters: ["html"] + }) + }) + .then(() => { + selenium.stop() + }) + }) + .then(() => { + server.kill() + }) + } +} diff --git a/mkdocs.yml b/mkdocs.yml index e60f188ad..2d9040979 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,7 +31,7 @@ repo_url: https://github.com/squidfunk/mkdocs-material # Copyright copyright: 'Copyright © 2016 Martin Donath' -# Documentation and theme +# Theme directory theme_dir: material # Options diff --git a/package.json b/package.json index f53fda25b..0ed8d2653 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "mocha": "^3.2.0", "modularscale-sass": "^2.1.1", "node-notifier": "^4.6.1", + "postcss-pseudo-classes": "^0.1.0", "selenium-standalone": "^5.9.1", "stylelint": "^7.7.1", "stylelint-config-standard": "^15.0.1", diff --git a/src/assets/stylesheets/layout/_footer.scss b/src/assets/stylesheets/layout/_footer.scss index f30f2aeb3..de0b65449 100644 --- a/src/assets/stylesheets/layout/_footer.scss +++ b/src/assets/stylesheets/layout/_footer.scss @@ -50,8 +50,8 @@ padding-bottom: 0.8rem; transition: opacity 0.25s; - // [mobile landscape +]: Set proportional width - @include break-from-device(mobile landscape) { + // [tablet +]: Set proportional width + @include break-from-device(tablet) { width: 50%; } @@ -68,8 +68,8 @@ // Title .md-footer-nav__title { - // [mobile portrait -]: Hide title for previous page - @include break-to-device(mobile portrait) { + // [mobile -]: Hide title for previous page + @include break-to-device(mobile) { display: none; } } diff --git a/tests/visual/.eslintrc b/tests/visual/.eslintrc new file mode 100644 index 000000000..fab56404b --- /dev/null +++ b/tests/visual/.eslintrc @@ -0,0 +1,8 @@ +{ + "globals": { + "gemini": true + }, + "rules": { + "no-loop-func": 0 + } +} diff --git a/tests/visual/.gemini-local.json b/tests/visual/.gemini-local.json new file mode 100644 index 000000000..706e5f900 --- /dev/null +++ b/tests/visual/.gemini-local.json @@ -0,0 +1,15 @@ +{ + "rootUrl": "http://localhost:8000", + "screenshotsDir": "./tests/visual/baseline", + "browsers": { + "local-chrome": { + "desiredCapabilities": { + "browserName": "chrome" + } + } + }, + "system": { + "projectRoot": "./", + "sourceRoot": "src/assets/stylesheets" + } +} diff --git a/tests/visual/break.json b/tests/visual/break.json new file mode 100644 index 000000000..de9bc12a0 --- /dev/null +++ b/tests/visual/break.json @@ -0,0 +1,39 @@ +{ + "breakpoints": [ + { + "name": "mobile-portrait", + "size": { + "width": 320, + "height": 600 + } + }, + { + "name": "mobile-landscape", + "size": { + "width": 480, + "height": 600 + } + }, + { + "name": "tablet-portrait", + "size": { + "width": 720, + "height": 600 + } + }, + { + "name": "tablet-landscape", + "size": { + "width": 960, + "height": 600 + } + }, + { + "name": "screen", + "size": { + "width": 1220, + "height": 600 + } + } + ] +} diff --git a/tests/visual/fixtures/extensions/admonition.md b/tests/visual/fixtures/extensions/admonition.md new file mode 100644 index 000000000..903bcdee6 --- /dev/null +++ b/tests/visual/fixtures/extensions/admonition.md @@ -0,0 +1,107 @@ +# Admonition Tests + + + +## Default + +!!! note + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +## Format + +### Custom title + +!!! note "Phasellus posuere in sem ut cursus" + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Long title + +!!! note "Phasellus posuere in sem ut cursus. Nullam sit amet tincidunt ipsum, sit amet elementum turpis. Etiam ipsum quam, mattis in purus vitae, lacinia fermentum enim." + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Empty title + +!!! note "" + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +## Types + +### Note + +!!! note + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Summary + +!!! summary + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Tip + +!!! tip + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Success + +!!! success + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Warning + +!!! warning + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Failure + +!!! failure + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Danger + +!!! danger + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. + +### Bug + +!!! bug + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod + nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor + massa, nec semper lorem quam in massa. diff --git a/tests/visual/generate.js b/tests/visual/generate.js new file mode 100644 index 000000000..a462c803a --- /dev/null +++ b/tests/visual/generate.js @@ -0,0 +1,87 @@ + +const config = require("./break.json") + +// TODO: also pass breakpoints to function! + +const generate = components => { + for (const c of Object.keys(components)) { + const component = components[c] + + // TODO: check states and generate a suite for each state! + // TODO: check name variants! + // TODO: build nested suites only once + // TODO: handle waiting/js + + const states = component.states ? component.states : + [{ name: "", wait: 0 }] + + let done = 0 + for (const state of states) { + gemini.suite(`${c}${state.name}`, suite => { + + /* Set URL of page to capture */ + if (component.url) + suite.setUrl(component.url) + + /* Set elements to capture */ + if (component.capture) + suite.setCaptureElements(component.capture) + + // TODO: otherwise throw error + if (component.break) { + const [mode, name] = component.break.split("@") + + // get matching breakpoint. TODO: handle non-existent!!! + const b = config.breakpoints.findIndex(bp => { + return bp.name === name + }) + + // now split according to method + let breakpoints = [] + switch (mode) { + case "": + breakpoints = config.breakpoints.slice(b, b + 1) + break + case "+": + breakpoints = config.breakpoints.slice( + b, config.breakpoints.length + 1) + break + case "-": + breakpoints = config.breakpoints.slice(0, b + 1) + break + } + + // iterate breakpoints + for (const breakpoint of breakpoints) { + suite.capture(`@${breakpoint.name}`, actions => { + actions.setWindowSize( + breakpoint.size.width, breakpoint.size.height) + if (state.wait) + actions.wait(state.wait) + if (state.name) { + // eval, as its executed at the frontend + if (typeof state.name === "string") { + actions.executeJS(new Function(` + document.querySelector( + "${component.capture}" + ).classList.add("${state.name}") + `) + ) + } else { + actions.executeJS(state.name) + } + } + }) + } + } + + // nested suites + if (!done && component.suite) { + done = 1 + generate(component.suite) + } + }) + } + } +} +export default generate diff --git a/tests/visual/mkdocs.yml b/tests/visual/mkdocs.yml new file mode 100644 index 000000000..a4d5de5e0 --- /dev/null +++ b/tests/visual/mkdocs.yml @@ -0,0 +1,75 @@ +# Copyright (c) 2016-2017 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# Project information +site_name: Material for MkDocs Tests +site_description: A Material Design theme for MkDocs +site_author: Martin Donath +site_url: http://squidfunk.github.io/mkdocs-material/ + +# Repository +repo_name: squidfunk/mkdocs-material +repo_url: https://github.com/squidfunk/mkdocs-material + +# Copyright +copyright: 'Copyright © 2016 Martin Donath' + +# Documentation and theme directories +docs_dir: fixtures +theme_dir: ../../material + +# Options +extra: + palette: + primary: indigo + accent: indigo + social: + - type: github-alt + link: https://github.com/squidfunk + - type: twitter + link: https://twitter.com/squidfunk + - type: linkedin + link: https://de.linkedin.com/in/martin-donath-20a95039 + +# Extensions +markdown_extensions: + - markdown.extensions.admonition + - markdown.extensions.codehilite(guess_lang=false) + - markdown.extensions.footnotes + - markdown.extensions.meta + - markdown.extensions.toc(permalink=true) + - pymdownx.arithmatex + - pymdownx.betterem(smart_enable=all) + - pymdownx.caret + - pymdownx.critic + - pymdownx.emoji: + emoji_generator: !!python/name:pymdownx.emoji.to_svg + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tasklist(custom_checkbox=true) + - pymdownx.tilde + +# Page tree +pages: + - Extensions: + - Admonition: extensions/admonition.md diff --git a/tests/visual/suites/extensions/admonition.js b/tests/visual/suites/extensions/admonition.js new file mode 100644 index 000000000..b9c810cf8 --- /dev/null +++ b/tests/visual/suites/extensions/admonition.js @@ -0,0 +1,132 @@ +/* +* Copyright (c) 2016-2017 Martin Donath +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to +* deal in the Software without restriction, including without limitation the +* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +* sell copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +* IN THE SOFTWARE. +*/ + +import generate from "../../generate.js" + +/* ---------------------------------------------------------------------------- + * Tests + * ------------------------------------------------------------------------- */ + +generate({ + + /* + * Admonition block + * + * The admonition block looks the same on everything above tablet + * portrait, so we can save a few testcases. + */ + ".admonition": { + "url": "/extensions/admonition", + "capture": "#default + .admonition", + "break": "-@tablet-portrait", + "suite": { + + /* + * Admonition block with a custom title + */ + "#custom-title": { + "capture": "#custom-title + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with a long title + */ + "#long-title": { + "capture": "#long-title + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with an empty title + */ + "#empty-title": { + "capture": "#empty-title + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "note" + */ + "#note": { + "capture": "#note + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "summary" + */ + "#summary": { + "capture": "#summary + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "tip" + */ + "#tip": { + "capture": "#tip + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "success" + */ + "#success": { + "capture": "#success + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "warning" + */ + "#warning": { + "capture": "#warning + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "failure" + */ + "#failure": { + "capture": "#failure + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "danger" + */ + "#danger": { + "capture": "#danger + .admonition", + "break": "@screen" + }, + + /* + * Admonition block with style "bug" + */ + "#bug": { + "capture": "#bug + .admonition", + "break": "@screen" + } + } + } +})