diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 727827f31f282e8e9c1e8c529c34c4b01b00f86b..89efceb63b85cdcc0e068d964cf20492411690f8 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -106,14 +106,25 @@ $ yarn run build-watch Run unit tests with + yarn run jest # Jest yarn run test # Karma - yarn run test-e2e # Selenium Webdriver Run the linters and type checker with yarn run lint yarn run flow +#### End-to-end tests + +End-to-end tests are written with [webschauffeur](https://github.com/metabase/webchauffeur) which is a wrapper around [`selenium-webdriver`](https://www.npmjs.com/package/selenium-webdriver). + +Run E2E tests once with + + yarn run test-e2e + +or use a persistent browser session with + + yarn run test-e2e-dev ## Backend development Leiningen and your REPL are the main development tools for the backend. There are some directions below on how to setup your REPL for easier development. diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx index a1740cf79f3fe965de2e8057f4d73a6fa3334268..ffc6dd9674b87acb30d3ee3139e569063fdbac84 100644 --- a/frontend/src/metabase/nav/containers/Navbar.jsx +++ b/frontend/src/metabase/nav/containers/Navbar.jsx @@ -143,8 +143,7 @@ export default class Navbar extends Component { </li> <li className="pl3 hide sm-show"> <Link to={Urls.question()} data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all"> - New - <span>Question</span> + New <span>Question</span> </Link> </li> <li className="flex-align-right transition-background"> diff --git a/frontend/src/metabase/questions/containers/EntityList.jsx b/frontend/src/metabase/questions/containers/EntityList.jsx index b06cc645fec8a4b0926a0e50f3543ea828433655..95227f8e7f69a60262b88891d4f2af13963dd58d 100644 --- a/frontend/src/metabase/questions/containers/EntityList.jsx +++ b/frontend/src/metabase/questions/containers/EntityList.jsx @@ -176,8 +176,10 @@ export default class EntityList extends Component { const section = this.getSection(); + const hasEntitiesInPlainState = entityIds.length > 0 || section.section !== "all"; + const showActionHeader = (editable && selectedCount > 0); - const showSearchHeader = (entityIds.length > 0 && showSearchWidget); + const showSearchHeader = (hasEntitiesInPlainState && showSearchWidget); const showEntityFilterWidget = onChangeSection; return ( <div style={style}> @@ -201,7 +203,7 @@ export default class EntityList extends Component { : null } - { showEntityFilterWidget && entityIds.length > 0 && + { showEntityFilterWidget && hasEntitiesInPlainState && <EntityFilterWidget section={section} onChange={onChangeSection} diff --git a/frontend/test/develop-incrementally.js b/frontend/test/develop-incrementally.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/frontend/test/e2e-with-persistent-browser.js b/frontend/test/e2e-with-persistent-browser.js new file mode 100755 index 0000000000000000000000000000000000000000..07441d5380fbc38961ce30630944e7ef252dc335 --- /dev/null +++ b/frontend/test/e2e-with-persistent-browser.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +const exec = require('child_process').exec +const execSync = require('child_process').execSync +const fs = require('fs'); +const webdriver = require('selenium-webdriver'); + +// User input initialization +const stdin = fs.openSync('/dev/stdin', 'rs'); +const buffer = Buffer.alloc(8); + +// Yarn must be executed from project root +process.chdir(__dirname + '/../..'); + +const url = 'http://localhost:9515'; +const driverProcess = exec('chromedriver --port=9515'); + +const driver = new webdriver.Builder() + .forBrowser('chrome') + .usingServer(url) + .build(); + +driver.getSession().then(function (session) { + const id = session.getId() + console.log('Launched persistent Webdriver session with session ID ' + id, url); + + function executeTest() { + const hasCommandToExecuteBeforeReload = + process.argv.length >= 4 && process.argv[2] === '--exec-before' + + if (hasCommandToExecuteBeforeReload) { + console.log(execSync(process.argv[3]).toString()) + } + + const cmd = 'WEBDRIVER_SESSION_ID=' + id + ' WEBDRIVER_SESSION_URL=' + url + ' yarn run test-e2e'; + console.log(cmd); + + const testProcess = exec(cmd); + testProcess.stdout.pipe(process.stdout); + testProcess.stderr.pipe(process.stderr); + testProcess.on('exit', function () { + console.log("Press <Enter> to rerun tests or <C-c> to quit.") + fs.readSync(stdin, buffer, 0, 8); + executeTest(); + }) + } + + executeTest(); +}); + +process.on('SIGTERM', function () { + console.log('Shutting down...') + driver.quit().then(function () { + process.exit(0) + }); + driverProcess.kill('SIGINT'); +}); diff --git a/frontend/test/e2e/support/webdriver.js b/frontend/test/e2e/support/webdriver.js index 5dc93947316f15afb809502b65ae450e40a8d899..28736c5fcd0ea64267e254a4212641b18057570f 100644 --- a/frontend/test/e2e/support/webdriver.js +++ b/frontend/test/e2e/support/webdriver.js @@ -1,8 +1,12 @@ -import { Builder } from "selenium-webdriver"; +import { Builder, WebDriver } from "selenium-webdriver"; import { USE_SAUCE, sauceCapabilities, sauceServer } from './sauce'; import createSharedResource from "./shared-resource"; +const SESSION_URL = process.env["WEBDRIVER_SESSION_URL"]; +const SESSION_ID = process.env["WEBDRIVER_SESSION_ID"]; +const USE_EXISTING_SESSION = SESSION_URL && SESSION_ID; + export const getConfig = ({ name }) => { if (USE_SAUCE) { return { @@ -35,24 +39,36 @@ export const WebdriverResource = createSharedResource("WebdriverResource", { }, async start(webdriver) { if (!webdriver.driver) { - let builder = new Builder(); - if (webdriver.config.capabilities) { - builder.withCapabilities(webdriver.config.capabilities); - } - if (webdriver.config.server) { - builder.usingServer(webdriver.config.server); - } - if (webdriver.config.browser) { - builder.forBrowser(webdriver.config.browser); + if (USE_EXISTING_SESSION) { + const _http = require('selenium-webdriver/http'); + + const client = new _http.HttpClient(SESSION_URL, null, null); + const executor = new _http.Executor(client); + + webdriver.driver = await WebDriver.attachToSession(executor, SESSION_ID); + } else { + let builder = new Builder(); + if (webdriver.config.capabilities) { + builder.withCapabilities(webdriver.config.capabilities); + } + if (webdriver.config.server) { + builder.usingServer(webdriver.config.server); + } + if (webdriver.config.browser) { + builder.forBrowser(webdriver.config.browser); + } + webdriver.driver = builder.build(); } - webdriver.driver = builder.build(); } }, async stop(webdriver) { if (webdriver.driver) { const driver = webdriver.driver; delete webdriver.driver; - await driver.quit(); + + if (!USE_EXISTING_SESSION) { + await driver.quit(); + } } } }); diff --git a/package.json b/package.json index 08b310ea3b1609eec0906ace803a348d6eb980d3..298fee0dd7351b4b5bee6abab79d8a11fa8262e7 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "test": "karma start frontend/test/karma.conf.js --single-run", "test-watch": "karma start frontend/test/karma.conf.js --auto-watch --reporters nyan", "test-e2e": "JASMINE_CONFIG_PATH=./frontend/test/e2e/support/jasmine.json jasmine", + "test-e2e-dev": "./frontend/test/e2e-with-persistent-browser.js", "test-e2e-sauce": "USE_SAUCE=true yarn run test-e2e", "build": "webpack --bail", "build-watch": "webpack --watch",