diff --git a/package.json b/package.json index 5c5d328a3030..455334a24d4c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "test:ui:runner": "grunt test:ui:runner", "test:server": "grunt test:server", "test:coverage": "grunt test:coverage", + "test:visualRegression": "grunt test:visualRegression", "build": "grunt build", "release": "grunt release", "start": "sh ./bin/kibana --dev", diff --git a/tasks/config/intern.js b/tasks/config/intern.js index 3a56007072df..7fc86fbdef2c 100644 --- a/tasks/config/intern.js +++ b/tasks/config/intern.js @@ -13,7 +13,13 @@ module.exports = function (grunt) { api: { options: { runType: 'client', - config: 'test/api_intern' + config: 'test/intern_api' + } + }, + visualRegression: { + options: { + runType: 'runner', + config: 'test/intern_visual_regression' } } }; diff --git a/tasks/test.js b/tasks/test.js index dd56aff10547..07ab10d225ef 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -2,8 +2,18 @@ const _ = require('lodash'); const visualRegression = require('../utilities/visual_regression'); module.exports = function (grunt) { + grunt.registerTask('test:visualRegression', [ + 'intern:visualRegression:takeScreenshots', + 'test:visualRegression:buildGallery' + ]); + + grunt.registerTask('test:visualRegression:takeScreenshots', [ + 'clean:screenshots', + 'intern:visualRegression' + ]); + grunt.registerTask( - 'test:visualRegression', + 'test:visualRegression:buildGallery', 'Compare screenshots and generate diff images.', function () { const done = this.async(); @@ -72,8 +82,7 @@ module.exports = function (grunt) { grunt.task.run(_.compact([ !grunt.option('quick') && 'eslint:source', 'licenses', - 'test:quick', - 'test:visualRegression' + 'test:quick' ])); }); diff --git a/test/functional/index.js b/test/functional/index.js index c881e78b1abe..2a1848dfed74 100644 --- a/test/functional/index.js +++ b/test/functional/index.js @@ -3,18 +3,18 @@ define(function (require) { const bdd = require('intern!bdd'); const intern = require('intern'); - const earliestBeforeCbs = []; + const initCallbacks = []; - function onEarliestBefore(cb) { - earliestBeforeCbs.push(cb); + function onInit(callback) { + initCallbacks.push(callback); } - global.__kibana__intern__ = { intern, bdd, onEarliestBefore }; + global.__kibana__intern__ = { intern, bdd, onInit }; bdd.describe('kibana', function () { bdd.before(function () { - earliestBeforeCbs.forEach(cb => { - cb.call(this); + initCallbacks.forEach(callback => { + callback.call(this); }); }); diff --git a/test/intern.js b/test/intern.js index 29599e699791..f3ee839d7a0d 100644 --- a/test/intern.js +++ b/test/intern.js @@ -1,8 +1,6 @@ define(function (require) { - var serverConfig = require('intern/dojo/node!./server_config'); - var _ = require('intern/dojo/node!lodash'); - - return _.assign({ + const serverConfig = require('intern/dojo/node!./server_config'); + return Object.assign({ debug: true, capabilities: { 'selenium-version': '2.53.0', diff --git a/test/api_intern.js b/test/intern_api.js similarity index 100% rename from test/api_intern.js rename to test/intern_api.js diff --git a/test/intern_visual_regression.js b/test/intern_visual_regression.js new file mode 100644 index 000000000000..778e17f0cbf8 --- /dev/null +++ b/test/intern_visual_regression.js @@ -0,0 +1,24 @@ +define(function (require) { + const serverConfig = require('intern/dojo/node!./server_config'); + return Object.assign({ + debug: true, + capabilities: { + 'selenium-version': '2.53.0', + // must match URL in tasks/config/downloadSelenium.js + 'idle-timeout': 99 + }, + environments: [{ + browserName: 'chrome' + }], + tunnelOptions: serverConfig.servers.webdriver, + functionalSuites: [ + 'test/visual_regression/index' + ], + + excludeInstrumentation: /.*/, + + defaultTimeout: 90000, + defaultTryTimeout: 40000, // tryForTime could include multiple 'find timeouts' + defaultFindTimeout: 10000 // this is how long we try to find elements on page + }, serverConfig); +}); diff --git a/test/support/index.js b/test/support/index.js index 6a0ccf41d014..a4a1db5eaed5 100644 --- a/test/support/index.js +++ b/test/support/index.js @@ -23,36 +23,64 @@ exports.scenarioManager = new ScenarioManager(url.format(exports.config.servers. exports.esClient = new EsClient(url.format(exports.config.servers.elasticsearch)); exports.bdd = new BddWrapper(kbnInternVars.bdd); -defineDelayedExport('remote', (suite) => suite.remote); -defineDelayedExport('common', () => new Common()); -defineDelayedExport('discoverPage', () => new DiscoverPage()); -defineDelayedExport('headerPage', () => new HeaderPage()); -defineDelayedExport('settingsPage', () => new SettingsPage()); -defineDelayedExport('visualizePage', () => new VisualizePage()); -defineDelayedExport('dashboardPage', () => new DashboardPage()); -defineDelayedExport('shieldPage', () => new ShieldPage()); -defineDelayedExport('consolePage', () => new ConsolePage()); -defineDelayedExport('elasticDump', () => new ElasticDump()); +const delayedExports = [{ + name: 'remote', + exportProvider: (suite) => suite.remote +}, { + name: 'common', + exportProvider: () => new Common() +}, { + name: 'discoverPage', + exportProvider: () => new DiscoverPage() +}, { + name: 'headerPage', + exportProvider: () => new HeaderPage() +}, { + name: 'settingsPage', + exportProvider: () => new SettingsPage() +}, { + name: 'visualizePage', + exportProvider: () => new VisualizePage() +}, { + name: 'dashboardPage', + exportProvider: () => new DashboardPage() +}, { + name: 'shieldPage', + exportProvider: () => new ShieldPage() +}, { + name: 'consolePage', + exportProvider: () => new ConsolePage() +}, { + name: 'elasticDump', + exportProvider: () => new ElasticDump() +}]; -// creates an export for values that aren't actually avaialable until -// until tests start to run. These getters will throw errors if the export +delayedExports.forEach(exportConfig => { + delayExportUntilInit( + exportConfig.name, + exportConfig.exportProvider + ); +}); + +// Creates an export for values that aren't actually avaialable until +// tests start to run. These getters will throw errors if the export // is accessed before it's available, hopefully making debugging easier. -// Once the first before() handler is called the onEarliestBefore() handlers +// Once the first before() handler is called the onInit() call // will fire and rewrite all of these exports to be their correct value. -function defineDelayedExport(name, define) { +function delayExportUntilInit(name, provideExport) { Object.defineProperty(exports, name, { configurable: true, get() { throw new TypeError( - 'remote is not available until tests start to run. Move your ' + + 'Remote is not available until tests start to run. Move your ' + 'usage of the import inside a test setup hook or a test itself.' ); } }); - kbnInternVars.onEarliestBefore(function () { + kbnInternVars.onInit(function defineExport() { Object.defineProperty(exports, name, { - value: define(this), + value: provideExport(this), }); }); } diff --git a/test/support/pages/common.js b/test/support/pages/common.js index 05fd74a61ed1..215333b22c50 100644 --- a/test/support/pages/common.js +++ b/test/support/pages/common.js @@ -1,48 +1,58 @@ -import { config, defaultTryTimeout, defaultFindTimeout, remote, shieldPage } from '../'; +import bluebird, { + promisify +} from 'bluebird'; import fs from 'fs'; +import _ from 'lodash'; import mkdirp from 'mkdirp'; -import { promisify } from 'bluebird'; +import moment from 'moment'; +import path from 'path'; +import testSubjSelector from '@spalger/test-subj-selector'; +import { + format, + parse +} from 'url'; +import util from 'util'; + +import getUrl from '../../utils/get_url'; +import { + config, + defaultTryTimeout, + defaultFindTimeout, + remote, + shieldPage +} from '../index'; const mkdirpAsync = promisify(mkdirp); const writeFileAsync = promisify(fs.writeFile); -export default (function () { - var Promise = require('bluebird'); - var moment = require('moment'); - var testSubjSelector = require('@spalger/test-subj-selector'); - var getUrl = require('../../utils/get_url'); - var _ = require('lodash'); - var parse = require('url').parse; - var format = require('url').format; - var util = require('util'); - var path = require('path'); - - function injectTimestampQuery(func, url) { - var formatted = modifyQueryString(url, function (parsed) { - parsed.query._t = Date.now(); - }); - return func.call(this, formatted); - } +export default class Common { - function removeTimestampQuery(func) { - return func.call(this) - .then(function (url) { - return modifyQueryString(url, function (parsed) { - parsed.query = _.omit(parsed.query, '_t'); + constructor() { + function injectTimestampQuery(func, url) { + var formatted = modifyQueryString(url, function (parsed) { + parsed.query._t = Date.now(); }); - }); - } + return func.call(this, formatted); + } - function modifyQueryString(url, func) { - var parsed = parse(url, true); - if (parsed.query === null) { - parsed.query = {}; + function removeTimestampQuery(func) { + return func.call(this) + .then(function (url) { + return modifyQueryString(url, function (parsed) { + parsed.query = _.omit(parsed.query, '_t'); + }); + }); + } + + function modifyQueryString(url, func) { + var parsed = parse(url, true); + if (parsed.query === null) { + parsed.query = {}; + } + func(parsed); + return format(_.pick(parsed, 'protocol', 'hostname', 'port', 'pathname', 'query', 'hash', 'auth')); } - func(parsed); - return format(_.pick(parsed, 'protocol', 'hostname', 'port', 'pathname', 'query', 'hash', 'auth')); - } - function Common() { this.remote = remote; if (remote.get.wrapper !== injectTimestampQuery) { this.remote.get = _.wrap(this.remote.get, injectTimestampQuery); @@ -51,253 +61,247 @@ export default (function () { } } - Common.prototype = { - constructor: Common, - - getHostPort: function getHostPort() { - return getUrl.baseUrl(config.servers.kibana); - }, - - getEsHostPort: function getHostPort() { - return getUrl.baseUrl(config.servers.elasticsearch); - }, - - navigateToApp: function (appName, testStatusPage) { - var self = this; - var appUrl = getUrl.noAuth(config.servers.kibana, config.apps[appName]); - self.debug('navigating to ' + appName + ' url: ' + appUrl); - - var doNavigation = function (url) { - return self.try(function () { - // since we're using hash URLs, always reload first to force re-render - self.debug('navigate to: ' + url); - return self.remote.get(url) - .then(function () { - return self.sleep(700); - }) - .then(function () { - self.debug('returned from get, calling refresh'); - return self.remote.refresh(); - }) - .then(function () { - self.debug('check testStatusPage'); - if (testStatusPage !== false) { - self.debug('self.checkForKibanaApp()'); - return self.checkForKibanaApp() - .then(function (kibanaLoaded) { - self.debug('kibanaLoaded = ' + kibanaLoaded); - if (!kibanaLoaded) { - var msg = 'Kibana is not loaded, retrying'; - self.debug(msg); - throw new Error(msg); - } - }); - } - }) - .then(function () { - return self.remote.getCurrentUrl(); - }) - .then(function (currentUrl) { - var loginPage = new RegExp('login').test(currentUrl); - if (loginPage) { - self.debug('Found loginPage = ' + loginPage + ', username = ' - + config.servers.kibana.shield.username); - return shieldPage.login(config.servers.kibana.shield.username, - config.servers.kibana.shield.password) - .then(function () { - return self.remote.getCurrentUrl(); - }); - } else { - return currentUrl; - } - }) - .then(function (currentUrl) { - currentUrl = currentUrl.replace(/\/\/\w+:\w+@/, '//'); - var maxAdditionalLengthOnNavUrl = 230; - // On several test failures at the end of the TileMap test we try to navigate back to - // Visualize so we can create the next Vertical Bar Chart, but we can see from the - // logging and the screenshot that it's still on the TileMap page. Why didn't the "get" - // with a new timestamped URL go? I thought that sleep(700) between the get and the - // refresh would solve the problem but didn't seem to always work. - // So this hack fails the navSuccessful check if the currentUrl doesn't match the - // appUrl plus up to 230 other chars. - // Navigating to Settings when there is a default index pattern has a URL length of 196 - // (from debug output). Some other tabs may also be long. But a rather simple configured - // visualization is about 1000 chars long. So at least we catch that case. - var navSuccessful = new RegExp(appUrl + '.{0,' + maxAdditionalLengthOnNavUrl + '}$') - .test(currentUrl); - - if (!navSuccessful) { - var msg = 'App failed to load: ' + appName + - ' in ' + defaultFindTimeout + 'ms' + - ' appUrl = ' + appUrl + - ' currentUrl = ' + currentUrl; - self.debug(msg); - throw new Error(msg); - } + getHostPort() { + return getUrl.baseUrl(config.servers.kibana); + } + + getEsHostPort() { + return getUrl.baseUrl(config.servers.elasticsearch); + } + + navigateToApp(appName, testStatusPage) { + var self = this; + var appUrl = getUrl.noAuth(config.servers.kibana, config.apps[appName]); + self.debug('navigating to ' + appName + ' url: ' + appUrl); + function navigateTo(url) { + return self.try(function () { + // since we're using hash URLs, always reload first to force re-render + self.debug('navigate to: ' + url); + return self.remote.get(url) + .then(function () { + return self.sleep(700); + }) + .then(function () { + self.debug('returned from get, calling refresh'); + return self.remote.refresh(); + }) + .then(function () { + self.debug('check testStatusPage'); + if (testStatusPage !== false) { + self.debug('self.checkForKibanaApp()'); + return self.checkForKibanaApp() + .then(function (kibanaLoaded) { + self.debug('kibanaLoaded = ' + kibanaLoaded); + if (!kibanaLoaded) { + var msg = 'Kibana is not loaded, retrying'; + self.debug(msg); + throw new Error(msg); + } + }); + } + }) + .then(function () { + return self.remote.getCurrentUrl(); + }) + .then(function (currentUrl) { + var loginPage = new RegExp('login').test(currentUrl); + if (loginPage) { + self.debug('Found loginPage = ' + loginPage + ', username = ' + + config.servers.kibana.shield.username); + return shieldPage.login(config.servers.kibana.shield.username, + config.servers.kibana.shield.password) + .then(function () { + return self.remote.getCurrentUrl(); + }); + } else { return currentUrl; - }); - }); - }; - - return doNavigation(appUrl) - .then(function (currentUrl) { - var lastUrl = currentUrl; - return self.try(function () { - // give the app time to update the URL - return self.sleep(501) - .then(function () { - return self.remote.getCurrentUrl(); - }) - .then(function (currentUrl) { - self.debug('in doNavigation url = ' + currentUrl); - if (lastUrl !== currentUrl) { - lastUrl = currentUrl; - throw new Error('URL changed, waiting for it to settle'); - } - }); + } + }) + .then(function (currentUrl) { + currentUrl = currentUrl.replace(/\/\/\w+:\w+@/, '//'); + var maxAdditionalLengthOnNavUrl = 230; + // On several test failures at the end of the TileMap test we try to navigate back to + // Visualize so we can create the next Vertical Bar Chart, but we can see from the + // logging and the screenshot that it's still on the TileMap page. Why didn't the "get" + // with a new timestamped URL go? I thought that sleep(700) between the get and the + // refresh would solve the problem but didn't seem to always work. + // So this hack fails the navSuccessful check if the currentUrl doesn't match the + // appUrl plus up to 230 other chars. + // Navigating to Settings when there is a default index pattern has a URL length of 196 + // (from debug output). Some other tabs may also be long. But a rather simple configured + // visualization is about 1000 chars long. So at least we catch that case. + var navSuccessful = new RegExp(appUrl + '.{0,' + maxAdditionalLengthOnNavUrl + '}$') + .test(currentUrl); + + if (!navSuccessful) { + var msg = 'App failed to load: ' + appName + + ' in ' + defaultFindTimeout + 'ms' + + ' appUrl = ' + appUrl + + ' currentUrl = ' + currentUrl; + self.debug(msg); + throw new Error(msg); + } + + return currentUrl; }); }); - }, - - runScript: function (fn, timeout) { - var self = this; - // by default, give the app 10 seconds to load - timeout = timeout || 10000; - - // wait for deps on window before running script - return self.remote - .setExecuteAsyncTimeout(timeout) - .executeAsync(function (done) { - var interval = setInterval(function () { - var ready = (document.readyState === 'complete'); - var hasJQuery = !!window.$; - - if (ready && hasJQuery) { - console.log('doc ready, jquery loaded'); - clearInterval(interval); - done(); + }; + + return navigateTo(appUrl) + .then(function (currentUrl) { + var lastUrl = currentUrl; + return self.try(function () { + // give the app time to update the URL + return self.sleep(501) + .then(function () { + return self.remote.getCurrentUrl(); + }) + .then(function (currentUrl) { + self.debug('in navigateTo url = ' + currentUrl); + if (lastUrl !== currentUrl) { + lastUrl = currentUrl; + throw new Error('URL changed, waiting for it to settle'); } - }, 10); - }).then(function () { - return self.remote.execute(fn); + }); }); - }, + }); + } - getApp: function () { - var self = this; + runScript(fn, timeout) { + var self = this; + // by default, give the app 10 seconds to load + timeout = timeout || 10000; + + // wait for deps on window before running script + return self.remote + .setExecuteAsyncTimeout(timeout) + .executeAsync(function (done) { + var interval = setInterval(function () { + var ready = (document.readyState === 'complete'); + var hasJQuery = !!window.$; + + if (ready && hasJQuery) { + console.log('doc ready, jquery loaded'); + clearInterval(interval); + done(); + } + }, 10); + }).then(function () { + return self.remote.execute(fn); + }); + } - return self.remote.setFindTimeout(defaultFindTimeout) - .findByCssSelector('.app-wrapper .application') - .then(function () { - return self.runScript(function () { - var $ = window.$; - var $scope = $('.app-wrapper .application').scope(); - return $scope ? $scope.chrome.getApp() : {}; - }); - }); - }, - - checkForKibanaApp: function () { - var self = this; - - return self.getApp() - .then(function (app) { - var appId = app.id; - self.debug('current application: ' + appId); - return appId === 'kibana'; - }) - .catch(function (err) { - self.debug('kibana check failed'); - self.debug(err); - // not on the kibana app... - return false; + getApp() { + var self = this; + + return self.remote.setFindTimeout(defaultFindTimeout) + .findByCssSelector('.app-wrapper .application') + .then(function () { + return self.runScript(function () { + var $ = window.$; + var $scope = $('.app-wrapper .application').scope(); + return $scope ? $scope.chrome.getApp() : {}; }); - }, + }); + } - tryForTime: function (timeout, block) { - var self = this; - var start = Date.now(); - var retryDelay = 502; - var lastTry = 0; - var tempMessage; + checkForKibanaApp() { + var self = this; + + return self.getApp() + .then(function (app) { + var appId = app.id; + self.debug('current application: ' + appId); + return appId === 'kibana'; + }) + .catch(function (err) { + self.debug('kibana check failed'); + self.debug(err); + // not on the kibana app... + return false; + }); + } - function attempt() { - lastTry = Date.now(); + tryForTime(timeout, block) { + var self = this; + var start = Date.now(); + var retryDelay = 502; + var lastTry = 0; + var tempMessage; - if (lastTry - start > timeout) { - throw new Error('timeout ' + tempMessage); - } + function attempt() { + lastTry = Date.now(); - return Promise - .try(block) - .catch(function tryForTimeCatch(err) { - self.debug('tryForTime failure: ' + err.message); - tempMessage = err.message; - return Promise.delay(retryDelay).then(attempt); - }); + if (lastTry - start > timeout) { + throw new Error('timeout ' + tempMessage); } - return Promise.try(attempt); - }, + return bluebird + .try(block) + .catch(function tryForTimeCatch(err) { + self.debug('tryForTime failure: ' + err.message); + tempMessage = err.message; + return bluebird.delay(retryDelay).then(attempt); + }); + } + + return bluebird.try(attempt); + } - try(block) { - return this.tryForTime(defaultTryTimeout, block); - }, + try(block) { + return this.tryForTime(defaultTryTimeout, block); + } - log(...args) { - console.log(moment().format('HH:mm:ss.SSS') + ':', util.format(...args)); - }, + log(...args) { + console.log(moment().format('HH:mm:ss.SSS') + ':', util.format(...args)); + } - debug(...args) { - if (config.debug) this.log(...args); - }, + debug(...args) { + if (config.debug) this.log(...args); + } - sleep: function sleep(sleepMilliseconds) { - var self = this; - self.debug('... sleep(' + sleepMilliseconds + ') start'); + sleep(sleepMilliseconds) { + var self = this; + self.debug('... sleep(' + sleepMilliseconds + ') start'); - return Promise.resolve().delay(sleepMilliseconds) - .then(function () { self.debug('... sleep(' + sleepMilliseconds + ') end'); }); - }, + return bluebird.resolve().delay(sleepMilliseconds) + .then(function () { self.debug('... sleep(' + sleepMilliseconds + ') end'); }); + } - handleError(testObj) { - const testName = (testObj.parent) ? [testObj.parent.name, testObj.name].join('_') : testObj.name; - return reason => { - const now = Date.now(); - const fileName = `failure_${now}_${testName}`; + handleError(testObj) { + const testName = (testObj.parent) ? [testObj.parent.name, testObj.name].join('_') : testObj.name; + return reason => { + const now = Date.now(); + const fileName = `failure_${now}_${testName}`; - return this.saveScreenshot(fileName, true) - .then(function () { - throw reason; - }); - }; - }, - - async saveScreenshot(fileName, isFailure = false) { - try { - const directoryName = isFailure ? 'failure' : 'session'; - const directoryPath = path.resolve(`test/screenshots/${directoryName}`); - const filePath = path.resolve(directoryPath, `${fileName}.png`); - this.debug(`Taking screenshot "${filePath}"`); - - const screenshot = await this.remote.takeScreenshot(); - await mkdirpAsync(directoryPath); - await writeFileAsync(filePath, screenshot); - } catch (err) { - this.log(`SCREENSHOT FAILED: ${err}`); - } - }, + return this.saveScreenshot(fileName, true) + .then(function () { + throw reason; + }); + }; + } - findTestSubject: function findTestSubject(selector) { - this.debug('in findTestSubject: ' + testSubjSelector(selector)); - return this.remote - .setFindTimeout(defaultFindTimeout) - .findDisplayedByCssSelector(testSubjSelector(selector)); + async saveScreenshot(fileName, isFailure = false) { + try { + const directoryName = isFailure ? 'failure' : 'session'; + const directoryPath = path.resolve(`test/screenshots/${directoryName}`); + const filePath = path.resolve(directoryPath, `${fileName}.png`); + this.debug(`Taking screenshot "${filePath}"`); + + const screenshot = await this.remote.takeScreenshot(); + await mkdirpAsync(directoryPath); + await writeFileAsync(filePath, screenshot); + } catch (err) { + this.log(`SCREENSHOT FAILED: ${err}`); } + } - }; + findTestSubject(selector) { + this.debug('in findTestSubject: ' + testSubjSelector(selector)); + return this.remote + .setFindTimeout(defaultFindTimeout) + .findDisplayedByCssSelector(testSubjSelector(selector)); + } - return Common; -}()); +}; diff --git a/test/visual_regression/home/_loading.js b/test/visual_regression/home/_loading.js new file mode 100644 index 000000000000..a3d77860f34f --- /dev/null +++ b/test/visual_regression/home/_loading.js @@ -0,0 +1,14 @@ +import { + bdd, + scenarioManager, + common, + consolePage +} from '../../support'; + +var expect = require('expect.js'); + +bdd.describe('Loading', function coverLoadingUi() { + bdd.it('should show loading feebdack', async function () { + // TODO: Take screenshots here. + }); +}); diff --git a/test/visual_regression/home/index.js b/test/visual_regression/home/index.js new file mode 100644 index 000000000000..34f3aa4b3e0e --- /dev/null +++ b/test/visual_regression/home/index.js @@ -0,0 +1,16 @@ +import { + bdd, + remote, + scenarioManager, + defaultTimeout +} from '../../support'; + +bdd.describe('Home', function () { + this.timeout = defaultTimeout; + + bdd.before(function () { + return remote.setWindowSize(1200, 800); + }); + + require('./_loading'); +}); diff --git a/test/visual_regression/index.js b/test/visual_regression/index.js new file mode 100644 index 000000000000..17bec9761f36 --- /dev/null +++ b/test/visual_regression/index.js @@ -0,0 +1,26 @@ +define(function (require) { + require('intern/dojo/node!../support/env_setup'); + + const bdd = require('intern!bdd'); + const intern = require('intern'); + const initCallbacks = []; + + function onInit(callback) { + initCallbacks.push(callback); + } + + global.__kibana__intern__ = { intern, bdd, onInit }; + + bdd.describe('Kibana visual regressions', function () { + bdd.before(function () { + initCallbacks.forEach(callback => { + callback.call(this); + }); + }); + + require([ + 'intern/dojo/node!../support/index', + 'intern/dojo/node!./home', + ], function () {}); + }); +});