diff --git a/.github/workflows/percy-issue-comment.yml b/.github/workflows/percy-issue-comment.yml index adad348a9827a36b6681939e3517c961dbdaeaf9..5407755699fd6619c652ac83d851520fbaab470d 100644 --- a/.github/workflows/percy-issue-comment.yml +++ b/.github/workflows/percy-issue-comment.yml @@ -52,7 +52,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: 8 - distribution: 'temurin' + distribution: "temurin" - name: Install Clojure CLI run: | curl -O https://download.clojure.org/install/linux-install-1.10.3.933.sh && @@ -116,7 +116,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: 8 - distribution: 'temurin' + distribution: "temurin" - name: Install Clojure CLI run: | curl -O https://download.clojure.org/install/linux-install-1.10.3.933.sh && @@ -132,7 +132,6 @@ jobs: path: ~/.cache/yarn key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - run: yarn install --frozen-lockfile --prefer-offline - - uses: actions/download-artifact@v2 name: Retrieve uberjar artifact with: @@ -141,7 +140,8 @@ jobs: run: | jar xf target/uberjar/metabase.jar version.properties mv version.properties resources/ - + - name: Run maildev + run: docker run -d -p 80:80 -p 25:25 maildev/maildev - name: Percy Test run: yarn run test-visual-no-build env: diff --git a/.github/workflows/percy.yml b/.github/workflows/percy.yml index a825ed2f4947aec459f8d2d3309470ea77bc018f..d933710ab870fccdf13ec5bb6a446885339c6faf 100644 --- a/.github/workflows/percy.yml +++ b/.github/workflows/percy.yml @@ -33,7 +33,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: 8 - distribution: 'temurin' + distribution: "temurin" - name: Install Clojure CLI run: | curl -O https://download.clojure.org/install/linux-install-1.10.3.933.sh && @@ -90,7 +90,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: 8 - distribution: 'temurin' + distribution: "temurin" - name: Install Clojure CLI run: | curl -O https://download.clojure.org/install/linux-install-1.10.3.933.sh && @@ -115,7 +115,8 @@ jobs: run: | jar xf target/uberjar/metabase.jar version.properties mv version.properties resources/ - + - name: Run maildev + run: docker run -d -p 80:80 -p 25:25 maildev/maildev - name: Percy Test run: yarn run test-visual-no-build env: diff --git a/frontend/test/__support__/e2e/commands.js b/frontend/test/__support__/e2e/commands.js index be5edc0f50cd87cdbcb727beea925e8e52033600..8cab51878a4fcd77db2c8faedfcb68ee2b500d4b 100644 --- a/frontend/test/__support__/e2e/commands.js +++ b/frontend/test/__support__/e2e/commands.js @@ -13,6 +13,7 @@ import "./commands/api/user"; import "./commands/api/composite/createQuestionAndDashboard"; import "./commands/api/composite/createNativeQuestionAndDashboard"; +import "./commands/api/composite/createQuestionAndAddToDashboard"; import "./commands/user/createUser"; import "./commands/user/authentication"; diff --git a/frontend/test/__support__/e2e/commands/api/composite/createQuestionAndAddToDashboard.js b/frontend/test/__support__/e2e/commands/api/composite/createQuestionAndAddToDashboard.js new file mode 100644 index 0000000000000000000000000000000000000000..f808d8c6f9fe29d670d0b68fa9730c52b3f782d9 --- /dev/null +++ b/frontend/test/__support__/e2e/commands/api/composite/createQuestionAndAddToDashboard.js @@ -0,0 +1,10 @@ +Cypress.Commands.add( + "createQuestionAndAddToDashboard", + (query, dashboardId) => { + return cy.createQuestion(query).then(response => { + cy.request("POST", `/api/dashboard/${dashboardId}/cards`, { + cardId: response.body.id, + }); + }); + }, +); diff --git a/frontend/test/__support__/e2e/commands/ui/button.js b/frontend/test/__support__/e2e/commands/ui/button.js index 016556399fce2935c0b5409531c06da414ad96ca..1b28b092ea94236723709ab101b3e7c5c134fb79 100644 --- a/frontend/test/__support__/e2e/commands/ui/button.js +++ b/frontend/test/__support__/e2e/commands/ui/button.js @@ -1,3 +1,3 @@ -Cypress.Commands.add("button", button_name => { - cy.findByRole("button", { name: button_name }); +Cypress.Commands.add("button", (button_name, timeout) => { + cy.findByRole("button", { name: button_name, timeout: timeout }); }); diff --git a/frontend/test/__support__/e2e/cypress.json b/frontend/test/__support__/e2e/cypress.json index 7efc9abc53bf615c90be7c6ade4996de6b2df943..ed9299a277ce958d0c7e5bd2f3acefcb03e48b39 100644 --- a/frontend/test/__support__/e2e/cypress.json +++ b/frontend/test/__support__/e2e/cypress.json @@ -4,6 +4,7 @@ "integrationFolder": ".", "supportFile": "frontend/test/__support__/e2e/cypress.js", "videoUploadOnPasses": false, + "chromeWebSecurity": false, "viewportHeight": 800, "viewportWidth": 1280, "retries": { diff --git a/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js index 346aadbd5821e668c96b60e35b2162b040aa936a..35a56298856ed25bc656ded0f5fdc92733de9785 100644 --- a/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js +++ b/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js @@ -94,3 +94,52 @@ export function interceptPromise(method, path) { }); return state; } + +const chainStart = Symbol(); + +/** + * Waits for all Cypress commands similarly to Promise.all. + * Helps to avoid excessive nesting and verbosity + * + * @param {Array.<Cypress.Chainable<any>>} commands - Cypress commands + * @example + * cypressWaitAll([ + * cy.createQuestionAndAddToDashboard(firstQuery, 1), + * cy.createQuestionAndAddToDashboard(secondQuery, 1), + * ]).then(() => { + * cy.visit(`/dashboard/1`); + * }); + */ +export const cypressWaitAll = function(commands) { + const _ = Cypress._; + const chain = cy.wrap(null, { log: false }); + + const stopCommand = _.find(cy.queue.commands, { + attributes: { chainerId: chain.chainerId }, + }); + + const startCommand = _.find(cy.queue.commands, { + attributes: { chainerId: commands[0].chainerId }, + }); + + const p = chain.then(() => { + return _(commands) + .map(cmd => { + return cmd[chainStart] + ? cmd[chainStart].attributes + : _.find(cy.queue.commands, { + attributes: { chainerId: cmd.chainerId }, + }).attributes; + }) + .concat(stopCommand.attributes) + .slice(1) + .flatMap(cmd => { + return cmd.prev.get("subject"); + }) + .value(); + }); + + p[chainStart] = startCommand; + + return p; +}; diff --git a/frontend/test/metabase-visual/static-visualizations/static-visualizations.cy.spec.js b/frontend/test/metabase-visual/static-visualizations/static-visualizations.cy.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..427fc68b57fdb7a0b60f360ad777bb6b547206f4 --- /dev/null +++ b/frontend/test/metabase-visual/static-visualizations/static-visualizations.cy.spec.js @@ -0,0 +1,105 @@ +import { restore, setupSMTP, cypressWaitAll } from "__support__/e2e/cypress"; +import { USERS } from "__support__/e2e/cypress_data"; +import { SAMPLE_DATASET } from "__support__/e2e/cypress_sample_dataset"; + +const { ORDERS_ID, ORDERS, PRODUCTS } = SAMPLE_DATASET; + +const { admin } = USERS; + +const visualizationTypes = ["line", "area", "bar", "combo"]; + +const SENDING_EMAIL_TIMEOUT = 30000; + +describe("static visualizations", () => { + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + setupSMTP(); + }); + + visualizationTypes.map(type => { + it(`${type} chart`, () => { + const dashboardName = `${type} charts dashboard`; + cy.createDashboard({ name: dashboardName }) + .then(({ body: { id: dashboardId } }) => { + return cypressWaitAll([ + createOneMetricTwoDimensionsQuestion(type, dashboardId), + createOneDimensionTwoMetricsQuestion(type, dashboardId), + ]).then(() => { + cy.visit(`/dashboard/${dashboardId}`); + }); + }) + .then(() => { + cy.icon("share").click(); + cy.findByText("Dashboard subscriptions").click(); + + cy.findByText("Email it").click(); + cy.findByPlaceholderText("Enter user names or email addresses") + .click() + .type(`${admin.first_name} ${admin.last_name}{enter}`) + .blur(); + + cy.button("Send email now").click(); + cy.button("Email sent", SENDING_EMAIL_TIMEOUT); + + openEmailPage(dashboardName).then(() => { + cy.percySnapshot(); + }); + }); + }); + }); +}); + +function createOneDimensionTwoMetricsQuestion(display, dashboardId) { + return cy.createQuestionAndAddToDashboard( + { + name: `${display} one dimension two metrics`, + query: { + "source-table": ORDERS_ID, + aggregation: [["count"], ["avg", ["field", ORDERS.TOTAL, null]]], + breakout: [["field", ORDERS.CREATED_AT, { "temporal-unit": "month" }]], + }, + visualization_settings: { + "graph.dimensions": ["CREATED_AT"], + "graph.metrics": ["count", "avg"], + }, + display: display, + database: 1, + }, + dashboardId, + ); +} + +function createOneMetricTwoDimensionsQuestion(display, dashboardId) { + return cy.createQuestionAndAddToDashboard( + { + name: `${display} one metric two dimensions`, + query: { + "source-table": ORDERS_ID, + aggregation: [["count"]], + breakout: [ + ["field", ORDERS.CREATED_AT, { "temporal-unit": "month" }], + ["field", PRODUCTS.CATEGORY, { "source-field": ORDERS.PRODUCT_ID }], + ], + }, + visualization_settings: { + "graph.dimensions": ["CREATED_AT", "CATEGORY"], + "graph.metrics": ["count"], + }, + display: display, + database: 1, + }, + dashboardId, + ); +} + +function openEmailPage(emailSubject) { + cy.window().then(win => (win.location.href = "http://localhost")); + cy.findByText(emailSubject).click(); + + return cy.hash().then(path => { + const htmlPath = `http://localhost${path.slice(1)}/html`; + cy.window().then(win => (win.location.href = htmlPath)); + cy.findByText(emailSubject); + }); +}