Skip to content

Commit 52d7dd6

Browse files
committed
fix: add operations tests
1 parent 827d8be commit 52d7dd6

File tree

6 files changed

+494
-4
lines changed

6 files changed

+494
-4
lines changed

src/containers/Operations/columns.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ interface OperationsActionsProps {
187187
}
188188

189189
function OperationsActions({operation, database, refreshTable}: OperationsActionsProps) {
190-
const [cancelOperation, {isLoading: isLoadingCancel}] =
190+
const [cancelOperation, {isLoading: isCancelLoading}] =
191191
operationsApi.useCancelOperationMutation();
192192
const [forgetOperation, {isLoading: isForgetLoading}] =
193193
operationsApi.useForgetOperationMutation();
@@ -197,8 +197,8 @@ function OperationsActions({operation, database, refreshTable}: OperationsAction
197197
return null;
198198
}
199199

200-
const isForgetButtonDisabled = isLoadingCancel;
201-
const isCancelButtonDisabled = isForgetLoading || operation.ready === true;
200+
const isForgetButtonDisabled = isForgetLoading;
201+
const isCancelButtonDisabled = isCancelLoading || operation.ready === true;
202202

203203
return (
204204
<Flex gap="2">

src/containers/Operations/useOperationsInfiniteQuery.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ export function useOperationsInfiniteQuery({
9898
});
9999

100100
window.addEventListener('resize', throttledHandleResize);
101-
return () => window.removeEventListener('resize', throttledHandleResize);
101+
return () => {
102+
throttledHandleResize.cancel();
103+
window.removeEventListener('resize', throttledHandleResize);
104+
};
102105
}, [checkAndLoadMorePages]);
103106

104107
// Listen for diagnostics refresh events

tests/suites/tenant/diagnostics/Diagnostics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {NodesPage} from '../../nodes/NodesPage';
66
import {StoragePage} from '../../storage/StoragePage';
77
import {VISIBILITY_TIMEOUT} from '../TenantPage';
88

9+
import {OperationsTable} from './tabs/OperationsModel';
10+
911
export enum DiagnosticsTab {
1012
Info = 'Info',
1113
Schema = 'Schema',
@@ -17,6 +19,7 @@ export enum DiagnosticsTab {
1719
HotKeys = 'Hot keys',
1820
Describe = 'Describe',
1921
Storage = 'Storage',
22+
Operations = 'Operations',
2023
Access = 'Access',
2124
}
2225

@@ -231,6 +234,7 @@ export class Diagnostics {
231234
storage: StoragePage;
232235
nodes: NodesPage;
233236
memoryViewer: MemoryViewer;
237+
operations: OperationsTable;
234238
private page: Page;
235239

236240
private tabs: Locator;
@@ -256,6 +260,7 @@ export class Diagnostics {
256260
this.storage = new StoragePage(page);
257261
this.nodes = new NodesPage(page);
258262
this.memoryViewer = new MemoryViewer(page);
263+
this.operations = new OperationsTable(page);
259264
this.tabs = page.locator('.kv-tenant-diagnostics__tabs');
260265
this.tableControls = page.locator('.ydb-table-with-controls-layout__controls');
261266
this.schemaViewer = page.locator('.schema-viewer');
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type {Locator, Page} from '@playwright/test';
2+
3+
import {BaseModel} from '../../../../models/BaseModel';
4+
import {VISIBILITY_TIMEOUT} from '../../TenantPage';
5+
6+
export enum OperationTab {
7+
Operations = 'Operations',
8+
}
9+
10+
export class OperationsTable extends BaseModel {
11+
private tableContainer: Locator;
12+
private tableRows: Locator;
13+
private emptyState: Locator;
14+
private loadingMore: Locator;
15+
private scrollContainer: Locator;
16+
17+
constructor(page: Page) {
18+
super(page, page.locator('.kv-tenant-diagnostics'));
19+
20+
this.tableContainer = page.locator('.ydb-table-with-controls-layout');
21+
this.tableRows = page.locator('.data-table__row:not(.data-table__row_header)');
22+
this.emptyState = page.locator('.operations__table:has-text("No operations data")');
23+
this.loadingMore = page.locator('.operations__loading-more');
24+
this.scrollContainer = page.locator('.kv-tenant-diagnostics__page-wrapper');
25+
}
26+
27+
async waitForTableVisible() {
28+
await this.tableContainer.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
29+
}
30+
31+
async waitForDataLoad() {
32+
// Wait for either data rows or empty state
33+
await this.page.waitForFunction(
34+
() => {
35+
const rows = document.querySelectorAll(
36+
'.data-table__row:not(.data-table__row_header)',
37+
);
38+
const tableContainer = document.querySelector('.operations__table');
39+
const hasEmptyText = tableContainer?.textContent?.includes('No operations data');
40+
return rows.length > 0 || hasEmptyText === true;
41+
},
42+
{timeout: VISIBILITY_TIMEOUT},
43+
);
44+
// Additional wait for stability
45+
await this.page.waitForTimeout(500);
46+
}
47+
48+
async getRowCount(): Promise<number> {
49+
return await this.tableRows.count();
50+
}
51+
52+
async isEmptyStateVisible(): Promise<boolean> {
53+
return await this.emptyState.isVisible();
54+
}
55+
56+
async scrollToBottom() {
57+
await this.scrollContainer.evaluate((element) => {
58+
element.scrollTo({top: element.scrollHeight, behavior: 'instant'});
59+
});
60+
}
61+
62+
async isLoadingMoreVisible(): Promise<boolean> {
63+
return await this.loadingMore.isVisible();
64+
}
65+
66+
async waitForLoadingMoreToDisappear() {
67+
await this.loadingMore.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT});
68+
}
69+
70+
async getRowData(rowIndex: number): Promise<Record<string, string>> {
71+
const row = this.tableRows.nth(rowIndex);
72+
const cells = row.locator('td');
73+
const allHeaders = await this.page.locator('.data-table__th').allTextContents();
74+
75+
// Take first occurrence of headers (they might be duplicated due to virtual scrolling)
76+
const uniqueHeaders: string[] = [];
77+
const seen = new Set<string>();
78+
let emptyCount = 0;
79+
80+
for (const header of allHeaders) {
81+
const trimmed = header.trim();
82+
if (trimmed === '') {
83+
// Handle multiple empty headers (e.g., for action columns)
84+
uniqueHeaders.push(`_empty_${emptyCount++}`);
85+
} else if (!seen.has(trimmed)) {
86+
seen.add(trimmed);
87+
uniqueHeaders.push(trimmed);
88+
}
89+
// Stop when we have enough headers for the cells
90+
if (uniqueHeaders.length >= (await cells.count())) {
91+
break;
92+
}
93+
}
94+
95+
const rowData: Record<string, string> = {};
96+
const cellCount = await cells.count();
97+
98+
for (let i = 0; i < cellCount && i < uniqueHeaders.length; i++) {
99+
const headerText = uniqueHeaders[i];
100+
const cellText = await cells.nth(i).textContent();
101+
// Don't include empty headers in the result
102+
if (!headerText.startsWith('_empty_')) {
103+
rowData[headerText] = cellText?.trim() || '';
104+
}
105+
}
106+
107+
return rowData;
108+
}
109+
110+
async hasActiveInfiniteScroll(): Promise<boolean> {
111+
// Check if scrolling triggers loading more
112+
const initialCount = await this.getRowCount();
113+
await this.scrollToBottom();
114+
115+
// Wait a bit to see if loading more appears
116+
await this.page.waitForTimeout(1000);
117+
118+
const hasLoadingMore = await this.isLoadingMoreVisible();
119+
if (hasLoadingMore) {
120+
await this.waitForLoadingMoreToDisappear();
121+
const newCount = await this.getRowCount();
122+
return newCount > initialCount;
123+
}
124+
125+
return false;
126+
}
127+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {expect, test} from '@playwright/test';
2+
3+
import {tenantName} from '../../../../utils/constants';
4+
import {TenantPage} from '../../TenantPage';
5+
import {Diagnostics, DiagnosticsTab} from '../Diagnostics';
6+
7+
import {setupEmptyOperationsMock, setupOperationsMock} from './operationsMocks';
8+
9+
test.describe('Operations Tab - Infinite Query', () => {
10+
test('loads initial page of operations on tab click', async ({page}) => {
11+
// Setup mocks with 30 operations (3 pages of 10)
12+
await setupOperationsMock(page, {totalOperations: 30});
13+
14+
const pageQueryParams = {
15+
schema: tenantName,
16+
database: tenantName,
17+
tenantPage: 'diagnostics',
18+
};
19+
20+
const tenantPageInstance = new TenantPage(page);
21+
await tenantPageInstance.goto(pageQueryParams);
22+
23+
const diagnostics = new Diagnostics(page);
24+
await diagnostics.clickTab(DiagnosticsTab.Operations);
25+
26+
// Wait for table to be visible and data to load
27+
await diagnostics.operations.waitForTableVisible();
28+
await diagnostics.operations.waitForDataLoad();
29+
30+
// Verify initial page loaded (should have some rows)
31+
const rowCount = await diagnostics.operations.getRowCount();
32+
expect(rowCount).toBeGreaterThan(0);
33+
expect(rowCount).toBeLessThanOrEqual(20); // Reasonable page size
34+
35+
// Verify first row data structure
36+
const firstRowData = await diagnostics.operations.getRowData(0);
37+
expect(firstRowData['Operation ID']).toBeTruthy();
38+
expect(firstRowData['Operation ID']).toContain('ydb://');
39+
expect(firstRowData['Status']).toBeTruthy();
40+
expect(['SUCCESS', 'GENERIC_ERROR', 'CANCELLED', 'ABORTED']).toContain(
41+
firstRowData['Status'],
42+
);
43+
expect(firstRowData['State']).toBeTruthy();
44+
expect(firstRowData['Progress']).toBeTruthy();
45+
46+
// Verify loading more indicator is not visible initially
47+
const isLoadingVisible = await diagnostics.operations.isLoadingMoreVisible();
48+
expect(isLoadingVisible).toBe(false);
49+
});
50+
51+
test('loads more operations on scroll', async ({page}) => {
52+
// Setup mocks with 30 operations (3 pages of 10)
53+
await setupOperationsMock(page, {totalOperations: 30});
54+
55+
const pageQueryParams = {
56+
schema: tenantName,
57+
database: tenantName,
58+
tenantPage: 'diagnostics',
59+
};
60+
61+
const tenantPageInstance = new TenantPage(page);
62+
await tenantPageInstance.goto(pageQueryParams);
63+
64+
const diagnostics = new Diagnostics(page);
65+
await diagnostics.clickTab(DiagnosticsTab.Operations);
66+
67+
// Wait for initial data
68+
await diagnostics.operations.waitForTableVisible();
69+
await diagnostics.operations.waitForDataLoad();
70+
71+
// Get initial row count
72+
const initialRowCount = await diagnostics.operations.getRowCount();
73+
expect(initialRowCount).toBeGreaterThan(0);
74+
75+
// Scroll to bottom
76+
await diagnostics.operations.scrollToBottom();
77+
78+
// Wait a bit for potential loading
79+
await page.waitForTimeout(2000);
80+
81+
// Get final row count
82+
const finalRowCount = await diagnostics.operations.getRowCount();
83+
84+
// Check if more rows were loaded
85+
if (finalRowCount > initialRowCount) {
86+
// Infinite scroll worked - more rows were loaded
87+
expect(finalRowCount).toBeGreaterThan(initialRowCount);
88+
} else {
89+
// No more data to load - row count should stay the same
90+
expect(finalRowCount).toBe(initialRowCount);
91+
}
92+
});
93+
94+
test('shows empty state when no operations', async ({page}) => {
95+
// Setup empty operations mock
96+
await setupEmptyOperationsMock(page);
97+
98+
const pageQueryParams = {
99+
schema: tenantName,
100+
database: tenantName,
101+
tenantPage: 'diagnostics',
102+
};
103+
104+
const tenantPageInstance = new TenantPage(page);
105+
await tenantPageInstance.goto(pageQueryParams);
106+
107+
const diagnostics = new Diagnostics(page);
108+
await diagnostics.clickTab(DiagnosticsTab.Operations);
109+
110+
// Wait for table to be visible
111+
await diagnostics.operations.waitForTableVisible();
112+
await diagnostics.operations.waitForDataLoad();
113+
114+
// Verify empty state is shown
115+
const isEmptyVisible = await diagnostics.operations.isEmptyStateVisible();
116+
expect(isEmptyVisible).toBe(true);
117+
118+
// Verify no data rows (or possibly one empty row)
119+
const rowCount = await diagnostics.operations.getRowCount();
120+
expect(rowCount).toBeLessThanOrEqual(1);
121+
});
122+
});

0 commit comments

Comments
 (0)