This is an automated email from the ASF dual-hosted git repository.

aminghadersohi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 0a0a1974c50 test(playwright): select existing dashboards without 
creating duplicates (#44856)
0a0a1974c50 is described below

commit 0a0a1974c50ac46544068897b4d4f78eccceaef4
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Fri Oct 2 08:32:19 2026 +1000

    test(playwright): select existing dashboards without creating duplicates 
(#44856)
---
 .../playwright/components/modals/SaveChartModal.ts | 21 +++----
 .../explore/cross-referenced-dashboards.spec.ts    | 23 +++++++-
 .../playwright/tests/explore/explore-save.spec.ts  |  6 +-
 .../tests/mobile/mobile-dashboard.spec.ts          | 65 +++++-----------------
 4 files changed, 51 insertions(+), 64 deletions(-)

diff --git a/superset-frontend/playwright/components/modals/SaveChartModal.ts 
b/superset-frontend/playwright/components/modals/SaveChartModal.ts
index 7b74bf0a292..e125f93d21b 100644
--- a/superset-frontend/playwright/components/modals/SaveChartModal.ts
+++ b/superset-frontend/playwright/components/modals/SaveChartModal.ts
@@ -105,19 +105,20 @@ export class SaveChartModal extends Modal {
   }
 
   /**
-   * Adds the chart to an existing dashboard. Waits for the select's initial
-   * options to load before typing: the "create new" option is only offered
-   * when no loaded option has the typed title, so typing earlier leaves it as
-   * the sole match and picking it silently saves the chart to a duplicate
-   * dashboard. Relies on the workspace holding fewer dashboards than the
-   * select's page size, so the initial load contains the target and typing
-   * filters client-side without a server search.
+   * Adds the chart to an existing dashboard. A strict prefix keeps the
+   * "create new" option's label distinct from the exact target title.
+   * The exact-only click waits for either local filtering or server search
+   * to expose the existing dashboard, without selecting a partial match.
    */
   async selectExistingDashboard(dashboardTitle: string): Promise<void> {
     await this.dashboardSelect.open();
-    await this.dashboardSelect.getVisibleOptionTexts();
-    await this.dashboardSelect.type(dashboardTitle);
-    await this.dashboardSelect.clickOption(dashboardTitle);
+    await this.dashboardSelect.type(dashboardTitle.slice(0, -1));
+    await this.page
+      .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
+      .last()
+      .locator('.ant-select-item-option')
+      .getByText(dashboardTitle, { exact: true })
+      .click();
   }
 
   /**
diff --git 
a/superset-frontend/playwright/tests/explore/cross-referenced-dashboards.spec.ts
 
b/superset-frontend/playwright/tests/explore/cross-referenced-dashboards.spec.ts
index 1eab392330f..8e8d957f574 100644
--- 
a/superset-frontend/playwright/tests/explore/cross-referenced-dashboards.spec.ts
+++ 
b/superset-frontend/playwright/tests/explore/cross-referenced-dashboards.spec.ts
@@ -42,6 +42,7 @@ async function overwriteToDashboard(
   explorePage: ExplorePage,
   chartId: number,
   dashboardName: string,
+  dashboardId: number,
 ): Promise<void> {
   const saveModal = await explorePage.openSaveModal();
   await saveModal.selectSaveAction('overwrite');
@@ -50,7 +51,11 @@ async function overwriteToDashboard(
     pathMatch: true,
   });
   await saveModal.clickSave();
-  expect((await updated).ok()).toBe(true);
+  const updatedResponse = await updated;
+  expect(updatedResponse.ok()).toBe(true);
+  expect(updatedResponse.request().postDataJSON().dashboards).toContain(
+    dashboardId,
+  );
   // A duplicate here means the save created a new dashboard instead of
   // picking the existing one; it would also escape testAssets cleanup.
   expect((await getDashboardsByName(page, dashboardName)).count).toBe(1);
@@ -89,7 +94,13 @@ testWithAssets(
     });
 
     // UI-save to dashboard 1: verifies singular metadata text.
-    await overwriteToDashboard(page, explorePage, chartId, dashboard1.name);
+    await overwriteToDashboard(
+      page,
+      explorePage,
+      chartId,
+      dashboard1.name,
+      dashboard1.id,
+    );
 
     await expect(explorePage.getDashboardsMetadataText()).toHaveText(
       'Added to 1 dashboard',
@@ -97,7 +108,13 @@ testWithAssets(
 
     // UI-save to dashboard 2: verifies plural metadata text and that both
     // dashboards are listed in the submenu.
-    await overwriteToDashboard(page, explorePage, chartId, dashboard2.name);
+    await overwriteToDashboard(
+      page,
+      explorePage,
+      chartId,
+      dashboard2.name,
+      dashboard2.id,
+    );
 
     await expect(explorePage.getDashboardsMetadataText()).toHaveText(
       'Added to 2 dashboards',
diff --git a/superset-frontend/playwright/tests/explore/explore-save.spec.ts 
b/superset-frontend/playwright/tests/explore/explore-save.spec.ts
index 4afae8fce59..2bd36c94ab2 100644
--- a/superset-frontend/playwright/tests/explore/explore-save.spec.ts
+++ b/superset-frontend/playwright/tests/explore/explore-save.spec.ts
@@ -116,7 +116,11 @@ testWithAssets(
       pathMatch: true,
     });
     await saveModal2.clickSave();
-    expect((await updated).ok()).toBe(true);
+    const updatedResponse = await updated;
+    expect(updatedResponse.ok()).toBe(true);
+    expect(updatedResponse.request().postDataJSON().dashboards).toContain(
+      dashboardId,
+    );
 
     expect((await getChartsByName(page, newChartName)).count).toBe(1);
     expect((await getDashboardsByName(page, dashboardTitle)).count).toBe(1);
diff --git a/superset-frontend/playwright/tests/mobile/mobile-dashboard.spec.ts 
b/superset-frontend/playwright/tests/mobile/mobile-dashboard.spec.ts
index 5c71612a58f..56fe21ebd9e 100644
--- a/superset-frontend/playwright/tests/mobile/mobile-dashboard.spec.ts
+++ b/superset-frontend/playwright/tests/mobile/mobile-dashboard.spec.ts
@@ -24,50 +24,39 @@ import { test, expect, devices, Page } from 
'@playwright/test';
 // environment (FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}).
 import { TIMEOUT } from '../../utils/constants';
 import { URL } from '../../utils/urls';
+import { gotoWithRetry } from '../../helpers/navigation';
 
 /**
  * Mobile dashboard viewing tests verify that dashboards can be viewed
  * and interacted with on mobile devices.
  *
- * These tests assume the World Bank's Health sample dashboard exists.
+ * These tests assume the World Bank's Data sample dashboard exists.
  */
 
 // Use iPhone 12 viewport for mobile tests
 const mobileViewport = devices['iPhone 12'];
 
-/**
- * Navigates to the dashboard list, clicks the first available dashboard
- * card, and waits for navigation into that dashboard. Skips the current
- * test when no dashboards are available to open.
- */
-async function openFirstDashboard(page: Page): Promise<void> {
-  await page.goto(URL.DASHBOARD_LIST);
+/** Opens the required sample dashboard and asserts navigation succeeds. */
+async function openExampleDashboard(page: Page): Promise<void> {
+  const response = await gotoWithRetry(page, 'dashboard/world_health/');
+  expect(
+    response?.status(),
+    'world_health missing; run superset load_examples',
+  ).toBe(200);
   await page.waitForLoadState('networkidle');
-
-  const cards = page.locator('[data-test="styled-card"]');
-  const cardCount = await cards.count();
-
-  test.skip(cardCount === 0, 'No dashboards available to open on mobile');
-
-  await cards.first().click();
-
-  await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
-    timeout: TIMEOUT.PAGE_LOAD,
-  });
 }
 
 /**
- * Navigates to the World Bank's Health dashboard and returns a locator
+ * Navigates to the World Bank's Data dashboard and returns a locator
  * for its mobile filter button. Skips the current test when the fixture
  * has no native filters configured.
  */
 async function getMobileFilterButton(page: Page) {
-  // Navigate directly to the World Bank's Health dashboard, which this
+  // Navigate directly to the World Bank's Data dashboard, which this
   // spec's fixtures require, rather than an arbitrary first card from
   // the list. Whether it has native filters configured depends on the
   // fixture, so callers skip themselves when none are present.
-  await page.goto('dashboard/world_health/');
-  await page.waitForLoadState('networkidle');
+  await openExampleDashboard(page);
 
   // Give filters time to load
   await page.waitForTimeout(2000);
@@ -164,32 +153,8 @@ test.describe('Mobile Dashboard Interaction', () => {
     userAgent: mobileViewport.userAgent,
   });
 
-  // Skip this test suite if no dashboards exist
-  test.beforeAll(async ({ browser }) => {
-    // browser.newPage() does not inherit the project's `storageState`, so
-    // it must be passed explicitly to reuse the authenticated session -
-    // otherwise this check hits the login page and always finds 0 cards.
-    const page = await browser.newPage({
-      viewport: mobileViewport.viewport,
-      userAgent: mobileViewport.userAgent,
-      storageState: 'playwright/.auth/user.json',
-    });
-
-    await page.goto(URL.DASHBOARD_LIST);
-    await page.waitForLoadState('networkidle');
-
-    const cards = page.locator('[data-test="styled-card"]');
-    const cardCount = await cards.count();
-
-    await page.close();
-
-    if (cardCount === 0) {
-      test.skip();
-    }
-  });
-
   test('dashboard loads and shows charts on mobile', async ({ page }) => {
-    await openFirstDashboard(page);
+    await openExampleDashboard(page);
 
     // Dashboard content should be visible
     await expect(
@@ -210,7 +175,7 @@ test.describe('Mobile Dashboard Interaction', () => {
   });
 
   test('dashboard header shows hamburger menu on mobile', async ({ page }) => {
-    await openFirstDashboard(page);
+    await openExampleDashboard(page);
 
     // Look for the hamburger menu / more actions button
     const menuButton = page
@@ -223,7 +188,7 @@ test.describe('Mobile Dashboard Interaction', () => {
   });
 
   test('refresh dashboard works from mobile menu', async ({ page }) => {
-    await openFirstDashboard(page);
+    await openExampleDashboard(page);
 
     // Open the actions menu
     const menuButton = page

Reply via email to