diff --git a/.travis.yml b/.travis.yml index e4380c2f4..c0e8785d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,6 +40,10 @@ addons: env: - CXX=g++-4.8 +# Limit clone depth to 5, to speed up build +git: + depth: 5 + # Cache dependencies cache: pip: true diff --git a/Gulpfile.babel.js b/Gulpfile.babel.js index 3a56645f0..d08d4285d 100755 --- a/Gulpfile.babel.js +++ b/Gulpfile.babel.js @@ -285,8 +285,8 @@ gulp.task("mkdocs:serve", * Generate visual tests */ gulp.task("tests:visual:generate", [ - "tests:visual:clean" ].concat(args.clean ? [ + "tests:visual:clean", "assets:build", "views:build" ] : []), diff --git a/lib/.eslintrc b/lib/.eslintrc new file mode 100644 index 000000000..bba4224ca --- /dev/null +++ b/lib/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-invalid-this": 0, + "max-params": 0 + } +} diff --git a/lib/servers/ecstatic.js b/lib/servers/ecstatic.js index f3e067e6b..829778729 100644 --- a/lib/servers/ecstatic.js +++ b/lib/servers/ecstatic.js @@ -58,5 +58,8 @@ export const start = (directory, port, done) => { * @param {Function} done - Resolve callback */ export const stop = done => { - server.close(done) + if (server) { + server.close(done) + server = null + } } diff --git a/lib/servers/sauce-connect.js b/lib/servers/sauce-connect.js new file mode 100644 index 000000000..a6d999d81 --- /dev/null +++ b/lib/servers/sauce-connect.js @@ -0,0 +1,71 @@ +/* + * 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 launcher from "sauce-connect-launcher" + +/* ---------------------------------------------------------------------------- + * Locals + * ------------------------------------------------------------------------- */ + +/* SauceConnect process */ +let server = null + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Open SauceConnect tunnel + * + * @param {string} id - Unique identifier + * @param {string} username - SauceConnect username + * @param {string} accesskey - SauceConnect accesskey + * @param {Function} done - Resolve callback + */ +export const start = (id, username, accesskey, done) => { + launcher({ + username, + accessKey: accesskey, + tunnelIdentifier: id + }, (err, proc) => { + if (err) + throw new Error(err) + server = proc + done() + }) + + /* Register signal handlers */ + for (const signal of ["SIGTERM", "SIGINT", "exit"]) + process.on(signal, stop) +} + +/** + * Close SauceConnect tunnel + * + * @param {Function} done - Resolve callback + */ +export const stop = done => { + if (server) { + server.close(done) + server = null + } +} diff --git a/lib/tasks/.eslintrc b/lib/tasks/.eslintrc deleted file mode 100644 index 709b01107..000000000 --- a/lib/tasks/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-invalid-this": 0 - } -} diff --git a/lib/tasks/assets/javascripts/build/application.js b/lib/tasks/assets/javascripts/build/application.js index a697c83a1..486a1ef66 100644 --- a/lib/tasks/assets/javascripts/build/application.js +++ b/lib/tasks/assets/javascripts/build/application.js @@ -101,7 +101,7 @@ export default (gulp, config, args) => { }, /* Sourcemap support */ - devtool: args.sourcemaps ? "source-map" : "" + devtool: args.sourcemaps ? "inline-source-map" : "" })) /* Revisioning */ diff --git a/lib/tasks/assets/stylesheets/build.js b/lib/tasks/assets/stylesheets/build.js index 4f716990e..10ac045bc 100644 --- a/lib/tasks/assets/stylesheets/build.js +++ b/lib/tasks/assets/stylesheets/build.js @@ -68,7 +68,7 @@ export default (gulp, config, args) => { .pipe(gulpif(args.revision, rev())) .pipe(gulpif(args.revision, version({ manifest: gulp.src("manifest.json") }))) - .pipe(gulpif(args.sourcemaps, sourcemaps.write("."))) + .pipe(gulpif(args.sourcemaps, sourcemaps.write())) .pipe(gulp.dest(`${config.assets.build}/stylesheets`)) .pipe(gulpif(args.revision, rev.manifest("manifest.json", { diff --git a/lib/tasks/tests/visual/run.js b/lib/tasks/tests/visual/run.js index 19624a5f3..f955fc652 100644 --- a/lib/tasks/tests/visual/run.js +++ b/lib/tasks/tests/visual/run.js @@ -20,42 +20,22 @@ * IN THE SOFTWARE. */ +import moniker from "moniker" import path from "path" import * as ecstatic from "~/lib/servers/ecstatic" +import * as sauce from "~/lib/servers/sauce-connect" import * as selenium from "~/lib/servers/selenium" import Gemini from "gemini" -/* ---------------------------------------------------------------------------- - * Test runner: Selenium - * ------------------------------------------------------------------------- */ - -class SeleniumTestRunner { - - /** - * Start Selenium - * - * @param {Function} done - Resolve callback - */ - start(done) { - selenium.start(done) - } - - /** - * Stop Selenium - * - * @param {Function} done - Resolve callback - */ - stop(done) { - selenium.stop(done) - } -} - /* ---------------------------------------------------------------------------- * Task: run visual tests * ------------------------------------------------------------------------- */ export default (gulp, config, args) => { + const id = process.env.TRAVIS + ? `Travis #${process.env.TRAVIS_BUILD_NUMBER}` + : `Local #${moniker.choose()}` return done => { /* Start static file server */ @@ -65,10 +45,28 @@ export default (gulp, config, args) => { /* Create and start test runner */ }).then(() => { return new Promise((resolve, reject) => { - const runner = new SeleniumTestRunner() - runner.start(err => { - return err ? reject(err) : resolve(runner) - }) + + /* Start SauceConnect tunnel */ + if (process.env.CI || process.env.SAUCE) { + if (!process.env.SAUCE_USERNAME || + !process.env.SAUCE_ACCESS_KEY) + throw new Error( + "SauceConnect: please provide SAUCE_USERNAME " + + "and SAUCE_ACCESS_KEY") + + /* Start tunnel, if credentials are given */ + sauce.start( + id, + process.env.SAUCE_USERNAME, + process.env.SAUCE_ACCESS_KEY, + err => { + return err ? reject(err) : resolve(sauce) + }) + + /* Start Selenium */ + } else { + selenium.start(() => resolve(selenium)) + } }) /* Setup and run Gemini */ @@ -76,8 +74,20 @@ export default (gulp, config, args) => { const gemini = require( path.join(process.cwd(), `${config.tests.visual}/config`, process.env.CI || process.env.SAUCE - ? "gemini.sauce.json" - : "gemini.local.json")) + ? "gemini.sauce-connect.json" + : "gemini.selenium.json")) + + /* Add dynamic configuration to capabilities */ + for (const key of Object.keys(gemini.browsers)) { + const caps = gemini.browsers[key].desiredCapabilities + caps.tunnelIdentifier = id + caps.public = "private" + caps.name = id + + /* Associate build with job when in CI */ + if (process.env.CI && process.env.TRAVIS) + caps.public = "public" + } /* Start Gemini and return runner upon finish */ return new Gemini(gemini).test(`${config.tests.visual}/suites`, { diff --git a/package.json b/package.json index 0d7e0ea9b..6b069dcc4 100644 --- a/package.json +++ b/package.json @@ -86,8 +86,10 @@ "material-shadows": "^3.0.1", "mocha": "^3.2.0", "modularscale-sass": "^2.1.1", + "moniker": "^0.1.2", "node-notifier": "^5.0.0", "postcss-pseudo-classes": "^0.1.0", + "sauce-connect-launcher": "^1.2.0", "selenium-standalone": "^5.9.1", "stylelint": "^7.7.1", "stylelint-config-standard": "^15.0.1", diff --git a/scripts/test/visual/run b/scripts/test/visual/run index 3d1d805d7..a5691bb5d 100755 --- a/scripts/test/visual/run +++ b/scripts/test/visual/run @@ -28,4 +28,4 @@ if [[ ! -d `npm bin` ]]; then fi # Run command -`npm bin`/gulp tests:visual:run --clean --optimize --revision $@ +`npm bin`/gulp tests:visual:run --clean $@ diff --git a/tests/visual/config.json b/tests/visual/config.json index de9bc12a0..61e746b6d 100644 --- a/tests/visual/config.json +++ b/tests/visual/config.json @@ -10,28 +10,28 @@ { "name": "mobile-landscape", "size": { - "width": 480, + "width": 560, "height": 600 } }, { "name": "tablet-portrait", "size": { - "width": 720, + "width": 800, "height": 600 } }, { "name": "tablet-landscape", "size": { - "width": 960, + "width": 1020, "height": 600 } }, { "name": "screen", "size": { - "width": 1220, + "width": 1280, "height": 600 } } diff --git a/tests/visual/config/gemini.sauce-connect.json b/tests/visual/config/gemini.sauce-connect.json new file mode 100644 index 000000000..b76b180e5 --- /dev/null +++ b/tests/visual/config/gemini.sauce-connect.json @@ -0,0 +1,22 @@ +{ + "rootUrl": "http://localhost:8000", + "gridUrl": "http://ondemand.saucelabs.com/wd/hub", + "screenshotsDir": "./tests/visual/baseline/ci", + "browsers": { + "ie11": { + "desiredCapabilities": { + "browserName": "internet explorer", + "version": "11.103", + "platform": "Windows 10", + "screenResolution": "1280x1024" + } + } + }, + "system": { + "projectRoot": ".", + "sourceRoot": "./tests/visual/data", + "coverage": { + "enabled": false + } + } +} diff --git a/tests/visual/config/gemini.local.json b/tests/visual/config/gemini.selenium.json similarity index 67% rename from tests/visual/config/gemini.local.json rename to tests/visual/config/gemini.selenium.json index cc8127f58..2c657e797 100644 --- a/tests/visual/config/gemini.local.json +++ b/tests/visual/config/gemini.selenium.json @@ -9,7 +9,10 @@ } }, "system": { - "projectRoot": "./", - "sourceRoot": "./src/assets/stylesheets" + "projectRoot": ".", + "sourceRoot": "./tests/visual/data", + "coverage": { + "enabled": false + } } } diff --git a/tests/visual/suites/layout/nav/mkdocs.yml b/tests/visual/suites/layout/nav/mkdocs.yml index 42be26fbd..f35cd7ced 100644 --- a/tests/visual/suites/layout/nav/mkdocs.yml +++ b/tests/visual/suites/layout/nav/mkdocs.yml @@ -24,8 +24,8 @@ pages: - Lorem ipsum dolor sit amet: index.md - Consectetur adipiscing elit: empty.md - Etiam condimentum lacinia urna id vestibulum: empty.md - - Maecenas tincidunt nulla dui: empty.md - A dapibus turpis iaculis at: - Donec tortor sem: empty.md - Scelerisque ut congue id: empty.md - Pretium ac risus: empty.md + - Maecenas tincidunt nulla dui: empty.md diff --git a/tests/visual/suites/layout/nav/suite.js b/tests/visual/suites/layout/nav/suite.js index 02764783c..2cc4c156c 100644 --- a/tests/visual/suites/layout/nav/suite.js +++ b/tests/visual/suites/layout/nav/suite.js @@ -45,6 +45,7 @@ spec.generate(__dirname, { "md-nav--primary": { "url": "/", "capture": ".md-nav--primary", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open } ], @@ -53,6 +54,7 @@ spec.generate(__dirname, { /* List title */ "md-nav__title": { "capture": ".md-nav--primary .md-nav__title", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open } ], @@ -62,6 +64,7 @@ spec.generate(__dirname, { "~overflow": { "dir": "_overflow", "capture": ".md-nav--primary .md-nav__title", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open } ] @@ -69,27 +72,21 @@ spec.generate(__dirname, { } }, - /* List of items */ - "md-nav__list": { - "capture": ".md-nav--primary .md-nav__list", - "states": [ - { "name": "", "wait": 250, "exec": open } - ] - }, - /* List item */ "md-nav__item": { "capture": ".md-nav--primary .md-nav__item", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open } ], "suites": { - /* Last list item */ + /* Last list item */ // TODO: this is not captured! ":last-child": { "capture": ".md-nav--primary > .md-nav__list >" + ".md-nav__item:last-child", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open } ] @@ -100,6 +97,7 @@ spec.generate(__dirname, { /* Item contains a nested list */ "md-nav__item--nested": { "capture": ".md-nav--primary .md-nav__item--nested", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open } ], @@ -110,6 +108,7 @@ spec.generate(__dirname, { "capture": ".md-nav--primary .md-nav__item--nested " + ".md-nav__link", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open }, { "name": ":focus", "wait": 250, "exec": open }, @@ -122,6 +121,7 @@ spec.generate(__dirname, { "capture": ".md-nav--primary .md-nav__item--nested " + ".md-nav__link--active", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open }, { "name": ":focus", "wait": 250, "exec": open }, @@ -131,18 +131,10 @@ spec.generate(__dirname, { } }, - /* Button with logo */ - "md-nav__button": { - "capture": ".md-nav--primary .md-nav__button", - "break": "-@tablet-landscape", - "states": [ - { "name": "", "wait": 250, "exec": open } - ] - }, - /* Link inside item */ "md-nav__link": { "capture": ".md-nav--primary .md-nav__item:nth-child(2) .md-nav__link", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open }, { "name": ":focus", "wait": 250, "exec": open }, @@ -153,6 +145,7 @@ spec.generate(__dirname, { /* Active link */ "md-nav__link--active": { "capture": ".md-nav--primary .md-nav__item .md-nav__link--active", + "break": "+@tablet-landscape", "states": [ { "name": "", "wait": 250, "exec": open }, { "name": ":focus", "wait": 250, "exec": open },