Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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();
Comment thread
travagliad marked this conversation as resolved.
Outdated
};

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
89 changes: 89 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,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);
Comment thread
kiranvuyurru marked this conversation as resolved.
Outdated
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);

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',
Comment thread
kiranvuyurru marked this conversation as resolved.
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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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