Compare commits

...
Author SHA1 Message Date
Joe LiandClaude Sonnet 5 799aab886c test(dashboard): assert no overflow after tabs resize
Address review feedback: the poll only checked that offsetWidth
decreased, which can pass from CSS layout alone even if the chart
content still overflows horizontally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 22:57:43 -07:00
Joe LiandClaude Sonnet 5 6765e64906 test(dashboard): remove dead selector constants
FILTER_BAR_SETTINGS and APPLY_FILTERS_BUTTON were orphaned from an
earlier implementation and never referenced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:33:43 -07:00
Joe Li d0aeaa0b06 test(dashboard): reuse dashboard test setup 2026-08-25 14:51:33 -07:00
Joe Li d5a8c9dfbc test(dashboard): stabilize tabs resize test 2026-08-25 14:00:52 -07:00
Joe LiandClaude Opus 4.8 5ae942a17f style(dashboard): wrap topLevelTabs locator to satisfy prettier
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 10:43:50 -07:00
Joe LiandClaude Opus 4.8 1504773f12 test(dashboard): scope top-level tab locator and use typed dataset helper
- topLevelTabs() now scopes the nav list to the immediate child (:scope >)
  so tabs from nested tab bars rendered inside the container content are not
  counted by .count()/.nth() callers.
- Replace local findDatasetIdByName(page: any) with the typed
  getDatasetByName(page: Page) helper (retry-wrapped apiGet, no new any).
Addresses review feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 10:43:49 -07:00
Joe LiandClaude Opus 4.8 8e46f5edcf test(dashboard): migrate tabs resize E2E to Playwright
Migrate the genuinely end-to-end case from the deprecated Cypress
"Dashboard tabs" suite to the Playwright framework: a chart in a hidden
tab must re-measure and refit its container when the tab is revealed
after the available width changed.

The dashboard is built hermetically (two top-level tabs, a width-
sensitive treemap in the first) via the API and the viewport is shrunk
while the treemap is hidden — replacing the original's dependency on a
seeded tabbed dashboard and native-filter-bar expansion. The test
asserts both that the container actually reflowed (guarding against a
trivial green) and that the chart refit with no horizontal overflow.

The other two original cases asserted only the active-tab CSS class and
query-on-visible behaviour (one already skipped); those are DOM/state
assertions with no backend invariant and are intentionally not migrated.

Adds DashboardPage.topLevelTabs()/switchToTopLevelTab() helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 10:43:49 -07:00
3 changed files with 234 additions and 7 deletions
@@ -47,6 +47,7 @@ type LayoutElementLabel =
export class DashboardPage {
private readonly page: Page;
private readonly filterBar: DashboardFilterBar;
private readonly dashboardTabs: Tabs;
private static readonly SELECTORS = {
DASHBOARD_HEADER: '[data-test="dashboard-header-container"]',
@@ -72,11 +73,19 @@ export class DashboardPage {
ACE_CONTENT: '.ace_content',
ACE_TEXT_INPUT: '.ace_text-input',
RESIZE_HANDLE_BOTTOM: '.resizable-container-handle--bottom',
DASHBOARD_TABS: '[data-test="dashboard-component-tabs"]',
} as const;
constructor(page: Page) {
this.page = page;
this.filterBar = new DashboardFilterBar(page);
this.dashboardTabs = new Tabs(
page,
page
.locator(DashboardPage.SELECTORS.DASHBOARD_TABS)
.first()
.locator(':scope > [data-test="nav-list"]'),
);
}
/**
@@ -216,6 +225,16 @@ export class DashboardPage {
return this.filterBar;
}
/**
* Switches to a top-level dashboard tab and waits for it to become active.
*/
async switchDashboardTab(tabName: string): Promise<void> {
await this.dashboardTabs.clickTab(tabName);
await expect
.poll(() => this.dashboardTabs.getActiveTabName())
.toBe(tabName);
}
/**
* Open the dashboard header actions menu (three-dot menu)
*/
@@ -0,0 +1,203 @@
/**
* 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 getEmptyLayout from '../../../src/dashboard/util/getEmptyLayout';
import {
BACKGROUND_TRANSPARENT,
DASHBOARD_GRID_ID,
DASHBOARD_ROOT_ID,
} from '../../../src/dashboard/util/constants';
import {
CHART_TYPE,
ROW_TYPE,
TABS_TYPE,
TAB_TYPE,
} from '../../../src/dashboard/util/componentTypes';
import { testWithAssets, expect } from '../../helpers/fixtures';
import type {
DashboardLayoutChart,
DashboardPositionJson,
} from '../../helpers/api/dashboard';
import { TIMEOUT } from '../../utils/constants';
import { DashboardPage } from '../../pages/DashboardPage';
import { createDashboardWithCharts } from './dashboard-test-helpers';
const DATASET_NAME = 'birth_names';
const WIDE_VIEWPORT = { width: 1400, height: 900 };
const NARROW_VIEWPORT = { width: 700, height: 900 };
const TABS_ID = 'TABS-TOP';
const FIRST_TAB_ID = 'TAB-A';
const SECOND_TAB_ID = 'TAB-B';
const ROW_ID = 'ROW-A';
function buildTabbedDashboardLayout(
charts: readonly DashboardLayoutChart[],
): DashboardPositionJson {
const [treemap] = charts;
if (!treemap) {
throw new Error('Tabbed dashboard layout requires a chart');
}
const emptyLayout = getEmptyLayout();
const chartKey = `CHART-${treemap.id}`;
return {
...emptyLayout,
[DASHBOARD_GRID_ID]: {
...emptyLayout[DASHBOARD_GRID_ID],
children: [TABS_ID],
},
[TABS_ID]: {
type: TABS_TYPE,
id: TABS_ID,
children: [FIRST_TAB_ID, SECOND_TAB_ID],
parents: [DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID],
meta: {},
},
[FIRST_TAB_ID]: {
type: TAB_TYPE,
id: FIRST_TAB_ID,
children: [ROW_ID],
parents: [DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, TABS_ID],
meta: {
text: 'Tab A',
defaultText: 'Tab title',
placeholder: 'Tab title',
},
},
[SECOND_TAB_ID]: {
type: TAB_TYPE,
id: SECOND_TAB_ID,
children: [],
parents: [DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, TABS_ID],
meta: {
text: 'Tab B',
defaultText: 'Tab title',
placeholder: 'Tab title',
},
},
[ROW_ID]: {
type: ROW_TYPE,
id: ROW_ID,
children: [chartKey],
parents: [DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, TABS_ID, FIRST_TAB_ID],
meta: { background: BACKGROUND_TRANSPARENT },
},
[chartKey]: {
type: CHART_TYPE,
id: chartKey,
children: [],
parents: [
DASHBOARD_ROOT_ID,
DASHBOARD_GRID_ID,
TABS_ID,
FIRST_TAB_ID,
ROW_ID,
],
meta: {
chartId: treemap.id,
width: 12,
height: 50,
sliceName: treemap.sliceName,
},
},
};
}
testWithAssets(
'chart in a hidden tab refits its container after the tab is revealed at a new width',
async ({ page, testAssets }, testInfo) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const { dashboardId, charts } = await createDashboardWithCharts(
page,
testAssets,
testInfo,
{
datasetName: DATASET_NAME,
chartNamePrefix: 'tabs',
dashboardTitlePrefix: 'tabs_resize',
chartSpecs: [
{
viz_type: 'treemap_v2',
params: {
metric: 'count',
groupby: ['gender'],
row_limit: 100,
},
},
],
buildLayout: buildTabbedDashboardLayout,
},
);
const [treemap] = charts;
if (!treemap) {
throw new Error('Dashboard setup did not create the treemap');
}
await page.setViewportSize(WIDE_VIEWPORT);
const dashboard = new DashboardPage(page);
await dashboard.gotoById(dashboardId);
await dashboard.waitForLoad();
const treemapContainer = dashboard
.getChart(treemap.id)
.locator('[data-test="chart-container"]');
await treemapContainer.waitFor({
state: 'visible',
timeout: TIMEOUT.API_RESPONSE,
});
await dashboard.waitForChartsToLoad();
const echartsHost = treemapContainer.locator('.echarts-host');
const widthAtWide = await echartsHost.evaluate(
(element: HTMLElement) => element.offsetWidth,
);
await dashboard.switchDashboardTab('Tab B');
await page.setViewportSize(NARROW_VIEWPORT);
await dashboard.switchDashboardTab('Tab A');
await treemapContainer.waitFor({
state: 'visible',
timeout: TIMEOUT.API_RESPONSE,
});
await dashboard.waitForChartsToLoad();
await expect
.poll(
() =>
echartsHost.evaluate((element: HTMLElement) => element.offsetWidth),
{
timeout: TIMEOUT.API_RESPONSE,
message: 'treemap should resize after the hidden tab is revealed',
},
)
.toBeLessThan(widthAtWide);
const { offsetWidth, scrollWidth } = await echartsHost.evaluate(
(element: HTMLElement) => ({
offsetWidth: element.offsetWidth,
scrollWidth: element.scrollWidth,
}),
);
expect(scrollWidth).toBeLessThanOrEqual(offsetWidth);
},
);
@@ -24,6 +24,7 @@ import {
apiPostDashboard,
buildSingleRowDashboardLayout,
type DashboardLayoutChart,
type DashboardPositionJson,
} from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { extractIdFromResponse } from '../../helpers/api/assertions';
@@ -236,14 +237,17 @@ interface CreateDashboardWithChartsOptions {
/** Dashboard title prefix: `${dashboardTitlePrefix}_${suffix}`. */
dashboardTitlePrefix: string;
chartSpecs: DashboardChartSpec[];
/** Custom dashboard layout; defaults to placing every chart in one row. */
buildLayout?: (
charts: readonly DashboardLayoutChart[],
) => DashboardPositionJson;
}
/**
* Builds a published dashboard via the API: creates each chart, lays them out in
* a single row, and associates them so they render. Every created chart and the
* dashboard are registered for fixture cleanup. Charts are returned in the same
* order as `chartSpecs`, so callers can pair them back to per-spec metadata by
* index.
* Builds a published dashboard via the API: creates each chart, lays them out,
* and associates them so they render. Every created chart and the dashboard are
* registered for fixture cleanup. Charts are returned in the same order as
* `chartSpecs`, so callers can pair them back to per-spec metadata by index.
*/
export async function createDashboardWithCharts(
page: Page,
@@ -282,8 +286,9 @@ export async function createDashboardWithCharts(
charts.push({ id: chartId, sliceName });
}
// Lay all charts out in a single row.
const positionJson = buildSingleRowDashboardLayout(charts);
const positionJson = options.buildLayout
? options.buildLayout(charts)
: buildSingleRowDashboardLayout(charts);
const dashResp = await apiPostDashboard(page, {
dashboard_title: `${options.dashboardTitlePrefix}_${uniqueSuffix}`,
published: true,