Compare commits

...

2 Commits

Author SHA1 Message Date
Joe Li
8785ac794f test(e2e): harden slow page loads 2026-07-14 15:25:47 -07:00
Joe Li
0a200e6114 refactor(e2e): model dashboard filter bar 2026-07-13 17:17:10 -07:00
7 changed files with 130 additions and 32 deletions

View File

@@ -0,0 +1,87 @@
/**
* 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 { Page } from '@playwright/test';
import { Button, Select } from '../core';
/**
* Dashboard native-filter bar component.
*/
export class DashboardFilterBar {
private static readonly SELECTORS = {
FILTER_VALUE: '[data-test="form-item-value"]',
APPLY_BUTTON:
'[data-test="filter-bar__apply-button"], [data-test="filterbar-action-buttons"] button[type="submit"]',
CLEAR_BUTTON: '[data-test="filter-bar__clear-button"]',
} as const;
constructor(private readonly page: Page) {}
/**
* Selects an option in a native filter by its zero-based position.
*/
async selectOption(optionText: string, index = 0): Promise<void> {
const select = new Select(
this.page,
this.page
.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();
if (!(await applyButton.isEnabled().catch(() => false))) {
return;
}
await applyButton.click();
}
/**
* Clears all native-filter values without applying the pending changes.
*/
async clearAll(): Promise<void> {
await new Button(
this.page,
DashboardFilterBar.SELECTORS.CLEAR_BUTTON,
).click();
}
private getApplyButton(): Button {
return new Button(
this.page,
this.page.locator(DashboardFilterBar.SELECTORS.APPLY_BUTTON).first(),
);
}
}

View File

@@ -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';

View File

@@ -19,6 +19,7 @@
import { Page, Download, Locator } from '@playwright/test';
import { Menu } from '../components/core';
import { DashboardFilterBar } from '../components/dashboard';
import { NativeFiltersConfigModal } from '../components/modals';
import { gotoWithRetry } from '../helpers/navigation';
import { TIMEOUT } from '../utils/constants';
@@ -28,6 +29,7 @@ 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"]',
@@ -35,12 +37,11 @@ export class DashboardPage {
// 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);
}
/**
@@ -116,6 +117,13 @@ export class DashboardPage {
return this.page.getByRole('heading', { name, exact: true });
}
/**
* Gets the dashboard native-filter bar component.
*/
getFilterBar(): DashboardFilterBar {
return this.filterBar;
}
/**
* Opens the native filters and Display Controls configuration modal.
*/
@@ -134,14 +142,7 @@ export class DashboardPage {
* 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();
await this.filterBar.applyIfEnabled();
}
/**

View File

@@ -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 { apiPostDashboard } from '../../helpers/api/dashboard';
@@ -148,23 +149,10 @@ testWithAssets(
await dashboardPage.gotoById(dashboardId);
await dashboardPage.waitForLoad({ timeout: TIMEOUT.SLOW_TEST });
await dashboardPage.waitForChartsToLoad();
const filterBar = dashboardPage.getFilterBar();
// 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(
@@ -173,21 +161,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);
@@ -209,7 +196,7 @@ testWithAssets(
r.request().method() === 'POST',
{ timeout: 10_000 },
);
await applyBtn.first().click();
await filterBar.apply();
await applyAfterClearPromise;
},
);

View File

@@ -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: 30_000 });
});
test.afterEach(async () => {

View File

@@ -39,6 +39,7 @@ import { apiPostChart, apiPutChart } from '../../helpers/api/chart';
import { apiPostDashboard } from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { DashboardPage } from '../../pages/DashboardPage';
import { TIMEOUT } from '../../utils/constants';
const DATASET_NAME = 'birth_names';
@@ -51,6 +52,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`);

View File

@@ -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.API_RESPONSE });
// Verify correct dataset is loaded in datasource control
const loadedDatasetName = await explorePage.getDatasetName();