From b5934e18b366547d31226d62d2c96d2aed3d63fc Mon Sep 17 00:00:00 2001 From: kiranvuyurru Date: Wed, 5 Aug 2026 09:46:33 -0600 Subject: [PATCH 1/2] Add QAN URL state persistence tests Add comprehensive tests for URL state persistence across RTA overview, sessions, and stored metrics pages. Includes new page object methods and locators for filters, pagination, and rows-per-page controls. Adds mock helper for RTA sessions and new storedMetrics URL state test suite (PMM-T2265, PMM-T2266, PMM-T2267, PMM-T2268). --- e2e_tests/helpers/apiEndpoints.ts | 1 + e2e_tests/helpers/mocks.helper.ts | 19 ++++ .../pages/qan/rta/realTimeAnalytics.page.ts | 20 ++++- .../qan/storedMetrics/storedMetrics.page.ts | 12 ++- e2e_tests/tests/qan/rta/overview.test.ts | 89 +++++++++++++++++++ e2e_tests/tests/qan/rta/session.test.ts | 20 +++++ .../tests/qan/storedMetrics/urlState.test.ts | 70 +++++++++++++++ 7 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 e2e_tests/tests/qan/storedMetrics/urlState.test.ts diff --git a/e2e_tests/helpers/apiEndpoints.ts b/e2e_tests/helpers/apiEndpoints.ts index cf64e76bf..0bebe6fae 100644 --- a/e2e_tests/helpers/apiEndpoints.ts +++ b/e2e_tests/helpers/apiEndpoints.ts @@ -27,6 +27,7 @@ const apiEndpoints = { }, realtimeanalytics: { queriesSearch: '/v1/realtimeanalytics/queries:search', + sessions: '/v1/realtimeanalytics/sessions', sessionsStart: '/v1/realtimeanalytics/sessions:start', sessionsStop: '/v1/realtimeanalytics/sessions:stop', }, diff --git a/e2e_tests/helpers/mocks.helper.ts b/e2e_tests/helpers/mocks.helper.ts index 75654e4ac..e4cb7b292 100644 --- a/e2e_tests/helpers/mocks.helper.ts +++ b/e2e_tests/helpers/mocks.helper.ts @@ -59,6 +59,25 @@ export default class mocksHelper { await this.page.route(apiEndpoints.management.services, fulfillNoServices); }; + mockRealTimeAnalyticsSessions = async (): Promise => { + const sessions = Array.from({ length: 26 }, (_, index) => ({ + cluster_name: `mock-cluster-${String(index + 1).padStart(2, '0')}`, + collect_interval: '2s', + service_id: `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`, + service_name: `mock-service-${String(index + 1).padStart(2, '0')}`, + start_time: '2026-01-01T00:00:00Z', + status: 'SESSION_STATUS_RUNNING', + })); + + await this.page.route(apiEndpoints.realtimeanalytics.sessions, (route) => + route.fulfill({ + body: JSON.stringify({ sessions }), + contentType: 'application/json', + status: 200, + }), + ); + }; + mockSnoozedUpdate = async (updateVersion: string): Promise<{ snoozedAt: number }> => { const state = { snoozedAt: 0, snoozedVersion: '' }; diff --git a/e2e_tests/pages/qan/rta/realTimeAnalytics.page.ts b/e2e_tests/pages/qan/rta/realTimeAnalytics.page.ts index accb11051..9fa214475 100644 --- a/e2e_tests/pages/qan/rta/realTimeAnalytics.page.ts +++ b/e2e_tests/pages/qan/rta/realTimeAnalytics.page.ts @@ -27,6 +27,7 @@ export default class RealTimeAnalyticsPage extends BasePage { this.page.getByTestId(realTimeTableTestId).locator(`//tbody//tr[position()=${rowIndex}]`), rowByQueryText: (queryText: string) => this.page.getByTestId(realTimeTableTestId).locator(`tr`, { hasText: queryText }), + rowsPerPageOption: (pageSize: string) => this.page.getByRole('option', { exact: true, name: pageSize }), }; buttons = { allSessions: this.page.getByTestId('overview-table-all-sessions-button'), @@ -47,26 +48,33 @@ export default class RealTimeAnalyticsPage extends BasePage { elements = { detailsOperationId: this.page.getByTestId('operation-id-value'), detailsPane: this.page.getByTestId('query-details-pane'), + durationCells: this.page.getByTestId(realTimeTableTestId).locator('tbody tr td:nth-child(4)'), elapsedTimeColumnHeader: this.page .getByTestId(realTimeTableTestId) - .getByText('Elapsed time', { exact: true }), - hostColumnHeader: this.page.getByTestId(realTimeTableTestId).getByText('Host', { exact: true }), + .getByRole('columnheader', { name: /Elapsed time/ }), + hostColumnHeader: this.page.getByTestId(realTimeTableTestId).getByRole('columnheader', { name: /Host/ }), noQueriesAvailable: this.builders.rowByIndex('1').getByRole('alert', { name: 'No queries available' }), queryTextColumnHeader: this.page .getByTestId(realTimeTableTestId) .getByText('Query text', { exact: true }), realTimeTable: this.page.getByTestId(realTimeTableTestId), realTimeTableRow: this.page.getByTestId(realTimeTableTestId).locator('tbody tr'), + selectedSessionRows: this.page.locator('tbody input[aria-label="Toggle select row"]:checked'), + sessionRows: this.page.locator('tbody > tr'), + sessionRowSelectionCheckboxes: this.page.locator('tbody input[aria-label="Toggle select row"]'), }; inputs = { clusterService: this.page.locator('input[name = "service"]'), filterByQueryText: this.page.getByTitle('Filter by Query text'), + maximumDuration: this.page.getByRole('textbox', { name: 'Max' }), + minimumDuration: this.page.getByRole('textbox', { name: 'Min' }), realTimeServiceInput: this.page.getByTestId('realtime-service-input'), + rowsLimit: this.page.getByRole('combobox', { name: 'Rows per page' }), }; messages = {}; clickElapsedTimeHeader = async () => { - await this.elements.elapsedTimeColumnHeader.click(); + await this.elements.elapsedTimeColumnHeader.getByText('Elapsed time', { exact: true }).click(); }; clickHostHeader = async () => { @@ -152,6 +160,12 @@ export default class RealTimeAnalyticsPage extends BasePage { await this.buttons.filters.click(); }; + openFiltersIfHidden = async () => { + if (!(await this.inputs.filterByQueryText.isVisible())) { + await this.openFilters(); + } + }; + selectClusterService = async () => { await this.inputs.clusterService.click(); await this.page.getByRole('option').first().click(); diff --git a/e2e_tests/pages/qan/storedMetrics/storedMetrics.page.ts b/e2e_tests/pages/qan/storedMetrics/storedMetrics.page.ts index 4ab53f86e..2ff104467 100644 --- a/e2e_tests/pages/qan/storedMetrics/storedMetrics.page.ts +++ b/e2e_tests/pages/qan/storedMetrics/storedMetrics.page.ts @@ -8,8 +8,16 @@ const serviceTypes: AccessServiceType[] = ['mongodb', 'mysql', 'postgresql']; export default class StoredMetricsPage extends BasePage { readonly url = 'graph/d/pmm-qan/pmm-query-analytics'; builders = { + paginationItem: (pageNumber: string) => + this.grafanaIframe().getByRole('listitem', { exact: true, name: pageNumber }), serviceTypeCheckbox: (serviceType: string) => this.grafanaIframe().getByTestId(`filter-checkbox-${serviceType}`), + serviceTypeFilter: (serviceType: string) => + this.grafanaIframe().locator(`input[name="service_type;${serviceType}"]`), + serviceTypeLabel: (serviceType: string) => + this.grafanaIframe() + .locator('label') + .filter({ has: this.builders.serviceTypeFilter(serviceType) }), }; buttons = {}; elements = { @@ -21,7 +29,9 @@ export default class StoredMetricsPage extends BasePage { spinner: this.grafanaIframe().locator('//*[@data-testid="Spinner"]'), totalCount: this.grafanaIframe().locator('//*[@data-testid="qan-total-items"]'), }; - inputs = {}; + inputs = { + search: this.grafanaIframe().locator('input[name="search"]'), + }; messages = {}; verifyOnlyServiceTypeVisible = async (expected: AccessServiceType) => { diff --git a/e2e_tests/tests/qan/rta/overview.test.ts b/e2e_tests/tests/qan/rta/overview.test.ts index 7c4b103b6..6d0d2b9fd 100644 --- a/e2e_tests/tests/qan/rta/overview.test.ts +++ b/e2e_tests/tests/qan/rta/overview.test.ts @@ -241,3 +241,92 @@ pmmTest('PMM-T2252 Verify RTA overview CSV export @rta', async ({ page, queryAna expect(csvOperationIds).toEqual(uiOperationIds); }); }); + +pmmTest( + 'PMM-T2265 Verify RTA overview table state is stored in the URL and restored after refresh @rta', + async ({ page, queryAnalytics }) => { + const { rta } = queryAnalytics; + + await rta.buttons.pauseRealTimeAnalytics.click(); + await rta.filterQueriesByText('db.runCommand'); + await rta.inputs.rowsLimit.click(); + await rta.builders.rowsPerPageOption('10').click(); + await rta.clickElapsedTimeHeader(); + + await expect + .poll(() => new URL(page.url()).searchParams.get('overview.f.queryText')) + .toBe('db.runCommand'); + await expect.poll(() => new URL(page.url()).searchParams.get('overview.pageSize')).toBe('10'); + await expect.poll(() => new URL(page.url()).searchParams.get('overview.sort')).not.toBeNull(); + + const urlBeforeReload = new URL(page.url()); + const serviceIds = urlBeforeReload.searchParams.getAll('serviceIds'); + + await page.reload(); + await rta.elements.realTimeTable.waitFor({ state: 'visible' }); + await rta.openFiltersIfHidden(); + + await expect(rta.inputs.filterByQueryText).toHaveValue('db.runCommand'); + await expect(rta.inputs.rowsLimit).toHaveText('10'); + await expect(rta.elements.elapsedTimeColumnHeader).toHaveAccessibleName( + /Elapsed time Sorted by Elapsed time descending/, + ); + expect(new URL(page.url()).searchParams.getAll('serviceIds')).toEqual(serviceIds); + }, +); + +pmmTest( + 'PMM-T2266 Verify RTA elapsed-time decimal filter and URL restoration @rta', + async ({ page, queryAnalytics }) => { + const { rta } = queryAnalytics; + + await rta.elements.realTimeTableRow.first().waitFor({ state: 'visible' }); + await rta.buttons.pauseRealTimeAnalytics.click(); + await rta.openFilters(); + + const rowsBeforeFilter = await rta.elements.realTimeTableRow.count(); + const durations = (await rta.elements.durationCells.allTextContents()).map(Number.parseFloat); + const shortestDuration = Math.min(...durations); + const longestDuration = Math.max(...durations); + const decimalMinimum = String(Number(((shortestDuration + longestDuration) / 2).toFixed(2))); + const decimalMaximum = String(longestDuration); + + expect(longestDuration).toBeGreaterThan(shortestDuration); + + await rta.inputs.minimumDuration.fill(decimalMinimum); + await rta.inputs.maximumDuration.fill(decimalMaximum); + await expect + .poll(async () => { + const values = await rta.elements.durationCells.allTextContents(); + + return ( + values.length > 0 && + values.length < rowsBeforeFilter && + values.every( + (value) => + Number.parseFloat(value) >= Number(decimalMinimum) && + Number.parseFloat(value) <= Number(decimalMaximum), + ) + ); + }) + .toBeTruthy(); + + const durationParameterName = 'overview.f.queryExecutionDurationMs'; + + await expect + .poll(() => new URL(page.url()).searchParams.get(durationParameterName)) + .toEqual(expect.stringContaining(decimalMinimum)); + await expect + .poll(() => new URL(page.url()).searchParams.get(durationParameterName)) + .toEqual(expect.stringContaining(decimalMaximum)); + + const durationParameterValue = new URL(page.url()).searchParams.get(durationParameterName); + + await page.reload(); + await rta.openFiltersIfHidden(); + + await expect(rta.inputs.minimumDuration).toHaveValue(decimalMinimum); + await expect(rta.inputs.maximumDuration).toHaveValue(decimalMaximum); + expect(new URL(page.url()).searchParams.get(durationParameterName)).toBe(durationParameterValue); + }, +); diff --git a/e2e_tests/tests/qan/rta/session.test.ts b/e2e_tests/tests/qan/rta/session.test.ts index d86fb803b..9aade9d03 100644 --- a/e2e_tests/tests/qan/rta/session.test.ts +++ b/e2e_tests/tests/qan/rta/session.test.ts @@ -51,3 +51,23 @@ pmmTest('PMM-T2182 Verify overview loads when session exists @rta', async ({ api await expect(queryAnalytics.rta.elements.realTimeTable).toBeVisible(); }); }); + +pmmTest( + 'PMM-T2267 Verify RTA sessions page size is stored in the URL and restored after refresh @rta', + async ({ mocks, page, queryAnalytics }) => { + await mocks.mockRealTimeAnalyticsSessions(); + await page.goto(queryAnalytics.rtaSessionsUrl); + + const { rta } = queryAnalytics; + const initialSize = await rta.inputs.rowsLimit.textContent(); + const selectedSize = initialSize === '10' ? '25' : '10'; + + await rta.inputs.rowsLimit.click(); + await rta.builders.rowsPerPageOption(selectedSize).click(); + await expect.poll(() => new URL(page.url()).searchParams.get('sessions.pageSize')).toBe(selectedSize); + + await page.reload(); + await expect(rta.inputs.rowsLimit).toHaveText(selectedSize); + await expect(rta.elements.sessionRows).toHaveCount(Number(selectedSize)); + }, +); diff --git a/e2e_tests/tests/qan/storedMetrics/urlState.test.ts b/e2e_tests/tests/qan/storedMetrics/urlState.test.ts new file mode 100644 index 000000000..0eb78dcc6 --- /dev/null +++ b/e2e_tests/tests/qan/storedMetrics/urlState.test.ts @@ -0,0 +1,70 @@ +import pmmTest from '@fixtures/pmmTest'; +import { Timeouts } from '@helpers/timeouts'; +import StoredMetricsPage from '@pages/qan/storedMetrics/storedMetrics.page'; +import { expect, type ConsoleMessage } from '@playwright/test'; + +pmmTest.beforeEach(async ({ grafanaHelper, page, queryAnalytics }) => { + await grafanaHelper.authorize(); + await page.goto(queryAnalytics.url); + await queryAnalytics.storedMetrics.elements.iframe.waitFor({ + state: 'visible', + timeout: Timeouts.THIRTY_SECONDS, + }); +}); + +pmmTest( + 'PMM-T2268 Verify QAN shared URL restores filters and pagination @rta', + async ({ context, page, queryAnalytics }) => { + const { storedMetrics } = queryAnalytics; + const mongoDbLabel = storedMetrics.builders.serviceTypeLabel('mongodb'); + const errors: string[] = []; + + const collectErrors = (message: ConsoleMessage) => { + if (message.type() === 'error') errors.push(message.text()); + }; + + page.on('console', collectErrors); + await mongoDbLabel.scrollIntoViewIfNeeded(); + await mongoDbLabel.click(); + + const secondPaginationItem = storedMetrics.builders.paginationItem('2'); + + await expect(secondPaginationItem).toBeVisible({ timeout: Timeouts.THIRTY_SECONDS }); + await secondPaginationItem.click(); + + for (let index = 0; index < 4; index++) { + await mongoDbLabel.click(); + } + + for (const pageNumber of ['1', '2', '1', '2']) { + await storedMetrics.builders.paginationItem(pageNumber).click(); + } + + for (const value of ['a', 'ab', 'abc', '']) { + await storedMetrics.inputs.search.fill(value); + } + + // eslint-disable-next-line playwright/no-wait-for-timeout -- allow debounce and console errors to settle + await page.waitForTimeout(Timeouts.HALF_SECOND); + + await expect.poll(() => new URL(page.url()).searchParams.has('dimensionSearchText')).toBeFalsy(); + await expect.poll(() => new URL(page.url()).searchParams.get('page_number')).toBe('2'); + await expect.poll(() => new URL(page.url()).searchParams.getAll('var-service_type')).toContain('mongodb'); + + const sharedUrl = page.url(); + const sharedPage = await context.newPage(); + + await sharedPage.goto(sharedUrl); + + const sharedStoredMetrics = new StoredMetricsPage(sharedPage); + + await expect(sharedStoredMetrics.builders.serviceTypeFilter('mongodb')).toBeChecked({ + timeout: Timeouts.THIRTY_SECONDS, + }); + await expect(sharedStoredMetrics.builders.paginationItem('2')).toHaveClass(/ant-pagination-item-active/); + + page.off('console', collectErrors); + expect(errors).toEqual([]); + await sharedPage.close(); + }, +); From bd5a98bd8e4cb0c928ccf03eaaf8a0d362dd1777 Mon Sep 17 00:00:00 2001 From: kiranvuyurru Date: Wed, 5 Aug 2026 11:36:59 -0600 Subject: [PATCH 2/2] Assert RTA service IDs persist --- e2e_tests/tests/qan/rta/overview.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e_tests/tests/qan/rta/overview.test.ts b/e2e_tests/tests/qan/rta/overview.test.ts index 6d0d2b9fd..18fa33e31 100644 --- a/e2e_tests/tests/qan/rta/overview.test.ts +++ b/e2e_tests/tests/qan/rta/overview.test.ts @@ -246,6 +246,9 @@ pmmTest( 'PMM-T2265 Verify RTA overview table state is stored in the URL and restored after refresh @rta', async ({ page, queryAnalytics }) => { const { rta } = queryAnalytics; + const expectedServiceIds = new URL(page.url()).searchParams.getAll('serviceIds'); + + expect(expectedServiceIds).toHaveLength(2); await rta.buttons.pauseRealTimeAnalytics.click(); await rta.filterQueriesByText('db.runCommand'); @@ -259,8 +262,7 @@ pmmTest( await expect.poll(() => new URL(page.url()).searchParams.get('overview.pageSize')).toBe('10'); await expect.poll(() => new URL(page.url()).searchParams.get('overview.sort')).not.toBeNull(); - const urlBeforeReload = new URL(page.url()); - const serviceIds = urlBeforeReload.searchParams.getAll('serviceIds'); + expect(new URL(page.url()).searchParams.getAll('serviceIds')).toEqual(expectedServiceIds); await page.reload(); await rta.elements.realTimeTable.waitFor({ state: 'visible' }); @@ -271,7 +273,7 @@ pmmTest( await expect(rta.elements.elapsedTimeColumnHeader).toHaveAccessibleName( /Elapsed time Sorted by Elapsed time descending/, ); - expect(new URL(page.url()).searchParams.getAll('serviceIds')).toEqual(serviceIds); + expect(new URL(page.url()).searchParams.getAll('serviceIds')).toEqual(expectedServiceIds); }, );