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
*/