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