From 3d8da615691cf3971688da702143250710bcba4f Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 7 Nov 2025 11:06:33 -0500 Subject: [PATCH] E2E: Improve ad-hoc filtering test (#113558) * E2E: Improve ad-hoc filtering test * remove unused import * fix some table e2es after making getCell sync --- .../adhoc-filter-from-panel.spec.ts | 121 +++++++++--------- .../panels-suite/table-kitchenSink.spec.ts | 22 ++-- e2e-playwright/panels-suite/table-utils.ts | 6 +- 3 files changed, 76 insertions(+), 73 deletions(-) diff --git a/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts b/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts index 8ad6b634a87..e47fb1b26f9 100644 --- a/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts +++ b/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts @@ -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); }); } ); diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index 7312027d3b6..a14085aa753 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -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/); diff --git a/e2e-playwright/panels-suite/table-utils.ts b/e2e-playwright/panels-suite/table-utils.ts index 6a225665542..a289c0b211a 100644 --- a/e2e-playwright/panels-suite/table-utils.ts +++ b/e2e-playwright/panels-suite/table-utils.ts @@ -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;