Skip to content

Commit 38c88e5

Browse files
authored
chore: e2e tests part 2 (#1799)
1 parent 9d2280e commit 38c88e5

File tree

5 files changed

+253
-2
lines changed

5 files changed

+253
-2
lines changed

src/containers/Tenant/ObjectSummary/SchemaTree/RefreshTreeButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit';
33
import {nanoid} from '@reduxjs/toolkit';
44

55
import {useDispatchTreeKey} from '../UpdateTreeContext';
6+
import {b} from '../shared';
67

78
export function RefreshTreeButton() {
89
const updateTreeKey = useDispatchTreeKey();
910
return (
1011
<ActionTooltip title="Refresh">
1112
<Button
13+
className={b('refresh-button')}
1214
view="flat-secondary"
1315
onClick={() => {
1416
updateTreeKey(nanoid());

tests/suites/tenant/summary/ObjectSummary.ts

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ export enum ObjectSummaryTab {
1010
ACL = 'ACL',
1111
Schema = 'Schema',
1212
}
13-
1413
export class ObjectSummary {
1514
private tabs: Locator;
1615
private schemaViewer: Locator;
1716
private tree: Locator;
1817
private treeRows: Locator;
1918
private primaryKeys: Locator;
2019
private actionsMenu: ActionsMenu;
20+
private aclWrapper: Locator;
21+
private aclList: Locator;
22+
private effectiveAclList: Locator;
23+
private createDirectoryModal: Locator;
24+
private createDirectoryInput: Locator;
25+
private createDirectoryButton: Locator;
26+
private refreshButton: Locator;
2127

2228
constructor(page: Page) {
2329
this.tree = page.locator('.ydb-object-summary__tree');
@@ -26,6 +32,82 @@ export class ObjectSummary {
2632
this.schemaViewer = page.locator('.schema-viewer');
2733
this.primaryKeys = page.locator('.schema-viewer__keys_type_primary');
2834
this.actionsMenu = new ActionsMenu(page.locator('.g-popup.g-popup_open'));
35+
this.aclWrapper = page.locator('.ydb-acl');
36+
this.aclList = this.aclWrapper.locator('dl.gc-definition-list').first();
37+
this.effectiveAclList = this.aclWrapper.locator('dl.gc-definition-list').last();
38+
this.createDirectoryModal = page.locator('.g-modal.g-modal_open');
39+
this.createDirectoryInput = page.locator(
40+
'.g-text-input__control[placeholder="Relative path"]',
41+
);
42+
this.createDirectoryButton = page.locator('button.g-button_view_action:has-text("Create")');
43+
this.refreshButton = page.locator('.ydb-object-summary__refresh-button');
44+
}
45+
46+
async isCreateDirectoryModalVisible(): Promise<boolean> {
47+
try {
48+
await this.createDirectoryModal.waitFor({
49+
state: 'visible',
50+
timeout: VISIBILITY_TIMEOUT,
51+
});
52+
return true;
53+
} catch (error) {
54+
return false;
55+
}
56+
}
57+
58+
async enterDirectoryName(name: string): Promise<void> {
59+
await this.createDirectoryInput.fill(name);
60+
}
61+
62+
async clickCreateDirectoryButton(): Promise<void> {
63+
await this.createDirectoryButton.click();
64+
}
65+
66+
async createDirectory(name: string): Promise<void> {
67+
await this.enterDirectoryName(name);
68+
await this.clickCreateDirectoryButton();
69+
// Wait for modal to close
70+
await this.createDirectoryModal.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT});
71+
}
72+
73+
async waitForAclVisible() {
74+
await this.aclWrapper.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
75+
return true;
76+
}
77+
78+
async getAccessRights(): Promise<{user: string; rights: string}[]> {
79+
await this.waitForAclVisible();
80+
const items = await this.aclList.locator('.gc-definition-list__item').all();
81+
const result = [];
82+
83+
for (const item of items) {
84+
const user =
85+
(await item.locator('.gc-definition-list__term-wrapper span').textContent()) || '';
86+
const definitionContent = await item.locator('.gc-definition-list__definition').first();
87+
const rights = (await definitionContent.textContent()) || '';
88+
result.push({user: user.trim(), rights: rights.trim()});
89+
}
90+
91+
return result;
92+
}
93+
94+
async getEffectiveAccessRights(): Promise<{group: string; permissions: string[]}[]> {
95+
await this.waitForAclVisible();
96+
const items = await this.effectiveAclList.locator('.gc-definition-list__item').all();
97+
const result = [];
98+
99+
for (const item of items) {
100+
const group =
101+
(await item.locator('.gc-definition-list__term-wrapper span').textContent()) || '';
102+
const definitionContent = await item.locator('.gc-definition-list__definition').first();
103+
const permissionElements = await definitionContent.locator('span').all();
104+
const permissions = await Promise.all(
105+
permissionElements.map(async (el) => ((await el.textContent()) || '').trim()),
106+
);
107+
result.push({group: group.trim(), permissions});
108+
}
109+
110+
return result;
29111
}
30112

31113
async isTreeVisible() {
@@ -111,9 +193,12 @@ export class ObjectSummary {
111193
async getTableTemplates(): Promise<RowTableAction[]> {
112194
return this.actionsMenu.getTableTemplates();
113195
}
114-
115196
async clickActionMenuItem(treeItemText: string, menuItemText: string): Promise<void> {
116197
await this.clickActionsButton(treeItemText);
117198
await this.clickActionsMenuItem(menuItemText);
118199
}
200+
201+
async clickRefreshButton(): Promise<void> {
202+
await this.refreshButton.click();
203+
}
119204
}

tests/suites/tenant/summary/objectSummary.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {expect, test} from '@playwright/test';
22

33
import {wait} from '../../../../src/utils';
4+
import {getClipboardContent} from '../../../utils/clipboard';
45
import {
56
backend,
67
dsStoragePoolsTableName,
@@ -160,4 +161,128 @@ test.describe('Object Summary', async () => {
160161
// Verify the column lists are different
161162
expect(vslotsColumns).not.toEqual(storagePoolsColumns);
162163
});
164+
165+
test('ACL tab shows correct access rights', async ({page}) => {
166+
const pageQueryParams = {
167+
schema: '/local/.sys_health',
168+
database: '/local',
169+
summaryTab: 'acl',
170+
tenantPage: 'query',
171+
};
172+
const tenantPage = new TenantPage(page);
173+
await tenantPage.goto(pageQueryParams);
174+
175+
const objectSummary = new ObjectSummary(page);
176+
await objectSummary.waitForAclVisible();
177+
178+
// Check Access Rights
179+
const accessRights = await objectSummary.getAccessRights();
180+
expect(accessRights).toEqual([{user: 'root@builtin', rights: 'Owner'}]);
181+
182+
// Check Effective Access Rights
183+
const effectiveRights = await objectSummary.getEffectiveAccessRights();
184+
expect(effectiveRights).toEqual([
185+
{group: 'USERS', permissions: ['ConnectDatabase']},
186+
{group: 'METADATA-READERS', permissions: ['List']},
187+
{group: 'DATA-READERS', permissions: ['SelectRow']},
188+
{group: 'DATA-WRITERS', permissions: ['UpdateRow', 'EraseRow']},
189+
{
190+
group: 'DDL-ADMINS',
191+
permissions: [
192+
'WriteAttributes',
193+
'CreateDirectory',
194+
'CreateTable',
195+
'RemoveSchema',
196+
'AlterSchema',
197+
],
198+
},
199+
{group: 'ACCESS-ADMINS', permissions: ['GrantAccessRights']},
200+
{group: 'DATABASE-ADMINS', permissions: ['Manage']},
201+
]);
202+
});
203+
204+
test('Copy path copies correct path to clipboard', async ({page}) => {
205+
const pageQueryParams = {
206+
schema: dsVslotsSchema,
207+
database: tenantName,
208+
general: 'query',
209+
};
210+
const tenantPage = new TenantPage(page);
211+
await tenantPage.goto(pageQueryParams);
212+
213+
const objectSummary = new ObjectSummary(page);
214+
await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.CopyPath);
215+
216+
// Wait for clipboard operation to complete
217+
await page.waitForTimeout(2000);
218+
219+
// Retry clipboard read a few times if needed
220+
let clipboardContent = '';
221+
for (let i = 0; i < 3; i++) {
222+
clipboardContent = await getClipboardContent(page);
223+
if (clipboardContent) {
224+
break;
225+
}
226+
await page.waitForTimeout(500);
227+
}
228+
expect(clipboardContent).toBe('.sys/ds_vslots');
229+
});
230+
231+
test('Create directory in local node', async ({page}) => {
232+
const pageQueryParams = {
233+
schema: tenantName,
234+
database: tenantName,
235+
general: 'query',
236+
};
237+
const tenantPage = new TenantPage(page);
238+
await tenantPage.goto(pageQueryParams);
239+
240+
const objectSummary = new ObjectSummary(page);
241+
await expect(objectSummary.isTreeVisible()).resolves.toBe(true);
242+
243+
const directoryName = `test_dir_${Date.now()}`;
244+
245+
// Open actions menu and click Create directory
246+
await objectSummary.clickActionMenuItem('local', RowTableAction.CreateDirectory);
247+
await expect(objectSummary.isCreateDirectoryModalVisible()).resolves.toBe(true);
248+
249+
// Create directory
250+
await objectSummary.createDirectory(directoryName);
251+
252+
// Verify the new directory appears in the tree
253+
const treeItem = page.locator('.ydb-tree-view').filter({hasText: directoryName});
254+
await expect(treeItem).toBeVisible();
255+
});
256+
257+
test('Refresh button updates tree view after creating table', async ({page}) => {
258+
const pageQueryParams = {
259+
schema: tenantName,
260+
database: tenantName,
261+
general: 'query',
262+
};
263+
const tenantPage = new TenantPage(page);
264+
await tenantPage.goto(pageQueryParams);
265+
266+
const objectSummary = new ObjectSummary(page);
267+
const queryEditor = new QueryEditor(page);
268+
await expect(objectSummary.isTreeVisible()).resolves.toBe(true);
269+
270+
const tableName = `a_test_table_${Date.now()}`;
271+
272+
// Create table by executing query
273+
await queryEditor.setQuery(`CREATE TABLE \`${tableName}\` (id Int32, PRIMARY KEY(id));`);
274+
await queryEditor.clickRunButton();
275+
await queryEditor.waitForStatus('Completed');
276+
277+
// Verify table is not visible before refresh
278+
const treeItemBeforeRefresh = page.locator('.ydb-tree-view').filter({hasText: tableName});
279+
await expect(treeItemBeforeRefresh).not.toBeVisible();
280+
281+
// Click refresh button to update tree view
282+
await objectSummary.clickRefreshButton();
283+
284+
// Verify table appears in tree
285+
const treeItemAfterRefresh = page.locator('.ydb-tree-view').filter({hasText: tableName});
286+
await expect(treeItemAfterRefresh).toBeVisible();
287+
});
163288
});

tests/suites/tenant/summary/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export enum RowTableAction {
66
UpsertQuery = 'Upsert query...',
77
AddIndex = 'Add index...',
88
CreateChangefeed = 'Create changefeed...',
9+
CreateDirectory = 'Create directory',
910
}

tests/utils/clipboard.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type {Page} from '@playwright/test';
2+
3+
export const getClipboardContent = async (page: Page): Promise<string> => {
4+
await page.context().grantPermissions(['clipboard-read']);
5+
6+
// First try the modern Clipboard API
7+
const clipboardText = await page.evaluate(async () => {
8+
try {
9+
const text = await navigator.clipboard.readText();
10+
return text;
11+
} catch (error) {
12+
return null;
13+
}
14+
});
15+
16+
if (clipboardText !== null) {
17+
return clipboardText;
18+
}
19+
20+
// Fallback: Create a contenteditable element, focus it, and send keyboard shortcuts
21+
return page.evaluate(async () => {
22+
const el = document.createElement('div');
23+
el.contentEditable = 'true';
24+
document.body.appendChild(el);
25+
el.focus();
26+
27+
try {
28+
// Send paste command
29+
document.execCommand('paste');
30+
const text = el.textContent || '';
31+
document.body.removeChild(el);
32+
return text;
33+
} catch (error) {
34+
document.body.removeChild(el);
35+
return '';
36+
}
37+
});
38+
};

0 commit comments

Comments
 (0)