Skip to content

Commit add9ab3

Browse files
author
Anton Standrik
committed
chore: e2e-test top shards table
1 parent 0bad262 commit add9ab3

File tree

4 files changed

+309
-3
lines changed

4 files changed

+309
-3
lines changed

tests/suites/memoryViewer/memoryViewer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ test.describe('Memory Viewer Widget', () => {
6262

6363
const memTable = await memoryViewer.getMemTableInfo();
6464
expect(memTable.status).toBe('good');
65-
expect(memTable.text).toMatch(/\d+ \/ \d+\s*GB/);
65+
expect(memTable.text).toMatch(/\d+(\.\d+)? \/ \d+(\.\d+)?\s*GB/);
6666

6767
// Check simple metrics
6868
const allocatorCaches = await memoryViewer.getAllocatorCachesInfo();

tests/suites/tenant/diagnostics/Diagnostics.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ export class Table {
7878
}
7979
return headerNames;
8080
}
81-
8281
async getCellValueByHeader(row: number, header: string) {
8382
const headers = await this.getHeaders();
8483
const colIndex = headers.indexOf(header);
@@ -91,6 +90,18 @@ export class Table {
9190
return cell.innerText();
9291
}
9392

93+
async verifyHeaders(expectedHeaders: string[]) {
94+
const actualHeaders = await this.getHeaders();
95+
for (const header of expectedHeaders) {
96+
if (!actualHeaders.includes(header)) {
97+
throw new Error(
98+
`Expected header "${header}" not found in actual headers: ${actualHeaders.join(', ')}`,
99+
);
100+
}
101+
}
102+
return true;
103+
}
104+
94105
async waitForCellValueByHeader(row: number, header: string, value: string) {
95106
await retryAction(async () => {
96107
const headers = await this.getHeaders();
@@ -116,6 +127,42 @@ export enum QueriesSwitch {
116127
Running = 'Running',
117128
}
118129

130+
export enum TopShardsMode {
131+
Immediate = 'Immediate',
132+
Historical = 'Historical',
133+
}
134+
135+
const TOP_SHARDS_COLUMNS_IDS = {
136+
TabletId: 'TabletId',
137+
CPUCores: 'CPUCores',
138+
DataSize: 'DataSize (B)',
139+
Path: 'Path',
140+
NodeId: 'NodeId',
141+
PeakTime: 'PeakTime',
142+
InFlightTxCount: 'InFlightTxCount',
143+
IntervalEnd: 'IntervalEnd',
144+
} as const;
145+
146+
export const TopShardsImmediateColumns = [
147+
TOP_SHARDS_COLUMNS_IDS.Path,
148+
TOP_SHARDS_COLUMNS_IDS.CPUCores,
149+
TOP_SHARDS_COLUMNS_IDS.DataSize,
150+
TOP_SHARDS_COLUMNS_IDS.TabletId,
151+
TOP_SHARDS_COLUMNS_IDS.NodeId,
152+
TOP_SHARDS_COLUMNS_IDS.InFlightTxCount,
153+
];
154+
155+
export const TopShardsHistoricalColumns = [
156+
TOP_SHARDS_COLUMNS_IDS.Path,
157+
TOP_SHARDS_COLUMNS_IDS.CPUCores,
158+
TOP_SHARDS_COLUMNS_IDS.DataSize,
159+
TOP_SHARDS_COLUMNS_IDS.TabletId,
160+
TOP_SHARDS_COLUMNS_IDS.NodeId,
161+
TOP_SHARDS_COLUMNS_IDS.PeakTime,
162+
TOP_SHARDS_COLUMNS_IDS.InFlightTxCount,
163+
TOP_SHARDS_COLUMNS_IDS.IntervalEnd,
164+
];
165+
119166
export class Diagnostics {
120167
table: Table;
121168
storage: StoragePage;
@@ -133,6 +180,7 @@ export class Diagnostics {
133180
private storageCard: Locator;
134181
private memoryCard: Locator;
135182
private healthcheckCard: Locator;
183+
private tableRadioButton: Locator;
136184

137185
constructor(page: Page) {
138186
this.storage = new StoragePage(page);
@@ -146,6 +194,9 @@ export class Diagnostics {
146194
this.refreshButton = page.locator('button[aria-label="Refresh"]');
147195
this.autoRefreshSelect = page.locator('.g-select');
148196
this.table = new Table(page.locator('.object-general'));
197+
this.tableRadioButton = page.locator(
198+
'.ydb-table-with-controls-layout__controls .g-radio-button',
199+
);
149200

150201
// Info tab cards
151202
this.cpuCard = page.locator('.metrics-cards__tab:has-text("CPU")');
@@ -247,4 +298,14 @@ export class Diagnostics {
247298
);
248299
return (await statusElement.textContent())?.trim() || '';
249300
}
301+
302+
async selectTopShardsMode(mode: TopShardsMode): Promise<void> {
303+
const option = this.tableRadioButton.locator(`.g-radio-button__option:has-text("${mode}")`);
304+
await option.evaluate((el) => (el as HTMLElement).click());
305+
}
306+
307+
async getSelectedTopShardsMode(): Promise<string> {
308+
const checkedOption = this.tableRadioButton.locator('.g-radio-button__option_checked');
309+
return (await checkedOption.textContent())?.trim() || '';
310+
}
250311
}

tests/suites/tenant/diagnostics/diagnostics.test.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import {NavigationTabs, TenantPage} from '../TenantPage';
66
import {longRunningQuery} from '../constants';
77
import {QueryEditor} from '../queryEditor/models/QueryEditor';
88

9-
import {Diagnostics, DiagnosticsTab, QueriesSwitch} from './Diagnostics';
9+
import {
10+
Diagnostics,
11+
DiagnosticsTab,
12+
QueriesSwitch,
13+
TopShardsHistoricalColumns,
14+
TopShardsImmediateColumns,
15+
TopShardsMode,
16+
} from './Diagnostics';
17+
import {setupTopShardsHistoryMock} from './mocks';
1018

1119
test.describe('Diagnostics tab', async () => {
1220
test('Info tab shows main page elements', async ({page}) => {
@@ -170,4 +178,135 @@ test.describe('Diagnostics tab', async () => {
170178
await diagnostics.table.waitForCellValueByHeader(1, 'Query text', longRunningQuery),
171179
).toBe(true);
172180
});
181+
182+
test('TopShards tab defaults to Immediate mode', async ({page}) => {
183+
const pageQueryParams = {
184+
schema: tenantName,
185+
database: tenantName,
186+
tenantPage: 'diagnostics',
187+
diagnosticsTab: 'topShards',
188+
};
189+
const tenantPage = new TenantPage(page);
190+
await tenantPage.goto(pageQueryParams);
191+
192+
const diagnostics = new Diagnostics(page);
193+
194+
// Verify Immediate mode is selected by default
195+
expect(await diagnostics.getSelectedTopShardsMode()).toBe(TopShardsMode.Immediate);
196+
});
197+
198+
test('TopShards immediate tab shows all expected column headers', async ({page}) => {
199+
const pageQueryParams = {
200+
schema: tenantName,
201+
database: tenantName,
202+
tenantPage: 'diagnostics',
203+
diagnosticsTab: 'topShards',
204+
};
205+
const tenantPage = new TenantPage(page);
206+
await tenantPage.goto(pageQueryParams);
207+
208+
const diagnostics = new Diagnostics(page);
209+
210+
// Verify table has data
211+
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
212+
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
213+
// Verify column headers exist
214+
await diagnostics.table.verifyHeaders(TopShardsImmediateColumns);
215+
});
216+
217+
test('TopShards history tab shows all expected column headers', async ({page}) => {
218+
const pageQueryParams = {
219+
schema: tenantName,
220+
database: tenantName,
221+
tenantPage: 'diagnostics',
222+
diagnosticsTab: 'topShards',
223+
};
224+
const tenantPage = new TenantPage(page);
225+
await tenantPage.goto(pageQueryParams);
226+
227+
const diagnostics = new Diagnostics(page);
228+
await diagnostics.selectTopShardsMode(TopShardsMode.Historical);
229+
// Verify table has data
230+
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
231+
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
232+
233+
// Verify column headers exist
234+
await diagnostics.table.verifyHeaders(TopShardsHistoricalColumns);
235+
});
236+
237+
test('TopShards tab first row has values for all columns in Immediate mode', async ({page}) => {
238+
const pageQueryParams = {
239+
schema: tenantName,
240+
database: tenantName,
241+
tenantPage: 'diagnostics',
242+
diagnosticsTab: 'topShards',
243+
};
244+
const tenantPage = new TenantPage(page);
245+
await tenantPage.goto(pageQueryParams);
246+
247+
const diagnostics = new Diagnostics(page);
248+
249+
// Verify table has data
250+
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
251+
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
252+
253+
// Verify first row has non-empty values for all columns
254+
for (const column of TopShardsImmediateColumns) {
255+
const columnValue = await diagnostics.table.getCellValueByHeader(1, column);
256+
expect(columnValue.trim()).toBeTruthy();
257+
}
258+
});
259+
260+
test('TopShards tab first row has values for all columns in History mode', async ({page}) => {
261+
// Setup mock for TopShards tab in History mode
262+
await setupTopShardsHistoryMock(page);
263+
264+
// Now navigate to diagnostics page to check topShards
265+
const pageQueryParams = {
266+
schema: tenantName,
267+
database: tenantName,
268+
tenantPage: 'diagnostics',
269+
diagnosticsTab: 'topShards',
270+
};
271+
const tenantPage = new TenantPage(page);
272+
await tenantPage.goto(pageQueryParams);
273+
274+
const diagnostics = new Diagnostics(page);
275+
await diagnostics.selectTopShardsMode(TopShardsMode.Historical);
276+
277+
// Verify table has data
278+
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
279+
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
280+
281+
// Verify first row has non-empty values for all columns
282+
for (const column of TopShardsHistoricalColumns) {
283+
const columnValue = await diagnostics.table.getCellValueByHeader(1, column);
284+
expect(columnValue.trim()).toBeTruthy();
285+
}
286+
});
287+
288+
test('TopShards tab can switch back to Immediate mode from Historical mode', async ({page}) => {
289+
const pageQueryParams = {
290+
schema: tenantName,
291+
database: tenantName,
292+
tenantPage: 'diagnostics',
293+
diagnosticsTab: 'topShards',
294+
};
295+
const tenantPage = new TenantPage(page);
296+
await tenantPage.goto(pageQueryParams);
297+
298+
const diagnostics = new Diagnostics(page);
299+
300+
// Switch to Historical mode
301+
await diagnostics.selectTopShardsMode(TopShardsMode.Historical);
302+
expect(await diagnostics.getSelectedTopShardsMode()).toBe(TopShardsMode.Historical);
303+
304+
// Switch back to Immediate mode
305+
await diagnostics.selectTopShardsMode(TopShardsMode.Immediate);
306+
expect(await diagnostics.getSelectedTopShardsMode()).toBe(TopShardsMode.Immediate);
307+
308+
// Verify table still has data after switching back
309+
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
310+
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
311+
});
173312
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type {Page} from '@playwright/test';
2+
3+
import {backend} from '../../../utils/constants';
4+
5+
const MOCK_DELAY = 200; // 200ms delay to simulate network latency
6+
7+
/**
8+
* Generates a mock row for the TopShards table in History mode
9+
* @param index Row index (0-based)
10+
* @returns An array of values for each column
11+
*/
12+
const generateTopShardsHistoryRow = (index: number) => {
13+
const now = new Date();
14+
const endTime = new Date(now);
15+
endTime.setHours(endTime.getHours() + 1);
16+
endTime.setMinutes(0);
17+
endTime.setSeconds(0);
18+
endTime.setMilliseconds(0);
19+
20+
// Calculate a peak time somewhere in the past hour
21+
const peakTime = new Date(now);
22+
peakTime.setMinutes(peakTime.getMinutes() - (index * 5 + Math.floor(Math.random() * 15)));
23+
24+
// Generate a CPU usage between 0.85 and 0.98
25+
const cpuCores = 0.85 + Math.random() * 0.13;
26+
27+
// Generate a data size between 1GB and 2GB (in bytes)
28+
const dataSize = Math.floor(1000000000 + Math.random() * 1000000000).toString();
29+
30+
// Generate row count (30-60 million rows)
31+
const rowCount = Math.floor(30000000 + Math.random() * 30000000).toString();
32+
33+
// Generate index size (approximately 8-15% of data size)
34+
const indexSize = Math.floor(parseInt(dataSize) * (0.08 + Math.random() * 0.07)).toString();
35+
36+
// NodeId between 50000 and 50020
37+
const nodeId = 50000 + Math.floor(Math.random() * 20);
38+
39+
// In-flight transactions (usually low, 0-3)
40+
const inFlightTxCount = Math.floor(Math.random() * 4);
41+
42+
// Generate unique tablet ID
43+
const tabletId = `7207518622${100000 + index}`;
44+
45+
// Generate paths
46+
const dbIndex = Math.floor(index / 3) + 1;
47+
const relativePath = `/db${dbIndex}/shard_${index + 1}`;
48+
const fullPath = `/dev01/home/ydbuser${relativePath}`;
49+
50+
return [
51+
cpuCores, // CPUCores
52+
dataSize, // DataSize
53+
0, // FollowerId
54+
inFlightTxCount, // InFlightTxCount
55+
indexSize, // IndexSize
56+
endTime.toISOString(), // IntervalEnd
57+
nodeId, // NodeId
58+
fullPath, // Path
59+
peakTime.toISOString(), // PeakTime
60+
index + 1, // Rank
61+
relativePath, // RelativePath
62+
rowCount, // RowCount
63+
tabletId, // TabletId
64+
];
65+
};
66+
67+
/**
68+
* Sets up a mock for the TopShards tab in History mode
69+
* This ensures the first row has values for all columns
70+
*/
71+
export const setupTopShardsHistoryMock = async (page: Page) => {
72+
await page.route(`${backend}/viewer/json/query?*`, async (route) => {
73+
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
74+
75+
// Generate 10 rows of data
76+
const rows = Array.from({length: 10}, (_, i) => generateTopShardsHistoryRow(i));
77+
78+
await route.fulfill({
79+
status: 200,
80+
contentType: 'application/json',
81+
body: JSON.stringify({
82+
version: 8,
83+
result: [
84+
{
85+
rows: rows,
86+
columns: [
87+
{name: 'CPUCores', type: 'Double?'},
88+
{name: 'DataSize', type: 'Uint64?'},
89+
{name: 'FollowerId', type: 'Uint32?'},
90+
{name: 'InFlightTxCount', type: 'Uint32?'},
91+
{name: 'IndexSize', type: 'Uint64?'},
92+
{name: 'IntervalEnd', type: 'Timestamp?'},
93+
{name: 'NodeId', type: 'Uint32?'},
94+
{name: 'Path', type: 'Utf8?'},
95+
{name: 'PeakTime', type: 'Timestamp?'},
96+
{name: 'Rank', type: 'Uint32?'},
97+
{name: 'RelativePath', type: 'Utf8?'},
98+
{name: 'RowCount', type: 'Uint64?'},
99+
{name: 'TabletId', type: 'Uint64?'},
100+
],
101+
},
102+
],
103+
}),
104+
});
105+
});
106+
};

0 commit comments

Comments
 (0)