From ad839609c1c6d7921b78d0bed79484e50c2b3680 Mon Sep 17 00:00:00 2001
From: Emmad Usmani <emmadusmani@berkeley.edu>
Date: Wed, 31 Jan 2024 12:25:09 -0800
Subject: [PATCH] add e2e test for dashboard card and tab duplication (#38246)

Part of https://github.com/metabase/metabase/issues/38208

### Description

Adds e2e tests for dashboard card and tab duplication

### Checklist

- [x] Tests have been added/updated to cover changes in this PR
---
 e2e/support/helpers/e2e-dashboard-helpers.js  |  20 +++
 .../dashcard-replace-question.cy.spec.js      |   5 +-
 .../duplicate-dashcards-tabs.cy.spec.js       | 122 ++++++++++++++++++
 .../src/metabase/dashboard/actions/tabs.ts    |   4 +-
 4 files changed, 145 insertions(+), 6 deletions(-)
 create mode 100644 e2e/test/scenarios/dashboard-cards/duplicate-dashcards-tabs.cy.spec.js

diff --git a/e2e/support/helpers/e2e-dashboard-helpers.js b/e2e/support/helpers/e2e-dashboard-helpers.js
index a6872c44e95..f9b7bc88def 100644
--- a/e2e/support/helpers/e2e-dashboard-helpers.js
+++ b/e2e/support/helpers/e2e-dashboard-helpers.js
@@ -69,6 +69,19 @@ export function showDashboardCardActions(index = 0) {
   getDashboardCard(index).realHover({ scrollBehavior: "bottom" });
 }
 
+/**
+ * Given a dashcard HTML element, will return the element for the action icon
+ * with the given label text (e.g. "Click behavior", "Replace", "Duplicate", etc)
+ *
+ * @param {Cypress.Chainable<JQuery<HTMLElement>>} dashcardElement
+ * @param {string} labelText
+ *
+ * @returns {Cypress.Chainable<JQuery<HTMLElement>>}
+ */
+export function findDashCardAction(dashcardElement, labelText) {
+  return dashcardElement.realHover().findByLabelText(labelText);
+}
+
 export function removeDashboardCard(index = 0) {
   getDashboardCard(index)
     .realHover({ scrollBehavior: "bottom" })
@@ -172,6 +185,13 @@ export function deleteTab(tabName) {
   });
 }
 
+export function duplicateTab(tabName) {
+  cy.findByRole("tab", { name: tabName }).findByRole("button").click();
+  popover().within(() => {
+    cy.findByText("Duplicate").click();
+  });
+}
+
 export function goToTab(tabName) {
   cy.findByRole("tab", { name: tabName }).click();
 }
diff --git a/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js b/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js
index 8d16a309725..49da795cfbe 100644
--- a/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js
+++ b/e2e/test/scenarios/dashboard-cards/dashcard-replace-question.cy.spec.js
@@ -4,6 +4,7 @@ import {
   popover,
   restore,
   visitDashboard,
+  findDashCardAction,
 } from "e2e/support/helpers";
 import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
 import { USER_GROUPS } from "e2e/support/cypress_data";
@@ -244,10 +245,6 @@ function findTargetDashcard() {
   return cy.findAllByTestId("dashcard").eq(2);
 }
 
-function findDashCardAction(dashcardElement, labelText) {
-  return dashcardElement.realHover().findByLabelText(labelText);
-}
-
 function replaceQuestion(
   dashcardElement,
   { nextQuestionName, collectionName },
diff --git a/e2e/test/scenarios/dashboard-cards/duplicate-dashcards-tabs.cy.spec.js b/e2e/test/scenarios/dashboard-cards/duplicate-dashcards-tabs.cy.spec.js
new file mode 100644
index 00000000000..71f8e3f0599
--- /dev/null
+++ b/e2e/test/scenarios/dashboard-cards/duplicate-dashcards-tabs.cy.spec.js
@@ -0,0 +1,122 @@
+import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
+import {
+  dashboardCards,
+  duplicateTab,
+  filterWidget,
+  findDashCardAction,
+  getDashboardCard,
+  popover,
+  restore,
+  saveDashboard,
+  visitDashboard,
+} from "e2e/support/helpers";
+import {
+  createMockDashboardCard,
+  createMockParameter,
+} from "metabase-types/api/mocks";
+
+const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
+
+const PARAMETER = {
+  CATEGORY: createMockParameter({
+    id: "2",
+    name: "Category",
+    type: "string/=",
+  }),
+};
+
+const DASHBOARD_CREATE_INFO = {
+  parameters: Object.values(PARAMETER),
+};
+
+const MAPPED_QUESTION_CREATE_INFO = {
+  name: "Products",
+  query: { "source-table": PRODUCTS_ID },
+};
+
+function createMappedDashcard(mappedQuestionId) {
+  return createMockDashboardCard({
+    id: 1,
+    card_id: mappedQuestionId,
+    parameter_mappings: [
+      {
+        parameter_id: PARAMETER.CATEGORY.id,
+        card_id: mappedQuestionId,
+        target: ["dimension", ["field", PRODUCTS.CATEGORY, null]],
+      },
+    ],
+    row: 0,
+    col: 0,
+    size_x: 10,
+    size_y: 5,
+  });
+}
+
+describe("scenarios > dashboard cards > duplicate", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsNormalUser();
+
+    cy.createQuestion(MAPPED_QUESTION_CREATE_INFO).then(
+      ({ body: { id: mappedQuestionId } }) => {
+        cy.createDashboard(DASHBOARD_CREATE_INFO).then(
+          ({ body: { id: dashboardId } }) => {
+            cy.request("PUT", `/api/dashboard/${dashboardId}`, {
+              dashcards: [createMappedDashcard(mappedQuestionId)],
+            }).then(() => {
+              cy.wrap(dashboardId).as("dashboardId");
+            });
+          },
+        );
+      },
+    );
+  });
+
+  it("should allow the user to duplicate a dashcard", () => {
+    // 1. Confirm duplication works
+    cy.get("@dashboardId").then(dashboardId => {
+      visitDashboard(dashboardId);
+      cy.findByLabelText("Edit dashboard").click();
+    });
+
+    findDashCardAction(getDashboardCard(0), "Duplicate").click();
+    saveDashboard();
+
+    cy.findAllByText("Products").should("have.length", 2);
+
+    // 2. Confirm filter still works
+    filterWidget().click();
+    popover().within(() => {
+      cy.findByText("Gadget").click();
+    });
+    cy.button("Add filter").click();
+
+    cy.findAllByText("Incredible Bronze Pants").should("have.length", 2);
+  });
+
+  it("should allow the user to duplicate a tab", () => {
+    // 1. Confirm duplication works
+    cy.get("@dashboardId").then(dashboardId => {
+      visitDashboard(dashboardId);
+      cy.findByLabelText("Edit dashboard").click();
+    });
+
+    duplicateTab("Tab 1");
+    saveDashboard();
+
+    dashboardCards().within(() => {
+      cy.findByText("Products");
+    });
+
+    // 2. Confirm filter still works
+    filterWidget().click();
+    popover().within(() => {
+      cy.findByText("Gadget").click();
+    });
+    cy.button("Add filter").click();
+
+    dashboardCards().within(() => {
+      cy.findByText("Incredible Bronze Pants");
+    });
+  });
+});
diff --git a/frontend/src/metabase/dashboard/actions/tabs.ts b/frontend/src/metabase/dashboard/actions/tabs.ts
index fa6b6047bd3..b0354852125 100644
--- a/frontend/src/metabase/dashboard/actions/tabs.ts
+++ b/frontend/src/metabase/dashboard/actions/tabs.ts
@@ -89,7 +89,7 @@ function _createInitialTabs({
 }: {
   dashId: DashboardId;
   newTabId: DashboardTabId;
-  state: Draft<DashboardState> | DashboardState; // union type needed to fix `possibly infinite` type error https://metaboat.slack.com/archives/C505ZNNH4/p1699541570878059?thread_ts=1699520485.702539&cid=C505ZNNH4
+  state: Draft<DashboardState> | DashboardState; // union type needed to fix `possibly infinite` type error
   prevDash: StoreDashboard;
   firstTabName?: string;
   secondTabName?: string;
@@ -339,7 +339,7 @@ export const tabsReducer = createReducer<DashboardState>(
           };
 
           // We don't have card (question) data for virtual dashcards (text, heading, link, action)
-          // @ts-expect-error - possibly infinite type error https://metaboat.slack.com/archives/C505ZNNH4/p1699541570878059?thread_ts=1699520485.702539&cid=C505ZNNH4
+          // @ts-expect-error - possibly infinite type error
           if (isVirtualDashCard(sourceDashCard)) {
             return;
           }
-- 
GitLab