E2E: Improve ad-hoc filtering test (#113558)

* E2E: Improve ad-hoc filtering test

* remove unused import

* fix some table e2es after making getCell sync
This commit is contained in:
Paul Marbach
2025-11-07 11:06:33 -05:00
committed by GitHub
parent d7d296df8e
commit 3d8da61569
3 changed files with 76 additions and 73 deletions
@@ -1,16 +1,9 @@
import { Page, Locator } from '@playwright/test';
import { test, expect } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/AdHocFilterTest.json';
import { getCell } from '../panels-suite/table-utils';
// Helper function to get a specific cell in a table
const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) =>
loc
.getByRole('row')
.nth(rowIdx)
.getByRole(rowIdx === 0 ? 'columnheader' : 'gridcell')
.nth(colIdx);
const fixture = require('../fixtures/prometheus-response.json');
test.describe(
'Dashboard with Table powered by Prometheus data source',
@@ -46,80 +39,90 @@ test.describe(
gotoDashboardPage,
selectors,
}) => {
// Handle query and query_range API calls
// Handle query and query_range API calls. Ideally, this would instead be directly tested against gdev-prometheus.
await page.route(/\/api\/ds\/query/, async (route) => {
const fixture = require('../fixtures/prometheus-response.json');
// during the test, we select the "inner_eval" slice to filter; this simulates the behavior
// of prometheus applying that filter and removing dataframes from the response.
if (route.request().postData()?.includes('{slice=\\\"inner_eval\\\"}')) {
fixture.results.A.frames.splice(1, 1);
const response = JSON.parse(JSON.stringify(fixture));
// This simulates the behavior of prometheus applying a filter and removing dataframes from the response where
// the label matches the selected filter. We check for either the slice being applied inline into the prometheus
// query or the adhoc filter being present in the request body of prometheus applying that filter and removing
// dataframes from the response.
const postData = route.request().postData();
const match =
postData?.match(/{slice=\\\"([\w_]+)\\\"}/) ??
postData?.match(/"adhocFilters":\[{"key":"slice","operator":"equals","value":"([\w_]+)"}\]/);
if (match) {
response.results.A.frames = response.results.A.frames.filter((frame) =>
frame.schema.fields.every((field) => !field.labels || field.labels.slice === match[1])
);
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(fixture),
body: JSON.stringify(response),
});
});
const dashboardPage = await gotoDashboardPage({ uid: dashboardUID });
const panel = dashboardPage.getByGrafanaSelector(
let panel = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title('Table powered by Prometheus')
);
await expect(panel).toBeVisible();
await expect(panel, 'panel is rendered').toBeVisible();
// Wait for the table to load completely
await expect(panel.locator('.rdg')).toBeVisible();
const table = panel.locator('.rdg');
await expect(table, 'table is rendered').toBeVisible();
// Get the first data cell in the third column (row 1, column 2)
const labelValueCell = await getCell(panel, 1, 1);
await expect(labelValueCell).toBeVisible();
const firstValue = (await getCell(table, 1, 1).textContent())!;
const secondValue = (await getCell(table, 2, 1).textContent())!;
expect(firstValue, `first cell is "${firstValue}"`).toBeTruthy();
expect(secondValue, `second cell is "${secondValue}"`).toBeTruthy();
expect(firstValue, 'first and second cell values are different').not.toBe(secondValue);
// Get the cell value before clicking the filter button
const labelValue = await labelValueCell.textContent();
expect(labelValue).toBeTruthy();
async function performTest(labelValue: string) {
// Confirm both cells are rendered before we proceed
const otherValue = labelValue === firstValue ? secondValue : firstValue;
await expect(table.getByText(labelValue), `"${labelValue}" is rendered`).toContainText(labelValue);
await expect(table.getByText(otherValue), `"${otherValue}" is rendered`).toContainText(otherValue);
const otherValueCell = await getCell(panel, 2, 1);
const otherValueLabel = await otherValueCell.textContent();
expect(otherValueLabel).toBeTruthy();
expect(otherValueLabel).not.toBe(labelValue);
// click the "Filter for value" button on the cell with the specified labelValue
await table.getByText(labelValue).hover();
table.getByText(labelValue).getByRole('button', { name: 'Filter for value' }).click();
// Hover over the first cell to trigger the appearance of filter actions
await labelValueCell.hover();
// Look for submenu items that contain the filtered value
// The adhoc filter should appear as a filter chip or within the variable controls
const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem);
await expect(submenuItems.filter({ hasText: labelValue }), `submenu contains "${labelValue}"`).toBeVisible();
await expect(
submenuItems.filter({ hasText: otherValue }),
`submenu does not contain "${otherValue}"`
).toBeHidden();
// Check if the "Filter for value" button appears on hover
const filterForValueButton = labelValueCell.getByRole('button', { name: 'Filter for value' });
await expect(filterForValueButton).toBeVisible();
// The URL parameter should contain the filter in format like: var-PromAdHoc=["columnName","=","value"]
const currentUrl = page.url();
const urlParams = new URLSearchParams(new URL(currentUrl).search);
const promAdHocParam = urlParams.get('var-PromAdHoc');
expect(promAdHocParam, `url contains "${labelValue}"`).toContain(labelValue);
expect(promAdHocParam, `url does not contain "${otherValue}"`).not.toContain(otherValue);
// Click on the "Filter for value" button
await filterForValueButton.click();
// finally, let's check that the table was updated and that the value was filtered out when the query was re-run
await expect(table.getByText(labelValue), `"${labelValue}" is still visible`).toHaveText(labelValue);
await expect(table.getByText(otherValue), `"${otherValue}" is filtered out`).toBeHidden();
// Check if the adhoc filter appears in the dashboard submenu
const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem);
await expect(submenuItems.first()).toBeVisible();
// Remove the adhoc filter by clicking the submenu item again
const filterChip = submenuItems.filter({ hasText: labelValue });
await filterChip.getByLabel(/Remove filter with key/).click();
await page.click('body', { position: { x: 0, y: 0 } }); // click outside to close the open menu from ad-hoc filters
// Look for submenu items that contain the filtered value
// The adhoc filter should appear as a filter chip or within the variable controls
const hasFilterValue = await submenuItems.filter({ hasText: labelValue! }).count();
expect(hasFilterValue).toBeGreaterThan(0);
// the "first" and "second" cells locators don't work here for some reason.
await expect(table.getByText(labelValue), `"${labelValue}" is still rendered`).toContainText(labelValue);
await expect(table.getByText(otherValue), `"${otherValue}" is rendered again`).toContainText(otherValue);
}
const hasOtherValue = await submenuItems.filter({ hasText: otherValueLabel! }).count();
expect(hasOtherValue).toBe(0);
// Check if the URL contains the var-PromAdHoc parameter with the filtered value
const currentUrl = page.url();
expect(currentUrl).toContain('var-PromAdHoc');
// The URL parameter should contain the filter in format like: var-PromAdHoc=["columnName","=","value"]
const urlParams = new URLSearchParams(new URL(currentUrl).search);
const promAdHocParam = urlParams.get('var-PromAdHoc');
expect(promAdHocParam).toBeTruthy();
expect(promAdHocParam).toContain(labelValue!);
expect(promAdHocParam).not.toContain(otherValueLabel!);
// finally, let's check that the table was updated and that the value was filtered out when the query was re-run
await expect(otherValueCell).toBeHidden();
await performTest(firstValue);
await performTest(secondValue);
});
}
);
@@ -65,11 +65,11 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// test that hover overflow works.
const loremIpsumCell = await getCell(page, 1, longTextColIdx);
const loremIpsumCell = getCell(page, 1, longTextColIdx);
await loremIpsumCell.scrollIntoViewIfNeeded();
await loremIpsumCell.hover();
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100);
await (await getCell(page, 1, longTextColIdx + 1)).hover();
await getCell(page, 1, longTextColIdx + 1).hover();
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// enable cell inspect, confirm that hover no longer triggers.
@@ -140,15 +140,15 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
).toBeVisible();
// click the "State" column header to sort it.
const stateColumnHeader = await getCell(page, 0, 1);
const stateColumnHeader = getCell(page, 0, 1);
await stateColumnHeader.getByText('Info').click();
await expect(stateColumnHeader).toHaveAttribute('aria-sort', 'ascending');
expect(getCell(page, 1, 1)).resolves.toContainText('down'); // down or down fast
await expect(getCell(page, 1, 1)).toContainText('down'); // down or down fast
await stateColumnHeader.getByText('Info').click();
await expect(stateColumnHeader).toHaveAttribute('aria-sort', 'descending');
expect(getCell(page, 1, 1)).resolves.toContainText('up'); // up or up fast
await expect(getCell(page, 1, 1)).toContainText('up'); // up or up fast
await stateColumnHeader.getByText('Info').click();
await expect(stateColumnHeader).not.toHaveAttribute('aria-sort');
@@ -171,7 +171,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
const stateColumnHeader = page.getByRole('columnheader').nth(infoColumnIdx);
// get the first value in the "State" column, filter it out, then check that it went away.
const firstStateValue = (await (await getCell(page, 1, infoColumnIdx)).textContent())!;
const firstStateValue = (await getCell(page, 1, infoColumnIdx).textContent())!;
await stateColumnHeader.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton).click();
const filterContainer = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Visualization.TableNG.Filters.Container
@@ -188,7 +188,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
await expect(filterContainer).not.toBeVisible();
// did it actually filter out our value?
await expect(getCell(page, 1, infoColumnIdx)).resolves.not.toHaveText(firstStateValue);
await expect(getCell(page, 1, infoColumnIdx)).not.toHaveText(firstStateValue);
});
test('Tests pagination, row height adjustment', async ({ gotoDashboardPage, selectors, page }) => {
@@ -289,7 +289,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
const dataLinkColIdx = await getColumnIdx(page, 'Data Link');
// Info column has a single DataLink by default.
const infoCell = await getCell(page, 1, infoColumnIdx);
const infoCell = getCell(page, 1, infoColumnIdx);
await expect(infoCell.locator('a')).toBeVisible();
expect(infoCell.locator('a')).toHaveAttribute('href');
expect(infoCell.locator('a')).not.toHaveAttribute('aria-haspopup');
@@ -306,7 +306,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
continue;
}
const cell = await getCell(page, 1, colIdx);
const cell = getCell(page, 1, colIdx);
await expect(cell.locator('a')).toBeVisible();
expect(cell.locator('a')).toHaveAttribute('href');
expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu');
@@ -319,7 +319,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
// loop thru the columns, click the links, observe that the tooltip appears, and close the tooltip.
for (let colIdx = 0; colIdx < colCount; colIdx++) {
const cell = await getCell(page, 1, colIdx);
const cell = getCell(page, 1, colIdx);
if (colIdx === infoColumnIdx) {
// the Info column should still have its single link.
expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu');
@@ -433,7 +433,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
await filterContainer.getByTitle('up', { exact: true }).locator('label').click();
await filterContainer.getByRole('button', { name: 'Ok' }).click();
const cell = await getCell(page, 1, dataLinkColumnIdx);
const cell = getCell(page, 1, dataLinkColumnIdx);
await expect(cell).toBeVisible();
await expect(cell).toHaveCSS('text-decoration', /line-through/);
+3 -3
View File
@@ -1,6 +1,6 @@
import { Page, Locator } from '@playwright/test';
export const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) =>
export const getCell = (loc: Page | Locator, rowIdx: number, colIdx: number) =>
loc
.getByRole('row')
.nth(rowIdx)
@@ -8,7 +8,7 @@ export const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: numbe
.nth(colIdx);
export const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx: number) => {
const cell = await getCell(loc, rowIdx, colIdx);
const cell = getCell(loc, rowIdx, colIdx);
return (await cell.boundingBox())?.height ?? 0;
};
@@ -18,7 +18,7 @@ export const getColumnIdx = async (loc: Page | Locator, columnName: string) => {
let result = -1;
const colCount = await loc.getByRole('columnheader').count();
for (let colIdx = 0; colIdx < colCount; colIdx++) {
const cell = await getCell(loc, 0, colIdx);
const cell = getCell(loc, 0, colIdx);
if ((await cell.textContent()) === columnName) {
result = colIdx;
break;