From 06f14c0126aeab927ddee542f851a950d5edeca3 Mon Sep 17 00:00:00 2001 From: "Damon P. Cortesi" <d.lifehacker@gmail.com> Date: Wed, 15 Apr 2020 07:20:45 -0700 Subject: [PATCH] Feature/cypress mongo (#12292) * Add database-specific tests to Cypress (starting with Mongo) * Initial changes to Cypress to allow a limited set of tests to run * Include drivers in frontend uberjar --- .circleci/config.yml | 45 ++++++- frontend/test/__runner__/run_cypress_tests.js | 18 ++- frontend/test/__support__/cypress.js | 11 ++ frontend/test/cypress.json | 2 +- .../test/metabase-db/mongo/add.cy.spec.js | 58 +++++++++ .../test/metabase-db/mongo/query.cy.spec.js | 110 ++++++++++++++++++ 6 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 frontend/test/metabase-db/mongo/add.cy.spec.js create mode 100644 frontend/test/metabase-db/mongo/query.cy.spec.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 59b9c85b047..6f4181a5efa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -170,6 +170,11 @@ executors: ACCEPT_EULA: Y SA_PASSWORD: 'P@ssw0rd' + fe-mongo: + working_directory: /home/circleci/metabase/metabase/ + docker: + - image: circleci/clojure:lein-2.8.1-node-browsers + - image: circleci/mongo:4.0 ######################################################################################################################## @@ -279,6 +284,13 @@ jobs: done; echo `md5sum yarn.lock` >> frontend-checksums.txt echo `md5sum webpack.config.js` >> frontend-checksums.txt + # As well as driver modules (database drivers) + - run: + name: Generate checksums of all driver module source files to use as Uberjar cache key + command: > + for file in `find ./modules -type f | sort`; + do echo `md5sum $file` >> modules-checksums.txt; + done; - run: name: Save last git commit message command: git log -1 --oneline > commit.txt @@ -530,18 +542,18 @@ jobs: - restore-be-deps-cache - restore_cache: keys: - - uberjar-{{ checksum "./backend-checksums.txt" }}-{{ checksum "./frontend-checksums.txt" }} + - uberjar-{{ checksum "./backend-checksums.txt" }}-{{ checksum "./frontend-checksums.txt" }}-{{ checksum "./modules-checksums.txt" }} - run: name: Build uberjar if needed command: > if [ ! -f './target/uberjar/metabase.jar' ]; - then ./bin/build version frontend uberjar; + then ./bin/build version frontend drivers uberjar; fi no_output_timeout: 5m - store_artifacts: path: /home/circleci/metabase/metabase/target/uberjar/metabase.jar - save_cache: - key: uberjar-{{ checksum "./backend-checksums.txt" }}-{{ checksum "./frontend-checksums.txt" }} + key: uberjar-{{ checksum "./backend-checksums.txt" }}-{{ checksum "./frontend-checksums.txt" }}-{{ checksum "./modules-checksums.txt" }} paths: - /home/circleci/metabase/metabase/target/uberjar/metabase.jar @@ -560,11 +572,24 @@ jobs: command: ./bin/build version fe-tests-cypress: - executor: clojure-and-node + parameters: + e: + type: executor + default: clojure-and-node + only-single-database: + type: boolean + default: false + test-files-location: + type: string + default: "" + driver: + type: string + default: "" + executor: << parameters.e >> steps: - run-yarn-command: command-name: Run Cypress tests - command: run test-cypress-no-build + command: run test-cypress-no-build <<# parameters.only-single-database >> --testFiles << parameters.test-files-location >> <</ parameters.only-single-database >> before-steps: - restore_cache: keys: @@ -852,6 +877,16 @@ workflows: - build-uberjar - fe-deps + - fe-tests-cypress: + name: fe-tests-cypress-mongo + requires: + - build-uberjar + - fe-deps + e: fe-mongo + driver: mongo + only-single-database: true + test-files-location: frontend/test/metabase-db/mongo + - deploy-master: requires: - yaml-linter diff --git a/frontend/test/__runner__/run_cypress_tests.js b/frontend/test/__runner__/run_cypress_tests.js index 1644bc2af47..20019ae94c2 100644 --- a/frontend/test/__runner__/run_cypress_tests.js +++ b/frontend/test/__runner__/run_cypress_tests.js @@ -7,8 +7,13 @@ const BackendResource = require("./backend.js").BackendResource; const server = BackendResource.get({ dbKey: "" }); +// We currently accept two (optional) command line arguments +// --open - Opens the Cypress test browser +// --testFiles <path> - Specifies a different path for the integration folder const userArgs = process.argv.slice(2); -const isOpenMode = userArgs[0] === "--open"; +const isOpenMode = userArgs.includes("--open"); +const testFiles = userArgs.includes("--testFiles"); +const testFilesLocation = userArgs[userArgs.indexOf("--testFiles") + 1]; function readFile(fileName) { return new Promise(function(resolve, reject) { @@ -30,6 +35,10 @@ const init = async () => { ); } + if (testFiles) { + console.log(chalk.bold(`Running tests in '${testFilesLocation}'`)); + } + try { const version = await readFile( __dirname + "/../../../resources/version.properties", @@ -54,6 +63,11 @@ const init = async () => { await BackendResource.start(server); console.log(chalk.bold("Starting Cypress")); + let commandLineConfig = `baseUrl=${server.host}`; + if (testFiles) { + commandLineConfig = `${commandLineConfig},integrationFolder=${testFilesLocation}`; + } + const cypressProcess = spawn( "yarn", [ @@ -62,7 +76,7 @@ const init = async () => { "--config-file", process.env["CONFIG_FILE"], "--config", - `baseUrl=${server.host}`, + commandLineConfig, ...(process.env["CI"] ? [ "--reporter", diff --git a/frontend/test/__support__/cypress.js b/frontend/test/__support__/cypress.js index 94c8ae103e4..075eecfc998 100644 --- a/frontend/test/__support__/cypress.js +++ b/frontend/test/__support__/cypress.js @@ -68,6 +68,8 @@ export function main() { return cy.get("nav").next(); } +// Metabase utility functions for commonly-used patterns + export function openOrdersTable() { cy.visit("/question/new?database=1&table=2"); } @@ -76,4 +78,13 @@ export function openProductsTable() { cy.visit("/question/new?database=1&table=1"); } +// Find a text field by label text, type it in, then blur the field. +// Commonly used in our Admin section as we auto-save settings. +export function typeAndBlurUsingLabel(label, value) { + cy.findByLabelText(label) + .clear() + .type(value) + .blur(); +} + Cypress.on("uncaught:exception", (err, runnable) => false); diff --git a/frontend/test/cypress.json b/frontend/test/cypress.json index e026f5171fd..2d6709c27b8 100644 --- a/frontend/test/cypress.json +++ b/frontend/test/cypress.json @@ -1,7 +1,7 @@ { "testFiles": "**/*.cy.spec.js", "pluginsFile": "frontend/test/cypress-plugins.js", - "integrationFolder": "frontend/test", + "integrationFolder": "frontend/test/metabase", "supportFile": "frontend/test/__support__/cypress.js", "viewportHeight": 800, "viewportWidth": 1280 diff --git a/frontend/test/metabase-db/mongo/add.cy.spec.js b/frontend/test/metabase-db/mongo/add.cy.spec.js new file mode 100644 index 00000000000..dde062aa147 --- /dev/null +++ b/frontend/test/metabase-db/mongo/add.cy.spec.js @@ -0,0 +1,58 @@ +import { + signInAsAdmin, + restore, + modal, + typeAndBlurUsingLabel, +} from "__support__/cypress"; + +function addMongoDatabase() { + cy.visit("/admin/databases/create"); + cy.contains("Database type") + .closest(".Form-field") + .find("a") + .click(); + cy.contains("MongoDB").click({ force: true }); + cy.contains("Additional Mongo connection"); + + typeAndBlurUsingLabel("Name", "MongoDB"); + typeAndBlurUsingLabel("Database name", "admin"); + + cy.findByText("Save") + .should("not.be.disabled") + .click(); +} + +describe("mongodb > admin > add", () => { + beforeEach(() => { + restore(); + signInAsAdmin(); + cy.server(); + }); + + it("should add a database and redirect to listing", () => { + cy.route({ + method: "POST", + url: "/api/database", + }).as("createDatabase"); + + addMongoDatabase(); + + cy.wait("@createDatabase"); + + cy.url().should("match", /\/admin\/databases\?created=\d+$/); + cy.contains("Your database has been added!"); + modal() + .contains("I'm good thanks") + .click(); + }); + + it("can query a Mongo database", () => { + addMongoDatabase(); + cy.url().should("match", /\/admin\/databases\?created=\d+$/); + cy.visit("/question/new"); + cy.contains("Simple question").click(); + cy.contains("MongoDB").click(); + cy.contains("Version").click(); + cy.contains("featureCompatibilityVersion"); + }); +}); diff --git a/frontend/test/metabase-db/mongo/query.cy.spec.js b/frontend/test/metabase-db/mongo/query.cy.spec.js new file mode 100644 index 00000000000..e156f3dfafa --- /dev/null +++ b/frontend/test/metabase-db/mongo/query.cy.spec.js @@ -0,0 +1,110 @@ +import { + signInAsAdmin, + restore, + modal, + signInAsNormalUser, +} from "__support__/cypress"; + +function addMongoDatabase() { + cy.request("POST", "/api/database", { + engine: "mongo", + name: "MongoDB", + details: { + host: "localhost", + dbname: "admin", + port: 27017, + user: null, + pass: null, + authdb: null, + "additional-options": null, + "use-srv": false, + "tunnel-enabled": false, + }, + auto_run_queries: true, + is_full_sync: true, + schedules: { + cache_field_values: { + schedule_day: null, + schedule_frame: null, + schedule_hour: 0, + schedule_type: "daily", + }, + metadata_sync: { + schedule_day: null, + schedule_frame: null, + schedule_hour: null, + schedule_type: "hourly", + }, + }, + }); +} + +describe("mongodb > user > query", () => { + before(() => { + restore(); + signInAsAdmin(); + addMongoDatabase(); + }); + + beforeEach(() => { + signInAsNormalUser(); + }); + + it("can query a Mongo database as a user", () => { + cy.visit("/question/new"); + cy.contains("Simple question").click(); + cy.contains("MongoDB").click(); + cy.contains("Version").click(); + cy.contains("featureCompatibilityVersion"); + }); + + it.only("can write a native MongoDB query", () => { + cy.visit("/question/new"); + cy.contains("Native query").click(); + cy.contains("MongoDB").click(); + + cy.get(".ace_content").type(`[ { $count: "Total" } ]`, { + parseSpecialCharSequences: false, + }); + cy.get(".NativeQueryEditor .Icon-play").click(); + cy.contains("1"); + }); + + it("can save a native MongoDB query", () => { + cy.server(); + cy.route("POST", "/api/card").as("createQuestion"); + + cy.visit("/question/new"); + cy.contains("Native query").click(); + cy.contains("MongoDB").click(); + + cy.get(".ace_content").type(`[ { $count: "Total" } ]`, { + parseSpecialCharSequences: false, + }); + cy.get(".NativeQueryEditor .Icon-play").click(); + cy.contains("1"); + + // Close the Ace editor because it interferes with the modal for some reason + cy.get(".Icon-contract").click(); + + cy.contains("Save").click(); + modal() + .findByLabelText("Name") + .focus() + .type("mongo count"); + modal() + .contains("button", "Save") + .should("not.be.disabled") + .click(); + + cy.wait("@createQuestion").then(({ status }) => { + expect(status).to.equal(202); + }); + + modal() + .contains("Not now") + .click(); + + cy.url().should("match", /\/question\/\d+$/); + }); +}); -- GitLab