Skip to content
Merged
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: 19 additions & 1 deletion 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,8 +48,13 @@ 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)
.getByRole('columnheader', { name: /Elapsed time/ }),
elapsedTimeColumnHeaderLabel: this.page
.getByTestId(realTimeTableTestId)
.getByRole('columnheader', { name: /Elapsed time/ })
.getByText('Elapsed time', { exact: true }),
hostColumnHeader: this.page.getByTestId(realTimeTableTestId).getByText('Host', { exact: true }),
noQueriesAvailable: this.builders.rowByIndex('1').getByRole('alert', { name: 'No queries available' }),
Expand All @@ -57,16 +63,22 @@ export default class RealTimeAnalyticsPage extends BasePage {
.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.elapsedTimeColumnHeaderLabel.click();
};

clickHostHeader = async () => {
Expand Down Expand Up @@ -152,6 +164,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
109 changes: 109 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,112 @@ 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');

await pmmTest.step('Set up table state', async () => {
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 pmmTest.step('Verify table state in the URL', async () => {
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 pmmTest.step('Reload the page', async () => {
await page.reload();
await rta.elements.realTimeTable.waitFor({ state: 'visible' });
await rta.openFiltersIfHidden();
});

await pmmTest.step('Verify restored table state', async () => {
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 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;
const durationParameterName = 'overview.f.queryExecutionDurationMs';
let decimalMaximum = '';
let decimalMinimum = '';
let rowsBeforeFilter = 0;

await pmmTest.step('Set up decimal duration filters', async () => {
await rta.elements.realTimeTableRow.first().waitFor({ state: 'visible' });
await rta.buttons.pauseRealTimeAnalytics.click();
await rta.openFilters();

const rowCount = 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);

rowsBeforeFilter = rowCount;
decimalMinimum = String(Number(((shortestDuration + longestDuration) / 2).toFixed(2)));
decimalMaximum = String(longestDuration);

expect(longestDuration).toBeGreaterThan(shortestDuration);
await rta.inputs.minimumDuration.fill(decimalMinimum);
await rta.inputs.maximumDuration.fill(decimalMaximum);
});

await pmmTest.step('Verify filtered results', async () => {
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 durationParameterValue = await pmmTest.step('Verify duration filters in the URL', async () => {
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));

return new URL(page.url()).searchParams.get(durationParameterName);
});

await pmmTest.step('Reload and verify restored duration filters', async () => {
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);
});
},
);
32 changes: 32 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,35 @@ 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 }) => {
const { rta } = queryAnalytics;

await pmmTest.step('Set up mocked sessions', async () => {
await mocks.mockRealTimeAnalyticsSessions();
await page.goto(queryAnalytics.rtaSessionsUrl);
});

const selectedSize = await pmmTest.step('Select a different page size', async () => {
const initialSize = await rta.inputs.rowsLimit.textContent();
const pageSize = initialSize === '10' ? '25' : '10';

await rta.inputs.rowsLimit.click();
await rta.builders.rowsPerPageOption(pageSize).click();

return pageSize;
});

await pmmTest.step('Verify page size in the URL', async () => {
await expect.poll(() => new URL(page.url()).searchParams.get('sessions.pageSize')).toBe(selectedSize);
});

await pmmTest.step('Reload and verify restored page size', async () => {
await page.reload();
await expect(rta.inputs.rowsLimit).toHaveText(selectedSize);
await expect(rta.elements.sessionRows).toHaveCount(Number(selectedSize));
});
},
);
81 changes: 81 additions & 0 deletions e2e_tests/tests/qan/storedMetrics/urlState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import pmmTest from '@fixtures/pmmTest';
import { Timeouts } from '@helpers/timeouts';
import StoredMetricsPage from '@pages/qan/storedMetrics/storedMetrics.page';
import { expect } 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');

await pmmTest.step('Apply service type filter', async () => {
await mongoDbLabel.scrollIntoViewIfNeeded();
await mongoDbLabel.click();
});

await pmmTest.step('Change pagination', async () => {
const secondPaginationItem = storedMetrics.builders.paginationItem('2');

await expect(secondPaginationItem).toBeVisible({ timeout: Timeouts.THIRTY_SECONDS });
await secondPaginationItem.click();
});

await pmmTest.step('Exercise filter changes', async () => {
for (let index = 0; index < 4; index++) {
await mongoDbLabel.click();
}
});

await pmmTest.step('Exercise pagination changes', async () => {
for (const pageNumber of ['1', '2', '1', '2']) {
await storedMetrics.builders.paginationItem(pageNumber).click();
}
});

await pmmTest.step('Exercise search filter changes', async () => {
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 pmmTest.step('Verify state in the URL', async () => {
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');
});

await pmmTest.step('Verify shared URL restores state', async () => {
const sharedPage = await context.newPage();

await sharedPage.goto(page.url());

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

const errors = (await page.consoleMessages()).filter((message) => message.type() === 'error');

expect(errors).toEqual([]);
},
);
Loading