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

rusackas 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 5286e565e49 refactor(e2e): model dashboard filter bar (#42017)
5286e565e49 is described below

commit 5286e565e497548b7a1d30190e4d11e6998ddc87
Author: Joe Li <[email protected]>
AuthorDate: Wed Jul 29 10:41:02 2026 -0700

    refactor(e2e): model dashboard filter bar (#42017)
---
 .../components/dashboard/DashboardFilterBar.ts     | 122 +++++++++++++++++++++
 .../playwright/components/dashboard/index.ts       |  20 ++++
 .../playwright/pages/DashboardPage.ts              |  35 ++----
 .../tests/dashboard/clear-all-filters.spec.ts      |  28 ++---
 .../tests/dashboard/delete-display-control.spec.ts |   5 +-
 .../playwright/tests/dashboard/export.spec.ts      |   6 +-
 .../tests/dashboard/gauge-interval-colors.spec.ts  |   2 +
 .../playwright/tests/dataset/dataset-list.spec.ts  |   2 +-
 superset-frontend/playwright/utils/constants.ts    |   5 +
 9 files changed, 170 insertions(+), 55 deletions(-)

diff --git 
a/superset-frontend/playwright/components/dashboard/DashboardFilterBar.ts 
b/superset-frontend/playwright/components/dashboard/DashboardFilterBar.ts
new file mode 100644
index 00000000000..3d1eccf6262
--- /dev/null
+++ b/superset-frontend/playwright/components/dashboard/DashboardFilterBar.ts
@@ -0,0 +1,122 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Locator, Page } from '@playwright/test';
+import { Button, Select } from '../core';
+import { NativeFiltersConfigModal } from '../modals';
+
+/**
+ * Dashboard native-filter bar component.
+ */
+export class DashboardFilterBar {
+  private static readonly SELECTORS = {
+    ROOT: '[data-test="filter-bar"]',
+    FILTER_VALUE: '[data-test="form-item-value"]',
+    APPLY_BUTTON: '[data-test="filter-bar__apply-button"]',
+    CLEAR_BUTTON: '[data-test="filter-bar__clear-button"]',
+    SETTINGS_BUTTON: '[data-test="filterbar-orientation-icon"]',
+  } as const;
+
+  constructor(private readonly page: Page) {}
+
+  /**
+   * Waits for the filter bar controls to become interactive.
+   */
+  async waitForReady(options?: { timeout?: number }): Promise<void> {
+    await this.getApplyButton().element.waitFor({
+      state: 'visible',
+      ...options,
+    });
+  }
+
+  /**
+   * Selects an option in a native filter.
+   * @param optionText - The option text to select.
+   * @param index - The zero-based position of the filter.
+   */
+  async selectOption(optionText: string, index = 0): Promise<void> {
+    const select = new Select(
+      this.page,
+      this.root
+        .locator(DashboardFilterBar.SELECTORS.FILTER_VALUE)
+        .nth(index)
+        .getByRole('combobox'),
+    );
+    await select.open();
+    await select.clickOption(optionText);
+    await select.close();
+  }
+
+  /**
+   * Applies pending native-filter changes.
+   */
+  async apply(): Promise<void> {
+    await this.getApplyButton().click();
+  }
+
+  /**
+   * Applies pending native-filter changes when the Apply button is enabled.
+   */
+  async applyIfEnabled(): Promise<void> {
+    const applyButton = this.getApplyButton();
+    await applyButton.element.waitFor({ state: 'visible' });
+    if (await applyButton.isDisabled()) {
+      return;
+    }
+
+    await applyButton.click();
+  }
+
+  /**
+   * Clears all native-filter values without applying the pending changes.
+   */
+  async clearAll(): Promise<void> {
+    await new Button(
+      this.page,
+      this.root.locator(DashboardFilterBar.SELECTORS.CLEAR_BUTTON),
+    ).click();
+  }
+
+  /**
+   * Opens the native filters and Display Controls configuration modal.
+   */
+  async openNativeFiltersConfigModal(): Promise<NativeFiltersConfigModal> {
+    await this.root
+      .locator(DashboardFilterBar.SELECTORS.SETTINGS_BUTTON)
+      .click();
+    await this.page
+      .getByText('Add or edit filters and controls', { exact: true })
+      .click();
+
+    const modal = new NativeFiltersConfigModal(this.page);
+    await modal.waitForVisible();
+    return modal;
+  }
+
+  private getApplyButton(): Button {
+    return new Button(
+      this.page,
+      this.root.locator(DashboardFilterBar.SELECTORS.APPLY_BUTTON),
+    );
+  }
+
+  private get root(): Locator {
+    return this.page.locator(DashboardFilterBar.SELECTORS.ROOT);
+  }
+}
diff --git a/superset-frontend/playwright/components/dashboard/index.ts 
b/superset-frontend/playwright/components/dashboard/index.ts
new file mode 100644
index 00000000000..84064687fcd
--- /dev/null
+++ b/superset-frontend/playwright/components/dashboard/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { DashboardFilterBar } from './DashboardFilterBar';
diff --git a/superset-frontend/playwright/pages/DashboardPage.ts 
b/superset-frontend/playwright/pages/DashboardPage.ts
index 0c58525ae25..398a8d29887 100644
--- a/superset-frontend/playwright/pages/DashboardPage.ts
+++ b/superset-frontend/playwright/pages/DashboardPage.ts
@@ -19,7 +19,7 @@
 
 import { Page, Download, Locator } from '@playwright/test';
 import { Menu } from '../components/core';
-import { NativeFiltersConfigModal } from '../components/modals';
+import { DashboardFilterBar } from '../components/dashboard';
 import { gotoWithRetry } from '../helpers/navigation';
 import { TIMEOUT } from '../utils/constants';
 
@@ -28,19 +28,18 @@ import { TIMEOUT } from '../utils/constants';
  */
 export class DashboardPage {
   private readonly page: Page;
+  private readonly filterBar: DashboardFilterBar;
 
   private static readonly SELECTORS = {
     DASHBOARD_HEADER: '[data-test="dashboard-header-container"]',
     DASHBOARD_MENU_TRIGGER: '[data-test="actions-trigger"]',
     // The header-actions-menu is the data-test for the dropdown menu content
     HEADER_ACTIONS_MENU: '[data-test="header-actions-menu"]',
-    FILTER_BAR_SETTINGS: '[data-test="filterbar-orientation-icon"]',
-    APPLY_FILTERS_BUTTON:
-      '[data-test="filter-bar__apply-button"], 
[data-test="filterbar-action-buttons"] button[type="submit"]',
   } as const;
 
   constructor(page: Page) {
     this.page = page;
+    this.filterBar = new DashboardFilterBar(page);
   }
 
   /**
@@ -126,31 +125,11 @@ export class DashboardPage {
   }
 
   /**
-   * Opens the native filters and Display Controls configuration modal.
+   * Waits for and returns the dashboard native-filter bar component.
    */
-  async openNativeFiltersConfigModal(): Promise<NativeFiltersConfigModal> {
-    await this.page.click(DashboardPage.SELECTORS.FILTER_BAR_SETTINGS);
-    await this.page
-      .getByText('Add or edit filters and controls', { exact: true })
-      .click();
-
-    const modal = new NativeFiltersConfigModal(this.page);
-    await modal.waitForVisible();
-    return modal;
-  }
-
-  /**
-   * Applies pending native filter changes when the Apply button is enabled.
-   */
-  async applyFiltersIfEnabled(): Promise<void> {
-    const applyButton = this.page
-      .locator(DashboardPage.SELECTORS.APPLY_FILTERS_BUTTON)
-      .first();
-    if (!(await applyButton.isEnabled().catch(() => false))) {
-      return;
-    }
-
-    await applyButton.click();
+  async waitForFilterBar(): Promise<DashboardFilterBar> {
+    await this.filterBar.waitForReady();
+    return this.filterBar;
   }
 
   /**
diff --git 
a/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts 
b/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts
index 992f2740a0a..945b2633a58 100644
--- a/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts
+++ b/superset-frontend/playwright/tests/dashboard/clear-all-filters.spec.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import type { Request } from '@playwright/test';
 import { testWithAssets, expect } from '../../helpers/fixtures';
 import { apiPost, apiPut } from '../../helpers/api/requests';
 import {
@@ -129,23 +130,9 @@ testWithAssets(
     await dashboardPage.gotoById(dashboardId);
     await dashboardPage.waitForLoad({ timeout: TIMEOUT.SLOW_TEST });
     await dashboardPage.waitForChartsToLoad();
+    const filterBar = await dashboardPage.waitForFilterBar();
 
-    // The Gender select should be visible in the filter bar
-    const filterCombobox = page
-      .locator('[data-test="form-item-value"]')
-      .first()
-      .locator('[role="combobox"]');
-    await filterCombobox.click();
-    await page
-      .locator('.ant-select-item-option', { hasText: /^boy$/ })
-      .first()
-      .click();
-    // Close the dropdown
-    await page.keyboard.press('Escape');
-
-    const applyBtn = page.locator(
-      '[data-test="filter-bar__apply-button"], 
[data-test="filterbar-action-buttons"] button[type="submit"]',
-    );
+    await filterBar.selectOption('boy');
 
     // Wait for chart data to come back after Apply
     const firstApplyResponse = page.waitForResponse(
@@ -154,21 +141,20 @@ testWithAssets(
         r.request().method() === 'POST',
       { timeout: 10_000 },
     );
-    await applyBtn.first().click();
+    await filterBar.apply();
     await firstApplyResponse;
     await dashboardPage.waitForChartsToLoad();
 
     // Now track POST /api/v1/chart/data requests around Clear All
     const postsAfterClearAll: string[] = [];
-    const handler = (req: any) => {
+    const handler = (req: Request) => {
       if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') 
{
         postsAfterClearAll.push(req.url());
       }
     };
     page.on('request', handler);
 
-    const clearBtn = page.locator('[data-test="filter-bar__clear-button"]');
-    await clearBtn.click();
+    await filterBar.clearAll();
 
     // Allow time for any debounced reload to fire if the bug is present
     await page.waitForTimeout(2000);
@@ -190,7 +176,7 @@ testWithAssets(
         r.request().method() === 'POST',
       { timeout: 10_000 },
     );
-    await applyBtn.first().click();
+    await filterBar.apply();
     await applyAfterClearPromise;
   },
 );
diff --git 
a/superset-frontend/playwright/tests/dashboard/delete-display-control.spec.ts 
b/superset-frontend/playwright/tests/dashboard/delete-display-control.spec.ts
index a403d08dcf9..191390f6b91 100644
--- 
a/superset-frontend/playwright/tests/dashboard/delete-display-control.spec.ts
+++ 
b/superset-frontend/playwright/tests/dashboard/delete-display-control.spec.ts
@@ -156,6 +156,7 @@ testWithAssets(
     await dashboardPage.gotoById(dashboardId);
     await dashboardPage.waitForLoad({ timeout: 30000 });
     await dashboardPage.waitForChartsToLoad({ timeout: 8000 }).catch(() => {});
+    const filterBar = await dashboardPage.waitForFilterBar();
 
     // Both the Gender filter and the Time grain Display Control should render.
     await expect(dashboardPage.getDisplayControlsHeader()).toBeVisible();
@@ -165,7 +166,7 @@ testWithAssets(
     await shot('01-initial-bar');
 
     // 4. Open the filters config modal via the settings gear.
-    const modal = await dashboardPage.openNativeFiltersConfigModal();
+    const modal = await filterBar.openNativeFiltersConfigModal();
     await shot('02-modal-open');
 
     // 5. Delete the "Time grain" Display Control in the modal sidebar.
@@ -191,7 +192,7 @@ testWithAssets(
     );
 
     // 7. Click Apply Filters.
-    await dashboardPage.applyFiltersIfEnabled();
+    await filterBar.applyIfEnabled();
     await dashboardPage.waitForChartsToLoad({ timeout: 8000 }).catch(() => {});
     await page.waitForTimeout(1500);
     await shot('05-after-apply');
diff --git a/superset-frontend/playwright/tests/dashboard/export.spec.ts 
b/superset-frontend/playwright/tests/dashboard/export.spec.ts
index 1687bf2ecac..4ef48fa82a9 100644
--- a/superset-frontend/playwright/tests/dashboard/export.spec.ts
+++ b/superset-frontend/playwright/tests/dashboard/export.spec.ts
@@ -39,8 +39,8 @@ const downloads: { delete: () => Promise<void> }[] = [];
 
 test.describe('Dashboard Export', () => {
   // Dashboard with multiple charts needs extra time for cold-cache CI runs:
-  // waitForLoad (10s) + waitForChartsToLoad (15s) + menu + download + toast
-  test.setTimeout(60_000);
+  // waitForLoad (10s) + waitForChartsToLoad (30s) + menu + download + toast
+  test.setTimeout(90_000);
 
   test.beforeEach(async ({ page }) => {
     dashboardPage = new DashboardPage(page);
@@ -49,7 +49,7 @@ test.describe('Dashboard Export', () => {
     await dashboardPage.gotoBySlug('world_health');
     await dashboardPage.waitForLoad({ timeout: TIMEOUT.PAGE_LOAD });
     // Wait for charts to finish loading - Download menu may be disabled while 
loading
-    await dashboardPage.waitForChartsToLoad();
+    await dashboardPage.waitForChartsToLoad({ timeout: TIMEOUT.CHART_RENDER });
   });
 
   test.afterEach(async () => {
diff --git 
a/superset-frontend/playwright/tests/dashboard/gauge-interval-colors.spec.ts 
b/superset-frontend/playwright/tests/dashboard/gauge-interval-colors.spec.ts
index 60edba569f0..7be748e29cc 100644
--- a/superset-frontend/playwright/tests/dashboard/gauge-interval-colors.spec.ts
+++ b/superset-frontend/playwright/tests/dashboard/gauge-interval-colors.spec.ts
@@ -55,6 +55,8 @@ const COLOR_UNUSED_3: [number, number, number] = [90, 193, 
137];
 testWithAssets(
   'Gauge renders configured interval colors on a dashboard (#28766)',
   async ({ page, testAssets }) => {
+    testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
+
     const dataset = await getDatasetByName(page, DATASET_NAME);
     if (!dataset) {
       throw new Error(`Dataset ${DATASET_NAME} not found`);
diff --git a/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts 
b/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts
index ca0e5f666a4..146f8704730 100644
--- a/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts
+++ b/superset-frontend/playwright/tests/dataset/dataset-list.spec.ts
@@ -79,7 +79,7 @@ test('should navigate to Explore when dataset name is 
clicked', async ({
   await datasetListPage.clickDatasetName(datasetName);
 
   // Wait for Explore page to load (validates URL + datasource control)
-  await explorePage.waitForPageLoad();
+  await explorePage.waitForPageLoad({ timeout: TIMEOUT.EXPLORE_PAGE_LOAD });
 
   // Verify correct dataset is loaded in datasource control
   const loadedDatasetName = await explorePage.getDatasetName();
diff --git a/superset-frontend/playwright/utils/constants.ts 
b/superset-frontend/playwright/utils/constants.ts
index bdaff0037d0..39e1e2f7507 100644
--- a/superset-frontend/playwright/utils/constants.ts
+++ b/superset-frontend/playwright/utils/constants.ts
@@ -39,6 +39,11 @@ export const TIMEOUT = {
    */
   PAGE_LOAD: 10000, // 10s for page transitions (login → welcome, dataset → 
explore)
 
+  /**
+   * Dataset-to-Explore navigation on cold CI runners
+   */
+  EXPLORE_PAGE_LOAD: 15000, // 15s for Explore to load its datasource control
+
   /**
    * Form and UI element load timeouts
    */

Reply via email to