diff --git a/frontend/test/__support__/e2e/cypress.js b/frontend/test/__support__/e2e/cypress.js index 8f7aca0ff1e2d81015c063d833688900341f5b2f..774b673abac8bb0bcc847723296500fc3c10e763 100644 --- a/frontend/test/__support__/e2e/cypress.js +++ b/frontend/test/__support__/e2e/cypress.js @@ -2,372 +2,21 @@ import "@testing-library/cypress/add-commands"; import "cypress-real-events/support"; import "@cypress/skip-test/support"; import "./commands"; -import _ from "underscore"; export const version = require("../../../../version.json"); -export function snapshot(name) { - cy.request("POST", `/api/testing/snapshot/${name}`); -} - -export function restore(name = "default") { - cy.log("Restore Data Set"); - cy.request("POST", `/api/testing/restore/${name}`); -} - -// various Metabase-specific "scoping" functions like inside popover/modal/navbar/main/sidebar content area -export function popover() { - return cy.get(".PopoverContainer.PopoverContainer--open"); -} - -export function modal() { - return cy.get(".ModalContainer .ModalContent"); -} - -export function nav() { - return cy.get("nav"); -} - -export function main() { - return cy.get("nav").next(); -} - -export function sidebar() { - return cy.get("aside"); -} - -export function browse() { - // takes you to `/browse` (reflecting changes made in `0.38-collection-redesign) - return cy.get(".Nav .Icon-table_spaced"); -} - -/** - * Get the `fieldset` HTML element that we use as a filter widget container. - * - * @returns HTMLFieldSetElement - * - * @example - * // Simple SQL filter widget (works for "Text" and "Number" SQL variable types) - * filterWidget().type("123"); - * - * @example - * // Filter widget that opens some other type of a filter picker (search, dropdown, input) - * filterWidget() - * .contains("Search") - * .click(); - * - * @todo Add the ability to choose between multiple widgets using their index. - * @todo Add the ability to alias the chosen filter widget. - * @todo Extract into a separate helper file. - */ -export function filterWidget() { - return cy.get("fieldset"); -} - -// Metabase utility functions for commonly-used patterns -export function selectDashboardFilter(selection, filterName) { - selection.contains("Select…").click(); - popover() - .contains(filterName) - .click({ force: true }); -} - -export function openTable({ database = 1, table, mode = null } = {}) { - const url = "/question/new?"; - const params = new URLSearchParams({ database, table }); - - if (mode === "notebook") { - params.append("mode", mode); - } - - cy.visit(url + params.toString()); -} - -export function openProductsTable({ mode } = {}) { - return openTable({ table: 1, mode }); -} - -export function openOrdersTable({ mode } = {}) { - return openTable({ table: 2, mode }); -} - -export function openPeopleTable({ mode } = {}) { - return openTable({ table: 3, mode }); -} - -export function openReviewsTable({ mode } = {}) { - return openTable({ table: 4, mode }); -} - -export function setupLocalHostEmail() { - // Email info - cy.findByPlaceholderText("smtp.yourservice.com").type("localhost"); - cy.findByPlaceholderText("587").type("1025"); - cy.findByText("None").click(); - // Leaves password and username blank - cy.findByPlaceholderText("metabase@yourcompany.com").type("test@local.host"); - - // *** Unnecessary click (metabase#12692) - cy.findByPlaceholderText("smtp.yourservice.com").click(); - - cy.findByText("Save changes").click(); - cy.findByText("Changes saved!"); - - cy.findByText("Send test email").click(); -} - -// 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(); -} +export * from "./helpers/e2e-setup-helpers"; +export * from "./helpers/e2e-ui-elements-helpers"; +export * from "./helpers/e2e-dashboard-helpers"; +export * from "./helpers/e2e-open-table-helpers"; +export * from "./helpers/e2e-database-metadata-helpers"; +export * from "./helpers/e2e-qa-databases-helpers"; +export * from "./helpers/e2e-ad-hoc-question-helpers"; +export * from "./helpers/e2e-enterprise-helpers"; +export * from "./helpers/e2e-mock-app-settings-helpers"; +export * from "./helpers/e2e-assertion-helpers"; +export * from "./helpers/e2e-data-model-helpers"; +export * from "./helpers/e2e-misc-helpers"; +export * from "./helpers/e2e-deprecated-helpers"; Cypress.on("uncaught:exception", (err, runnable) => false); - -export function withDatabase(databaseId, f) { - cy.request("GET", `/api/database/${databaseId}/metadata`).then(({ body }) => { - const database = {}; - for (const table of body.tables) { - const fields = {}; - for (const field of table.fields) { - fields[field.name.toUpperCase()] = field.id; - } - database[table.name.toUpperCase()] = fields; - database[table.name.toUpperCase() + "_ID"] = table.id; - } - f(database); - }); -} - -export function withSampleDataset(f) { - return withDatabase(1, f); -} - -export function visitAlias(alias) { - cy.get(alias).then(url => { - cy.visit(url); - }); -} - -export function createNativeQuestion(name, query) { - return cy.request("POST", "/api/card", { - name, - dataset_query: { - type: "native", - native: { query }, - database: 1, - }, - display: "table", - visualization_settings: {}, - }); -} - -export const describeWithToken = Cypress.env("HAS_ENTERPRISE_TOKEN") - ? describe - : describe.skip; - -// TODO: does this really need to be a global helper function? -export function createBasicAlert({ firstAlert, includeNormal } = {}) { - cy.get(".Icon-bell").click(); - if (firstAlert) { - cy.findByText("Set up an alert").click(); - } - cy.findByText("Let's set up your alert"); - if (includeNormal) { - cy.findByText("Email alerts to:") - .parent() - .children() - .last() - .click(); - cy.findByText("Robert Tableton").click(); - } - cy.findByText("Done").click(); - cy.findByText("Let's set up your alert").should("not.exist"); -} - -export function setupDummySMTP() { - cy.log("Set up dummy SMTP server"); - cy.request("PUT", "/api/setting", { - "email-smtp-host": "smtp.foo.test", - "email-smtp-port": "587", - "email-smtp-security": "none", - "email-smtp-username": "nevermind", - "email-smtp-password": "it-is-secret-NOT", - "email-from-address": "nonexisting@metabase.test", - }); -} - -export function expectedRouteCalls({ route_alias, calls } = {}) { - const requestsCount = alias => - cy.state("requests").filter(req => req.alias === alias); - // It is hard and unreliable to assert that something didn't happen in Cypress - // This solution was the only one that worked out of all others proposed in this SO topic: https://stackoverflow.com/a/59302542/8815185 - cy.get("@" + route_alias).then(() => { - expect(requestsCount(route_alias)).to.have.length(calls); - }); -} - -export function remapDisplayValueToFK({ display_value, name, fk } = {}) { - // Both display_value and fk are expected to be field IDs - // You can get them from frontend/test/__support__/e2e/cypress_sample_dataset.json - cy.request("POST", `/api/field/${display_value}/dimension`, { - field_id: display_value, - name, - human_readable_field_id: fk, - type: "external", - }); -} - -/***************************************** - ** QA DATABASES ** - ******************************************/ - -export function addMongoDatabase(name = "QA Mongo4") { - // https://hub.docker.com/layers/metabase/qa-databases/mongo-sample-4.0/images/sha256-3f568127248b6c6dba0b114b65dc3b3bf69bf4c804310eb57b4e3de6eda989cf - addQADatabase("mongo", name, 27017); -} - -export function addPostgresDatabase(name = "QA Postgres12") { - // https://hub.docker.com/layers/metabase/qa-databases/postgres-sample-12/images/sha256-80bbef27dc52552d6dc64b52796ba356d7541e7bba172740336d7b8a64859cf8 - addQADatabase("postgres", name, 5432); -} - -export function addMySQLDatabase(name = "QA MySQL8") { - // https://hub.docker.com/layers/metabase/qa-databases/mysql-sample-8/images/sha256-df67db50379ec59ac3a437b5205871f85ab519ce8d2cdc526e9313354d00f9d4 - addQADatabase("mysql", name, 3306); -} - -function addQADatabase(engine, db_display_name, port) { - const PASS_KEY = engine === "mongo" ? "pass" : "password"; - const AUTH_DB = engine === "mongo" ? "admin" : null; - const OPTIONS = engine === "mysql" ? "allowPublicKeyRetrieval=true" : null; - - cy.log(`**-- Adding ${engine.toUpperCase()} DB --**`); - cy.request("POST", "/api/database", { - engine: engine, - name: db_display_name, - details: { - dbname: "sample", - host: "localhost", - port: port, - user: "metabase", - [PASS_KEY]: "metasample123", // NOTE: we're inconsistent in where we use `pass` vs `password` as a key - authdb: AUTH_DB, - "additional-options": OPTIONS, - "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", - }, - }, - }).then(({ status }) => { - expect(status).to.equal(200); - }); - - // Make sure we have all the metadata because we'll need to use it in tests - cy.request("POST", "/api/database/2/sync_schema").then(({ status }) => { - expect(status).to.equal(200); - }); - cy.request("POST", "/api/database/2/rescan_values").then(({ status }) => { - expect(status).to.equal(200); - }); -} - -export function adhocQuestionHash(question) { - if (question.display) { - // without "locking" the display, the QB will run its picking logic and override the setting - question = Object.assign({}, question, { displayIsLocked: true }); - } - return btoa(unescape(encodeURIComponent(JSON.stringify(question)))); -} - -export function visitQuestionAdhoc(question) { - cy.visit("/question#" + adhocQuestionHash(question)); -} - -export function getIframeBody(selector = "iframe") { - return cy - .get(selector) - .its("0.contentDocument") - .should("exist") - .its("body") - .should("not.be.null") - .then(cy.wrap); -} - -function mockProperty(propertyOrObject, value, url) { - cy.intercept("GET", url, req => { - req.reply(res => { - if (typeof propertyOrObject === "object") { - Object.assign(res.body, propertyOrObject); - } - { - res.body[propertyOrObject] = value; - } - }); - }); -} - -export function mockSessionProperty(propertyOrObject, value) { - mockProperty(propertyOrObject, value, "/api/session/properties"); -} - -export function mockCurrentUserProperty(propertyOrObject, value) { - mockProperty(propertyOrObject, value, "/api/user/current"); -} - -export function showDashboardCardActions(index = 0) { - cy.get(".DashCard") - .eq(index) - .realHover(); -} - -export function generateUsers(count, groupIds) { - const users = _.range(count).map(index => ({ - first_name: `FirstName ${index}`, - last_name: `LastName ${index}`, - email: `user_${index}@metabase.com`, - password: `secure password ${index}`, - groupIds, - })); - - users.forEach(u => cy.createUserFromRawData(u)); - - return users; -} - -export function enableSharingQuestion(id) { - cy.request("POST", `/api/card/${id}/public_link`); -} - -/** - * Open native (SQL) editor and alias it. - * - * @param {string} alias - The alias that can be used later in the test as `cy.get("@" + alias)`. - * @example - * openNativeEditor().type("SELECT 123"); - */ -export function openNativeEditor(alias = "editor") { - cy.visit("/"); - cy.icon("sql").click(); - return cy - .get(".ace_content") - .as(alias) - .should("be.visible"); -} diff --git a/frontend/test/__support__/e2e/helpers/e2e-ad-hoc-question-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-ad-hoc-question-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..70638edc1c7dc864568c9b392f882bfe059daff2 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-ad-hoc-question-helpers.js @@ -0,0 +1,11 @@ +export function adhocQuestionHash(question) { + if (question.display) { + // without "locking" the display, the QB will run its picking logic and override the setting + question = Object.assign({}, question, { displayIsLocked: true }); + } + return btoa(unescape(encodeURIComponent(JSON.stringify(question)))); +} + +export function visitQuestionAdhoc(question) { + cy.visit("/question#" + adhocQuestionHash(question)); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-assertion-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-assertion-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..b4c81d9b00e837f5f3d66e2cde6501a4b176b782 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-assertion-helpers.js @@ -0,0 +1,9 @@ +export function expectedRouteCalls({ route_alias, calls } = {}) { + const requestsCount = alias => + cy.state("requests").filter(req => req.alias === alias); + // It is hard and unreliable to assert that something didn't happen in Cypress + // This solution was the only one that worked out of all others proposed in this SO topic: https://stackoverflow.com/a/59302542/8815185 + cy.get("@" + route_alias).then(() => { + expect(requestsCount(route_alias)).to.have.length(calls); + }); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-dashboard-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-dashboard-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..4d59621126c421e2824349c22396bd774da6816f --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-dashboard-helpers.js @@ -0,0 +1,15 @@ +import { popover } from "./e2e-ui-elements-helpers"; + +// Metabase utility functions for commonly-used patterns +export function selectDashboardFilter(selection, filterName) { + selection.contains("Select…").click(); + popover() + .contains(filterName) + .click({ force: true }); +} + +export function showDashboardCardActions(index = 0) { + cy.get(".DashCard") + .eq(index) + .realHover(); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-data-model-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-data-model-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..5b9a6eebec283cb9b433b7ac4ecdbfbc3f80f056 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-data-model-helpers.js @@ -0,0 +1,10 @@ +export function remapDisplayValueToFK({ display_value, name, fk } = {}) { + // Both display_value and fk are expected to be field IDs + // You can get them from frontend/test/__support__/e2e/cypress_sample_dataset.json + cy.request("POST", `/api/field/${display_value}/dimension`, { + field_id: display_value, + name, + human_readable_field_id: fk, + type: "external", + }); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-database-metadata-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-database-metadata-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..7b22d930b4188e44fc935f61ca395e044a491249 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-database-metadata-helpers.js @@ -0,0 +1,18 @@ +export function withDatabase(databaseId, f) { + cy.request("GET", `/api/database/${databaseId}/metadata`).then(({ body }) => { + const database = {}; + for (const table of body.tables) { + const fields = {}; + for (const field of table.fields) { + fields[field.name.toUpperCase()] = field.id; + } + database[table.name.toUpperCase()] = fields; + database[table.name.toUpperCase() + "_ID"] = table.id; + } + f(database); + }); +} + +export function withSampleDataset(f) { + return withDatabase(1, f); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-deprecated-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-deprecated-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..335ce2ef7f673f574afad3436f8612af8158c984 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-deprecated-helpers.js @@ -0,0 +1,60 @@ +// TODO: does this really need to be a global helper function? +export function createBasicAlert({ firstAlert, includeNormal } = {}) { + cy.get(".Icon-bell").click(); + if (firstAlert) { + cy.findByText("Set up an alert").click(); + } + cy.findByText("Let's set up your alert"); + if (includeNormal) { + cy.findByText("Email alerts to:") + .parent() + .children() + .last() + .click(); + cy.findByText("Robert Tableton").click(); + } + cy.findByText("Done").click(); + cy.findByText("Let's set up your alert").should("not.exist"); +} + +export function setupDummySMTP() { + cy.log("Set up dummy SMTP server"); + cy.request("PUT", "/api/setting", { + "email-smtp-host": "smtp.foo.test", + "email-smtp-port": "587", + "email-smtp-security": "none", + "email-smtp-username": "nevermind", + "email-smtp-password": "it-is-secret-NOT", + "email-from-address": "nonexisting@metabase.test", + }); +} + +export function createNativeQuestion(name, query) { + return cy.request("POST", "/api/card", { + name, + dataset_query: { + type: "native", + native: { query }, + database: 1, + }, + display: "table", + visualization_settings: {}, + }); +} + +export function setupLocalHostEmail() { + // Email info + cy.findByPlaceholderText("smtp.yourservice.com").type("localhost"); + cy.findByPlaceholderText("587").type("1025"); + cy.findByText("None").click(); + // Leaves password and username blank + cy.findByPlaceholderText("metabase@yourcompany.com").type("test@local.host"); + + // *** Unnecessary click (metabase#12692) + cy.findByPlaceholderText("smtp.yourservice.com").click(); + + cy.findByText("Save changes").click(); + cy.findByText("Changes saved!"); + + cy.findByText("Send test email").click(); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-enterprise-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-enterprise-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..18d38a69744b64882ac74c4603a7f391c3913220 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-enterprise-helpers.js @@ -0,0 +1,3 @@ +export const describeWithToken = Cypress.env("HAS_ENTERPRISE_TOKEN") + ? describe + : describe.skip; diff --git a/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..2b941d96b1111921a4cf9aba0045615735f4a390 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js @@ -0,0 +1,30 @@ +// 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(); +} + +export function visitAlias(alias) { + cy.get(alias).then(url => { + cy.visit(url); + }); +} + +/** + * Open native (SQL) editor and alias it. + * + * @param {string} alias - The alias that can be used later in the test as `cy.get("@" + alias)`. + * @example + * openNativeEditor().type("SELECT 123"); + */ +export function openNativeEditor(alias = "editor") { + cy.visit("/"); + cy.icon("sql").click(); + return cy + .get(".ace_content") + .as(alias) + .should("be.visible"); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-mock-app-settings-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-mock-app-settings-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..90b079eb3d57a60f11f6e9d716efcdc5c467a697 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-mock-app-settings-helpers.js @@ -0,0 +1,20 @@ +function mockProperty(propertyOrObject, value, url) { + cy.intercept("GET", url, req => { + req.reply(res => { + if (typeof propertyOrObject === "object") { + Object.assign(res.body, propertyOrObject); + } + { + res.body[propertyOrObject] = value; + } + }); + }); +} + +export function mockSessionProperty(propertyOrObject, value) { + mockProperty(propertyOrObject, value, "/api/session/properties"); +} + +export function mockCurrentUserProperty(propertyOrObject, value) { + mockProperty(propertyOrObject, value, "/api/user/current"); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-open-table-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-open-table-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..77567b9311e63d4dcee674f6e8deb0328596fff3 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-open-table-helpers.js @@ -0,0 +1,26 @@ +export function openTable({ database = 1, table, mode = null } = {}) { + const url = "/question/new?"; + const params = new URLSearchParams({ database, table }); + + if (mode === "notebook") { + params.append("mode", mode); + } + + cy.visit(url + params.toString()); +} + +export function openProductsTable({ mode } = {}) { + return openTable({ table: 1, mode }); +} + +export function openOrdersTable({ mode } = {}) { + return openTable({ table: 2, mode }); +} + +export function openPeopleTable({ mode } = {}) { + return openTable({ table: 3, mode }); +} + +export function openReviewsTable({ mode } = {}) { + return openTable({ table: 4, mode }); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-qa-databases-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-qa-databases-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..1ecda7aaf79383a88e489956ac5a5df587098798 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-qa-databases-helpers.js @@ -0,0 +1,67 @@ +/***************************************** + ** QA DATABASES ** + ******************************************/ + +export function addMongoDatabase(name = "QA Mongo4") { + // https://hub.docker.com/layers/metabase/qa-databases/mongo-sample-4.0/images/sha256-3f568127248b6c6dba0b114b65dc3b3bf69bf4c804310eb57b4e3de6eda989cf + addQADatabase("mongo", name, 27017); +} + +export function addPostgresDatabase(name = "QA Postgres12") { + // https://hub.docker.com/layers/metabase/qa-databases/postgres-sample-12/images/sha256-80bbef27dc52552d6dc64b52796ba356d7541e7bba172740336d7b8a64859cf8 + addQADatabase("postgres", name, 5432); +} + +export function addMySQLDatabase(name = "QA MySQL8") { + // https://hub.docker.com/layers/metabase/qa-databases/mysql-sample-8/images/sha256-df67db50379ec59ac3a437b5205871f85ab519ce8d2cdc526e9313354d00f9d4 + addQADatabase("mysql", name, 3306); +} + +function addQADatabase(engine, db_display_name, port) { + const PASS_KEY = engine === "mongo" ? "pass" : "password"; + const AUTH_DB = engine === "mongo" ? "admin" : null; + const OPTIONS = engine === "mysql" ? "allowPublicKeyRetrieval=true" : null; + + cy.log(`**-- Adding ${engine.toUpperCase()} DB --**`); + cy.request("POST", "/api/database", { + engine: engine, + name: db_display_name, + details: { + dbname: "sample", + host: "localhost", + port: port, + user: "metabase", + [PASS_KEY]: "metasample123", // NOTE: we're inconsistent in where we use `pass` vs `password` as a key + authdb: AUTH_DB, + "additional-options": OPTIONS, + "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", + }, + }, + }).then(({ status }) => { + expect(status).to.equal(200); + }); + + // Make sure we have all the metadata because we'll need to use it in tests + cy.request("POST", "/api/database/2/sync_schema").then(({ status }) => { + expect(status).to.equal(200); + }); + cy.request("POST", "/api/database/2/rescan_values").then(({ status }) => { + expect(status).to.equal(200); + }); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-setup-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-setup-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..35c3d4ecf83b6dfdda281dc2fe1d9a78ee538a99 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-setup-helpers.js @@ -0,0 +1,8 @@ +export function snapshot(name) { + cy.request("POST", `/api/testing/snapshot/${name}`); +} + +export function restore(name = "default") { + cy.log("Restore Data Set"); + cy.request("POST", `/api/testing/restore/${name}`); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..6dd1c3b704658f0eed8cbbbe0c5e4da686b5e6a4 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-ui-elements-helpers.js @@ -0,0 +1,40 @@ +// various Metabase-specific "scoping" functions like inside popover/modal/navbar/main/sidebar content area +export function popover() { + return cy.get(".PopoverContainer.PopoverContainer--open"); +} + +export function modal() { + return cy.get(".ModalContainer .ModalContent"); +} + +export function sidebar() { + return cy.get("aside"); +} + +export function browse() { + // takes you to `/browse` (reflecting changes made in `0.38-collection-redesign) + return cy.get(".Nav .Icon-table_spaced"); +} + +/** + * Get the `fieldset` HTML element that we use as a filter widget container. + * + * @returns HTMLFieldSetElement + * + * @example + * // Simple SQL filter widget (works for "Text" and "Number" SQL variable types) + * filterWidget().type("123"); + * + * @example + * // Filter widget that opens some other type of a filter picker (search, dropdown, input) + * filterWidget() + * .contains("Search") + * .click(); + * + * @todo Add the ability to choose between multiple widgets using their index. + * @todo Add the ability to alias the chosen filter widget. + * @todo Extract into a separate helper file. + */ +export function filterWidget() { + return cy.get("fieldset"); +} diff --git a/frontend/test/metabase/scenarios/admin/people/people.cy.spec.js b/frontend/test/metabase/scenarios/admin/people/people.cy.spec.js index 93ba92c62402dbaf2f2a2315c754e106459c6e37..d7cd4928a6d5680c85205c66ff8166cc7b971bce 100644 --- a/frontend/test/metabase/scenarios/admin/people/people.cy.spec.js +++ b/frontend/test/metabase/scenarios/admin/people/people.cy.spec.js @@ -1,9 +1,5 @@ -import { - restore, - popover, - setupDummySMTP, - generateUsers, -} from "__support__/e2e/cypress"; +import _ from "underscore"; +import { restore, popover, setupDummySMTP } from "__support__/e2e/cypress"; import { USERS, USER_GROUPS } from "__support__/e2e/cypress_data"; const { normal, admin } = USERS; @@ -288,3 +284,17 @@ function clickButton(button_name) { function assertTableRowsCount(length) { cy.get(".ContentTable tbody tr").should("have.length", length); } + +function generateUsers(count, groupIds) { + const users = _.range(count).map(index => ({ + first_name: `FirstName ${index}`, + last_name: `LastName ${index}`, + email: `user_${index}@metabase.com`, + password: `secure password ${index}`, + groupIds, + })); + + users.forEach(u => cy.createUserFromRawData(u)); + + return users; +} diff --git a/frontend/test/metabase/scenarios/question/public/question.cy.spec.js b/frontend/test/metabase/scenarios/question/public/question.cy.spec.js index e51ad8ceba495e21bfab1385d6a13242ea5c1661..197ee73554a4b360e321246657cc29527d62fb4c 100644 --- a/frontend/test/metabase/scenarios/question/public/question.cy.spec.js +++ b/frontend/test/metabase/scenarios/question/public/question.cy.spec.js @@ -1,4 +1,4 @@ -import { enableSharingQuestion, restore } from "__support__/e2e/cypress"; +import { restore } from "__support__/e2e/cypress"; import { SAMPLE_DATASET } from "__support__/e2e/cypress_sample_dataset"; const { PEOPLE } = SAMPLE_DATASET; @@ -69,3 +69,7 @@ const visitPublicURL = () => { cy.visit(publicURL); }); }; + +const enableSharingQuestion = id => { + cy.request("POST", `/api/card/${id}/public_link`); +}; diff --git a/frontend/test/metabase/scenarios/visualizations/pivot_tables.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/pivot_tables.cy.spec.js index d9eead13a2dee945a248f201c0ccd43cfbbc8591..b2219b6b38a9c052e953910f7a2206d9092054b1 100644 --- a/frontend/test/metabase/scenarios/visualizations/pivot_tables.cy.spec.js +++ b/frontend/test/metabase/scenarios/visualizations/pivot_tables.cy.spec.js @@ -1,10 +1,10 @@ import { restore, visitQuestionAdhoc, - getIframeBody, popover, sidebar, } from "__support__/e2e/cypress"; + import { SAMPLE_DATASET } from "__support__/e2e/cypress_sample_dataset"; const { @@ -927,3 +927,13 @@ function dragField(startIndex, dropIndex) { .eq(dropIndex) .trigger("drop"); } + +function getIframeBody(selector = "iframe") { + return cy + .get(selector) + .its("0.contentDocument") + .should("exist") + .its("body") + .should("not.be.null") + .then(cy.wrap); +}