From 980291218ddb617f3b997d65d4f0de08596ece4a Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Fri, 11 Oct 2024 22:46:27 -0400 Subject: [PATCH] Add e2e tests for native query drills (#48503) --- .../api/createNativeQuestionAndDashboard.ts | 46 ++ e2e/support/helpers/api/index.ts | 1 + .../helpers/e2e-ui-elements-helpers.js | 22 + .../multiple-column-breakouts.cy.spec.ts | 30 +- .../question/native-query-drill.cy.spec.ts | 479 ++++++++++++++++++ .../public-resource-downloads.cy.spec.ts | 3 +- .../line_chart.cy.spec.js | 49 -- 7 files changed, 551 insertions(+), 79 deletions(-) create mode 100644 e2e/support/helpers/api/createNativeQuestionAndDashboard.ts create mode 100644 e2e/test/scenarios/question/native-query-drill.cy.spec.ts diff --git a/e2e/support/helpers/api/createNativeQuestionAndDashboard.ts b/e2e/support/helpers/api/createNativeQuestionAndDashboard.ts new file mode 100644 index 00000000000..f25f828104b --- /dev/null +++ b/e2e/support/helpers/api/createNativeQuestionAndDashboard.ts @@ -0,0 +1,46 @@ +import { createNativeQuestion } from "e2e/support/helpers"; +import type { CardId, Dashboard, DashboardCard } from "metabase-types/api"; + +import { type DashboardDetails, createDashboard } from "./createDashboard"; +import type { NativeQuestionDetails } from "./createQuestion"; + +export const createNativeQuestionAndDashboard = ({ + questionDetails, + dashboardDetails, + cardDetails, +}: { + questionDetails: NativeQuestionDetails; + dashboardDetails?: DashboardDetails; + cardDetails?: Partial<DashboardCard>; +}): Cypress.Chainable< + Cypress.Response<DashboardCard> & { questionId: CardId } +> => { + return createNativeQuestion(questionDetails).then( + ({ body: { id: questionId } }) => { + return createDashboard(dashboardDetails).then( + ({ body: { id: dashboardId } }) => { + return cy + .request<Dashboard>("PUT", `/api/dashboard/${dashboardId}`, { + dashcards: [ + { + id: -1, + card_id: questionId, + // Add sane defaults for the dashboard card size + row: 0, + col: 0, + size_x: 11, + size_y: 6, + ...cardDetails, + }, + ], + }) + .then(response => ({ + ...response, + body: response.body.dashcards[0], + questionId, + })); + }, + ); + }, + ); +}; diff --git a/e2e/support/helpers/api/index.ts b/e2e/support/helpers/api/index.ts index 47d6073d9dc..ae10089e5d0 100644 --- a/e2e/support/helpers/api/index.ts +++ b/e2e/support/helpers/api/index.ts @@ -11,6 +11,7 @@ export { createDashboardWithTabs } from "./createDashboardWithTabs"; export { createModerationReview } from "./createModerationReview"; export { createNativeQuestion } from "./createNativeQuestion"; export type { NativeQuestionDetails } from "./createNativeQuestion"; +export { createNativeQuestionAndDashboard } from "./createNativeQuestionAndDashboard"; export { createPulse } from "./createPulse"; export { createQuestion } from "./createQuestion"; export type { diff --git a/e2e/support/helpers/e2e-ui-elements-helpers.js b/e2e/support/helpers/e2e-ui-elements-helpers.js index 4994cf34e5a..33154d89f99 100644 --- a/e2e/support/helpers/e2e-ui-elements-helpers.js +++ b/e2e/support/helpers/e2e-ui-elements-helpers.js @@ -284,6 +284,28 @@ export function tableHeaderClick(headerString) { }); } +export function assertTableData({ columns, firstRows = [] }) { + tableInteractive() + .findAllByTestId("header-cell") + .should("have.length", columns.length); + + columns.forEach((column, index) => { + tableInteractive() + .findAllByTestId("header-cell") + .eq(index) + .should("have.text", column); + }); + + firstRows.forEach((row, rowIndex) => { + row.forEach((cell, cellIndex) => { + tableInteractiveBody() + .findAllByTestId("cell-data") + .eq(columns.length * rowIndex + cellIndex) + .should("have.text", cell); + }); + }); +} + /** * selects the global new button * @param {*} menuItem optional, if provided, will click the New button and return the menu item with the text provided diff --git a/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts b/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts index 39d0596929b..0c76e4c41f2 100644 --- a/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts +++ b/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts @@ -3,6 +3,7 @@ import { type DashboardDetails, type StructuredQuestionDetails, assertQueryBuilderRowCount, + assertTableData, createQuestion, createQuestionAndDashboard, dragField, @@ -22,7 +23,6 @@ import { startNewQuestion, summarize, tableInteractive, - tableInteractiveBody, visitDashboard, visitEmbeddedPage, visitPublicDashboard, @@ -1825,31 +1825,3 @@ function tableHeaderClick( .eq(columnIndex) .trigger("mouseup"); } - -function assertTableData({ - columns = [], - firstRows = [], -}: { - columns: string[]; - firstRows?: string[][]; -}) { - tableInteractive() - .findAllByTestId("header-cell") - .should("have.length", columns.length); - - columns.forEach((column, index) => { - tableInteractive() - .findAllByTestId("header-cell") - .eq(index) - .should("have.text", column); - }); - - firstRows.forEach((row, rowIndex) => { - row.forEach((cell, cellIndex) => { - tableInteractiveBody() - .findAllByTestId("cell-data") - .eq(columns.length * rowIndex + cellIndex) - .should("have.text", cell); - }); - }); -} diff --git a/e2e/test/scenarios/question/native-query-drill.cy.spec.ts b/e2e/test/scenarios/question/native-query-drill.cy.spec.ts new file mode 100644 index 00000000000..ae169d541ed --- /dev/null +++ b/e2e/test/scenarios/question/native-query-drill.cy.spec.ts @@ -0,0 +1,479 @@ +import { SAMPLE_DB_ID } from "e2e/support/cypress_data"; +import { + type NativeQuestionDetails, + assertQueryBuilderRowCount, + assertTableData, + cartesianChartCircle, + createNativeQuestion, + createNativeQuestionAndDashboard, + echartsContainer, + getDashboardCard, + modal, + popover, + restore, + tableHeaderClick, + tableInteractive, + visitDashboard, + visitQuestion, + visitQuestionAdhoc, +} from "e2e/support/helpers"; + +const ordersTableQuestionDetails: NativeQuestionDetails = { + display: "table", + native: { + query: "SELECT ID, CREATED_AT, QUANTITY FROM ORDERS ORDER BY ID LIMIT 10", + }, +}; + +const peopleTableQuestionDetails: NativeQuestionDetails = { + display: "table", + native: { + query: "SELECT ID, EMAIL, CREATED_AT FROM PEOPLE ORDER BY ID LIMIT 10", + }, +}; + +const timeseriesLineQuestionDetails: NativeQuestionDetails = { + display: "line", + native: { + query: "SELECT CREATED_AT, QUANTITY FROM ORDERS ORDER BY ID LIMIT 10", + }, + visualization_settings: { + "graph.dimensions": ["CREATED_AT"], + "graph.metrics": ["QUANTITY"], + }, +}; + +const timeseriesWithCategoryLineQuestionDetails: NativeQuestionDetails = { + display: "line", + native: { + query: + "SELECT PRICE, CATEGORY, CREATED_AT FROM PRODUCTS ORDER BY ID LIMIT 10", + }, + visualization_settings: { + "graph.dimensions": ["CREATED_AT", "CATEGORY"], + "graph.metrics": ["PRICE"], + }, +}; + +const numericLineQuestionDetails: NativeQuestionDetails = { + display: "line", + native: { + query: "SELECT ID, QUANTITY FROM ORDERS ORDER BY ID LIMIT 10", + }, + visualization_settings: { + "graph.dimensions": ["ID"], + "graph.metrics": ["QUANTITY"], + }, +}; + +const pinMapQuestionDetails: NativeQuestionDetails = { + display: "map", + native: { + query: "SELECT LATITUDE, LONGITUDE FROM PEOPLE ORDER BY ID LIMIT 10", + }, + visualization_settings: { + "map.type": "pin", + "map.longitude_column": "LONGITUDE", + "map.latitude_column": "LATITUDE", + }, +}; + +const gridMapQuestionDetails: NativeQuestionDetails = { + display: "map", + native: { + query: "SELECT LATITUDE, LONGITUDE FROM PEOPLE ORDER BY ID LIMIT 10", + }, + visualization_settings: { + "map.type": "grid", + "map.longitude_column": "LONGITUDE", + "map.latitude_column": "LATITUDE", + }, +}; + +describe("scenarios > question > native query drill", () => { + beforeEach(() => { + restore(); + cy.signInAsNormalUser(); + cy.intercept("POST", "/api/dataset").as("dataset"); + cy.intercept("POST", "/api/card").as("saveCard"); + }); + + describe("query builder metadata", () => { + it("should allow to save an ad-hoc native query when attempting to drill", () => { + visitQuestionAdhoc({ + display: "table", + dataset_query: { + database: SAMPLE_DB_ID, + type: "native", + native: peopleTableQuestionDetails.native, + }, + }); + cy.wait("@dataset"); + + tableInteractive().findByText("October 7, 2023, 1:34 AM").click(); + popover().within(() => { + cy.findByText("Filter by this date").should("not.exist"); + cy.button("Save").click(); + }); + modal().within(() => { + cy.findByLabelText("Name").type("SQL"); + cy.button("Save").click(); + cy.wait("@saveCard"); + }); + modal().findByText("Not now").click(); + + tableInteractive().findByText("October 7, 2023, 1:34 AM").click(); + popover().within(() => { + cy.findByText("Filter by this date").should("be.visible"); + cy.findByText("On").click(); + }); + cy.wait("@dataset"); + assertQueryBuilderRowCount(1); + }); + }); + + describe("query builder drills", () => { + it("column-extract drill", () => { + cy.log("from column header"); + createNativeQuestion(ordersTableQuestionDetails, { + visitQuestion: true, + wrapId: true, + }); + tableHeaderClick("CREATED_AT"); + popover().within(() => { + cy.findByText("Extract day, month…").click(); + cy.findByText("Quarter of year").click(); + cy.wait("@dataset"); + }); + assertTableData({ + columns: ["ID", "CREATED_AT", "QUANTITY", "Quarter of year"], + firstRows: [ + ["1", "February 11, 2025, 9:40 PM", "2", "Q1"], + ["2", "May 15, 2024, 8:04 AM", "3", "Q2"], + ], + }); + + cy.log("from plus button"); + visitQuestion("@questionId"); + tableInteractive().button("Add column").click(); + popover().within(() => { + cy.findByText("Extract part of column").click(); + cy.findByText("CREATED_AT").click(); + cy.findByText("Quarter of year").click(); + cy.wait("@dataset"); + }); + assertTableData({ + columns: ["ID", "CREATED_AT", "QUANTITY", "Quarter of year"], + firstRows: [ + ["1", "February 11, 2025, 9:40 PM", "2", "Q1"], + ["2", "May 15, 2024, 8:04 AM", "3", "Q2"], + ], + }); + }); + + it("combine-columns drill", () => { + cy.log("from column header"); + createNativeQuestion(peopleTableQuestionDetails, { + visitQuestion: true, + wrapId: true, + }); + tableHeaderClick("EMAIL"); + popover().findByText("Combine columns").click(); + popover().button("Done").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["ID", "EMAIL", "CREATED_AT", "Combined EMAIL, ID"], + firstRows: [ + [ + "1", + "borer-hudson@yahoo.com", + "October 7, 2023, 1:34 AM", + "borer-hudson@yahoo.com 1", + ], + ], + }); + + cy.log("from plus button"); + visitQuestion("@questionId"); + tableInteractive().button("Add column").click(); + popover().findByText("Combine columns").click(); + popover().button("Done").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["ID", "EMAIL", "CREATED_AT", "Combined ID, EMAIL"], + firstRows: [ + [ + "1", + "borer-hudson@yahoo.com", + "October 7, 2023, 1:34 AM", + "1 borer-hudson@yahoo.com", + ], + ], + }); + }); + + it("column-filter drill", () => { + createNativeQuestion(ordersTableQuestionDetails, { visitQuestion: true }); + assertQueryBuilderRowCount(10); + tableHeaderClick("QUANTITY"); + popover().findByText("Filter by this column").click(); + popover().within(() => { + cy.findByPlaceholderText("Min").type("2"); + cy.findByPlaceholderText("Max").type("5"); + cy.button("Add filter").click(); + cy.wait("@dataset"); + }); + assertQueryBuilderRowCount(8); + }); + + it("distribution drill", () => { + createNativeQuestion(ordersTableQuestionDetails, { visitQuestion: true }); + tableHeaderClick("QUANTITY"); + popover().findByText("Distribution").click(); + cy.wait("@dataset"); + echartsContainer().within(() => { + cy.findByText("Count").should("be.visible"); + cy.findByText("QUANTITY").should("be.visible"); + }); + assertQueryBuilderRowCount(5); + }); + + it("quick-filter drill", () => { + createNativeQuestion(timeseriesLineQuestionDetails, { + visitQuestion: true, + }); + assertQueryBuilderRowCount(10); + cartesianChartCircle().eq(0).click(); + popover().within(() => { + cy.findByText("Filter by this value").should("be.visible"); + cy.findByText("=").click(); + cy.wait("@dataset"); + }); + assertQueryBuilderRowCount(3); + }); + + it("sort drill", () => { + cy.log("ascending"); + createNativeQuestion(ordersTableQuestionDetails, { + visitQuestion: true, + wrapId: true, + }); + tableHeaderClick("QUANTITY"); + popover().icon("arrow_up").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["ID", "CREATED_AT", "QUANTITY"], + firstRows: [["1", "February 11, 2025, 9:40 PM", "2"]], + }); + + cy.log("descending"); + visitQuestion("@questionId"); + tableHeaderClick("QUANTITY"); + popover().icon("arrow_down").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["ID", "CREATED_AT", "QUANTITY"], + firstRows: [["8", "June 17, 2025, 2:37 AM", "7"]], + }); + }); + + it("summarize drill", () => { + cy.log("distinct values"); + createNativeQuestion(ordersTableQuestionDetails, { + visitQuestion: true, + wrapId: true, + }); + tableHeaderClick("QUANTITY"); + popover().findByText("Distinct values").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["Distinct values of QUANTITY"], + firstRows: [["5"]], + }); + + cy.log("sum"); + visitQuestion("@questionId"); + tableHeaderClick("QUANTITY"); + popover().findByText("Sum").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["Sum of QUANTITY"], + firstRows: [["38"]], + }); + + cy.log("avg"); + visitQuestion("@questionId"); + tableHeaderClick("QUANTITY"); + popover().findByText("Avg").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["Average of QUANTITY"], + firstRows: [["3.8"]], + }); + }); + + it("summarize-column-by-time drill", () => { + createNativeQuestion(ordersTableQuestionDetails, { visitQuestion: true }); + tableHeaderClick("QUANTITY"); + popover().findByText("Sum over time").click(); + cy.wait("@dataset"); + assertTableData({ + columns: ["CREATED_AT: Month", "Sum of QUANTITY"], + firstRows: [ + ["May 2023", "3"], + ["May 2024", "3"], + ["September 2024", "5"], + ], + }); + }); + + it("unsupported drills", () => { + cy.log("aggregated cell click"); + createNativeQuestion(timeseriesLineQuestionDetails, { + visitQuestion: true, + }); + assertQueryBuilderRowCount(10); + cartesianChartCircle().eq(0).click(); + popover().within(() => { + cy.findByText(/See these/).should("not.exist"); + cy.findByText(/Breakout by/).should("not.exist"); + cy.findByText(/Automatic insights/).should("not.exist"); + }); + + cy.log("legend item click"); + createNativeQuestion(timeseriesWithCategoryLineQuestionDetails, { + visitQuestion: true, + }); + cy.findByTestId("visualization-root").findByText("Gadget").click(); + cy.findByRole("tooltip").should("not.exist"); + }); + }); + + describe("query builder brush filters", () => { + it("timeseries filter", () => { + createNativeQuestion(timeseriesLineQuestionDetails, { + visitQuestion: true, + }); + assertQueryBuilderRowCount(10); + applyBrushFilter({ left: 200, right: 800 }); + cy.wait("@dataset"); + assertQueryBuilderRowCount(4); + }); + + it("numeric filter", () => { + createNativeQuestion(numericLineQuestionDetails, { + visitQuestion: true, + }); + assertQueryBuilderRowCount(10); + applyBrushFilter({ left: 200, right: 800 }); + cy.wait("@dataset"); + assertQueryBuilderRowCount(5); + }); + + it("coordinates filter", () => { + cy.log("pin map"); + createNativeQuestion(pinMapQuestionDetails, { visitQuestion: true }); + cy.findByTestId("visualization-root").realHover(); + cy.findByTestId("visualization-root").within(() => { + cy.findByText("Save as default view").should("be.visible"); + cy.findByText("Draw box to filter").click(); + }); + applyBoxFilter({ + top: 100, + left: 100, + right: 500, + bottom: 500, + }); + cy.wait("@dataset"); + assertQueryBuilderRowCount(1); + + cy.log("grid map"); + createNativeQuestion(gridMapQuestionDetails, { visitQuestion: true }); + cy.findByTestId("visualization-root").realHover(); + cy.findByTestId("visualization-root").within(() => { + cy.findByText("Save as default view").should("be.visible"); + cy.findByText("Draw box to filter").should("not.exist"); + }); + }); + }); + + describe("dashboard drills", () => { + it("quick-filter drill", () => { + cy.log("cell click"); + createNativeQuestionAndDashboard({ + questionDetails: ordersTableQuestionDetails, + }).then(({ body }) => visitDashboard(body.dashboard_id)); + getDashboardCard().findByText("May 15, 2024, 8:04 AM").click(); + popover().within(() => { + cy.findByText("Filter by this date").should("be.visible"); + cy.findByText("On").click(); + cy.wait("@dataset"); + }); + assertQueryBuilderRowCount(1); + + cy.log("aggregated cell click"); + createNativeQuestionAndDashboard({ + questionDetails: timeseriesLineQuestionDetails, + }).then(({ body }) => visitDashboard(body.dashboard_id)); + getDashboardCard().within(() => cartesianChartCircle().eq(0).click()); + popover().within(() => { + cy.findByText("Filter by this value").should("be.visible"); + cy.findByText("=").click(); + cy.wait("@dataset"); + }); + assertQueryBuilderRowCount(3); + }); + }); + + describe("dashboard brush filters", () => { + it("timeseries filter", () => { + createNativeQuestionAndDashboard({ + questionDetails: timeseriesLineQuestionDetails, + }).then(({ body }) => visitDashboard(body.dashboard_id)); + getDashboardCard().within(() => + applyBrushFilter({ left: 100, right: 300 }), + ); + cy.wait("@dataset"); + assertQueryBuilderRowCount(4); + }); + + it("numeric filter", () => { + createNativeQuestionAndDashboard({ + questionDetails: numericLineQuestionDetails, + }).then(({ body }) => visitDashboard(body.dashboard_id)); + getDashboardCard().within(() => + applyBrushFilter({ left: 100, right: 300 }), + ); + cy.wait("@dataset"); + assertQueryBuilderRowCount(5); + }); + }); +}); + +function applyBrushFilter({ left, right }: { left: number; right: number }) { + cy.wait(100); // wait to avoid grabbing the svg before the chart redraws + + echartsContainer() + .trigger("mousedown", left, 100) + .trigger("mousemove", left, 100) + .trigger("mouseup", right, 100); +} + +function applyBoxFilter({ + top, + left, + right, + bottom, +}: { + top: number; + left: number; + right: number; + bottom: number; +}) { + cy.wait(100); // wait to avoid grabbing the svg before the chart redraws + + cy.findByTestId("visualization-root") + .realMouseDown({ x: left, y: top }) + .realMouseMove(right - left, bottom - top) + .realMouseUp({ x: right, y: bottom }); +} diff --git a/e2e/test/scenarios/sharing/public-resource-downloads.cy.spec.ts b/e2e/test/scenarios/sharing/public-resource-downloads.cy.spec.ts index 47701c11364..c20823db1a2 100644 --- a/e2e/test/scenarios/sharing/public-resource-downloads.cy.spec.ts +++ b/e2e/test/scenarios/sharing/public-resource-downloads.cy.spec.ts @@ -45,7 +45,7 @@ describeWithSnowplowEE( popover() .findByTestId("public-link-input") - .should("not.have.value", "") + .should("contain.value", "/public/") .invoke("val") .then(url => { publicLink = url as string; @@ -126,6 +126,7 @@ describeWithSnowplowEE( popover() .findByTestId("public-link-input") + .should("contain.value", "/public/") .invoke("val") .then(url => { publicLink = url as string; diff --git a/e2e/test/scenarios/visualizations-charts/line_chart.cy.spec.js b/e2e/test/scenarios/visualizations-charts/line_chart.cy.spec.js index 4cd81e921cd..e9736df398f 100644 --- a/e2e/test/scenarios/visualizations-charts/line_chart.cy.spec.js +++ b/e2e/test/scenarios/visualizations-charts/line_chart.cy.spec.js @@ -807,55 +807,6 @@ describe("scenarios > visualizations > line chart", () => { cy.findByText(X_AXIS_VALUE); }); }); - - it( - "should apply brush filters to the native query series selecting area range when axis is a number", - { viewportHeight: 800, viewportWidth: 1280 }, - () => { - cy.createNativeQuestion( - { - native: { - query: ` - SELECT - ORDERS.QUANTITY AS "Quantity", - COUNT(*) AS "count" - FROM - ORDERS - GROUP BY - ORDERS.QUANTITY - `, - }, - display: "line", - visualization_settings: { - "graph.metrics": ["count"], - "graph.dimensions": ["Quantity"], - }, - }, - { visitQuestion: true }, - ); - - queryBuilderMain().within(() => { - echartsContainer().findByText("Quantity").should("exist"); - }); - cy.wait(100); // wait to avoid grabbing the svg before the chart redraws - - cy.findByTestId("query-visualization-root") - .trigger("mousedown", 180, 200) - .trigger("mousemove", 180, 200) - .trigger("mouseup", 220, 200); - - cy.findByTestId("filters-visibility-control").click(); - cy.findByTestId("filter-pill").should( - "contain.text", - "Quantity is between", - ); - const X_AXIS_VALUE = 8; - echartsContainer().within(() => { - cy.get("text").contains("Quantity").should("be.visible"); - cy.findByText(X_AXIS_VALUE); - }); - }, - ); }); function showTooltipForFirstCircleInSeries(seriesColor) { -- GitLab