Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e_tests/helpers/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
19 changes: 19 additions & 0 deletions e2e_tests/helpers/mocks.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ export default class mocksHelper {
await this.page.route(apiEndpoints.management.services, fulfillNoServices);
};

mockRealTimeAnalyticsSessions = async (): Promise<void> => {
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: '' };

Expand Down
20 changes: 17 additions & 3 deletions e2e_tests/pages/qan/rta/realTimeAnalytics.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 11 additions & 1 deletion e2e_tests/pages/qan/storedMetrics/storedMetrics.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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) => {
Expand Down
91 changes: 91 additions & 0 deletions e2e_tests/tests/qan/rta/overview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,94 @@ 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;
const expectedServiceIds = new URL(page.url()).searchParams.getAll('serviceIds');

expect(expectedServiceIds).toHaveLength(2);

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();

expect(new URL(page.url()).searchParams.getAll('serviceIds')).toEqual(expectedServiceIds);

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(expectedServiceIds);
},
Comment on lines +245 to +277

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add readable pmmTest.step() blocks to the new URL-state tests.

  • e2e_tests/tests/qan/rta/overview.test.ts#L245-L275: Separate state setup, URL verification, reload, and restored-state checks.
  • e2e_tests/tests/qan/rta/overview.test.ts#L278-L331: Separate duration setup, filtered-results verification, URL verification, and restoration checks.
  • e2e_tests/tests/qan/rta/session.test.ts#L55-L72: Separate mock setup, page-size selection, URL verification, and reload checks.

As per coding guidelines, “Use Playwright's test.step() for readable test structure.”

📍 Affects 2 files
  • e2e_tests/tests/qan/rta/overview.test.ts#L245-L275 (this comment)
  • e2e_tests/tests/qan/rta/overview.test.ts#L278-L331
  • e2e_tests/tests/qan/rta/session.test.ts#L55-L72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_tests/tests/qan/rta/overview.test.ts` around lines 245 - 275, Add
readable pmmTest.step() blocks to structure the URL-state tests: in
e2e_tests/tests/qan/rta/overview.test.ts lines 245-275, separate state setup,
URL verification, reload, and restored-state checks; in lines 278-331, separate
duration setup, filtered-results verification, URL verification, and restoration
checks; in e2e_tests/tests/qan/rta/session.test.ts lines 55-72, separate mock
setup, page-size selection, URL verification, and reload checks.

Source: Coding guidelines

);

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);
},
);
20 changes: 20 additions & 0 deletions e2e_tests/tests/qan/rta/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
);
70 changes: 70 additions & 0 deletions e2e_tests/tests/qan/storedMetrics/urlState.test.ts
Original file line number Diff line number Diff line change
@@ -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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the @qan tag.

This QAN test declares only @rta. A CI selection for @qan cannot include this test. Retain @rta if that selection is also required.

As per coding guidelines, “Tag tests for CI filtering, using applicable tags such as @inventory, @dashboards, and @qan.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_tests/tests/qan/storedMetrics/urlState.test.ts` at line 16, Add the `@qan`
tag to the test title for “PMM-T2268 Verify QAN shared URL restores filters and
pagination,” while retaining the existing `@rta` tag.

Source: Coding guidelines

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make test cleanup unconditional.

If an assertion fails after Line 26, page.off('console', collectErrors) and sharedPage.close() do not run. Use try/finally so the listener is removed and the created page is closed on every path.

As per coding guidelines, “Make tests idempotent and clean up resources created during tests.”

Also applies to: 55-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_tests/tests/qan/storedMetrics/urlState.test.ts` at line 26, Wrap the test
body after the page setup and console listener registration in a try/finally
block. Keep the existing assertions and test flow in try, and move
page.off('console', collectErrors) plus sharedPage.close() into finally so both
cleanup operations run on success and failure.

Source: Coding guidelines

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();
},
);
Loading