Skip to content

Commit

Permalink
chore: e2e tests part 2 (#1799)
Browse files Browse the repository at this point in the history
  • Loading branch information
astandrik authored Dec 28, 2024
1 parent 9d2280e commit 38c88e5
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit';
import {nanoid} from '@reduxjs/toolkit';

import {useDispatchTreeKey} from '../UpdateTreeContext';
import {b} from '../shared';

export function RefreshTreeButton() {
const updateTreeKey = useDispatchTreeKey();
return (
<ActionTooltip title="Refresh">
<Button
className={b('refresh-button')}
view="flat-secondary"
onClick={() => {
updateTreeKey(nanoid());
Expand Down
89 changes: 87 additions & 2 deletions tests/suites/tenant/summary/ObjectSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ export enum ObjectSummaryTab {
ACL = 'ACL',
Schema = 'Schema',
}

export class ObjectSummary {
private tabs: Locator;
private schemaViewer: Locator;
private tree: Locator;
private treeRows: Locator;
private primaryKeys: Locator;
private actionsMenu: ActionsMenu;
private aclWrapper: Locator;
private aclList: Locator;
private effectiveAclList: Locator;
private createDirectoryModal: Locator;
private createDirectoryInput: Locator;
private createDirectoryButton: Locator;
private refreshButton: Locator;

constructor(page: Page) {
this.tree = page.locator('.ydb-object-summary__tree');
Expand All @@ -26,6 +32,82 @@ export class ObjectSummary {
this.schemaViewer = page.locator('.schema-viewer');
this.primaryKeys = page.locator('.schema-viewer__keys_type_primary');
this.actionsMenu = new ActionsMenu(page.locator('.g-popup.g-popup_open'));
this.aclWrapper = page.locator('.ydb-acl');
this.aclList = this.aclWrapper.locator('dl.gc-definition-list').first();
this.effectiveAclList = this.aclWrapper.locator('dl.gc-definition-list').last();
this.createDirectoryModal = page.locator('.g-modal.g-modal_open');
this.createDirectoryInput = page.locator(
'.g-text-input__control[placeholder="Relative path"]',
);
this.createDirectoryButton = page.locator('button.g-button_view_action:has-text("Create")');
this.refreshButton = page.locator('.ydb-object-summary__refresh-button');
}

async isCreateDirectoryModalVisible(): Promise<boolean> {
try {
await this.createDirectoryModal.waitFor({
state: 'visible',
timeout: VISIBILITY_TIMEOUT,
});
return true;
} catch (error) {
return false;
}
}

async enterDirectoryName(name: string): Promise<void> {
await this.createDirectoryInput.fill(name);
}

async clickCreateDirectoryButton(): Promise<void> {
await this.createDirectoryButton.click();
}

async createDirectory(name: string): Promise<void> {
await this.enterDirectoryName(name);
await this.clickCreateDirectoryButton();
// Wait for modal to close
await this.createDirectoryModal.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT});
}

async waitForAclVisible() {
await this.aclWrapper.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return true;
}

async getAccessRights(): Promise<{user: string; rights: string}[]> {
await this.waitForAclVisible();
const items = await this.aclList.locator('.gc-definition-list__item').all();
const result = [];

for (const item of items) {
const user =
(await item.locator('.gc-definition-list__term-wrapper span').textContent()) || '';
const definitionContent = await item.locator('.gc-definition-list__definition').first();
const rights = (await definitionContent.textContent()) || '';
result.push({user: user.trim(), rights: rights.trim()});
}

return result;
}

async getEffectiveAccessRights(): Promise<{group: string; permissions: string[]}[]> {
await this.waitForAclVisible();
const items = await this.effectiveAclList.locator('.gc-definition-list__item').all();
const result = [];

for (const item of items) {
const group =
(await item.locator('.gc-definition-list__term-wrapper span').textContent()) || '';
const definitionContent = await item.locator('.gc-definition-list__definition').first();
const permissionElements = await definitionContent.locator('span').all();
const permissions = await Promise.all(
permissionElements.map(async (el) => ((await el.textContent()) || '').trim()),
);
result.push({group: group.trim(), permissions});
}

return result;
}

async isTreeVisible() {
Expand Down Expand Up @@ -111,9 +193,12 @@ export class ObjectSummary {
async getTableTemplates(): Promise<RowTableAction[]> {
return this.actionsMenu.getTableTemplates();
}

async clickActionMenuItem(treeItemText: string, menuItemText: string): Promise<void> {
await this.clickActionsButton(treeItemText);
await this.clickActionsMenuItem(menuItemText);
}

async clickRefreshButton(): Promise<void> {
await this.refreshButton.click();
}
}
125 changes: 125 additions & 0 deletions tests/suites/tenant/summary/objectSummary.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {expect, test} from '@playwright/test';

import {wait} from '../../../../src/utils';
import {getClipboardContent} from '../../../utils/clipboard';
import {
backend,
dsStoragePoolsTableName,
Expand Down Expand Up @@ -160,4 +161,128 @@ test.describe('Object Summary', async () => {
// Verify the column lists are different
expect(vslotsColumns).not.toEqual(storagePoolsColumns);
});

test('ACL tab shows correct access rights', async ({page}) => {
const pageQueryParams = {
schema: '/local/.sys_health',
database: '/local',
summaryTab: 'acl',
tenantPage: 'query',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const objectSummary = new ObjectSummary(page);
await objectSummary.waitForAclVisible();

// Check Access Rights
const accessRights = await objectSummary.getAccessRights();
expect(accessRights).toEqual([{user: 'root@builtin', rights: 'Owner'}]);

// Check Effective Access Rights
const effectiveRights = await objectSummary.getEffectiveAccessRights();
expect(effectiveRights).toEqual([
{group: 'USERS', permissions: ['ConnectDatabase']},
{group: 'METADATA-READERS', permissions: ['List']},
{group: 'DATA-READERS', permissions: ['SelectRow']},
{group: 'DATA-WRITERS', permissions: ['UpdateRow', 'EraseRow']},
{
group: 'DDL-ADMINS',
permissions: [
'WriteAttributes',
'CreateDirectory',
'CreateTable',
'RemoveSchema',
'AlterSchema',
],
},
{group: 'ACCESS-ADMINS', permissions: ['GrantAccessRights']},
{group: 'DATABASE-ADMINS', permissions: ['Manage']},
]);
});

test('Copy path copies correct path to clipboard', async ({page}) => {
const pageQueryParams = {
schema: dsVslotsSchema,
database: tenantName,
general: 'query',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const objectSummary = new ObjectSummary(page);
await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.CopyPath);

// Wait for clipboard operation to complete
await page.waitForTimeout(2000);

// Retry clipboard read a few times if needed
let clipboardContent = '';
for (let i = 0; i < 3; i++) {
clipboardContent = await getClipboardContent(page);
if (clipboardContent) {
break;
}
await page.waitForTimeout(500);
}
expect(clipboardContent).toBe('.sys/ds_vslots');
});

test('Create directory in local node', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
general: 'query',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const objectSummary = new ObjectSummary(page);
await expect(objectSummary.isTreeVisible()).resolves.toBe(true);

const directoryName = `test_dir_${Date.now()}`;

// Open actions menu and click Create directory
await objectSummary.clickActionMenuItem('local', RowTableAction.CreateDirectory);
await expect(objectSummary.isCreateDirectoryModalVisible()).resolves.toBe(true);

// Create directory
await objectSummary.createDirectory(directoryName);

// Verify the new directory appears in the tree
const treeItem = page.locator('.ydb-tree-view').filter({hasText: directoryName});
await expect(treeItem).toBeVisible();
});

test('Refresh button updates tree view after creating table', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
general: 'query',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const objectSummary = new ObjectSummary(page);
const queryEditor = new QueryEditor(page);
await expect(objectSummary.isTreeVisible()).resolves.toBe(true);

const tableName = `a_test_table_${Date.now()}`;

// Create table by executing query
await queryEditor.setQuery(`CREATE TABLE \`${tableName}\` (id Int32, PRIMARY KEY(id));`);
await queryEditor.clickRunButton();
await queryEditor.waitForStatus('Completed');

// Verify table is not visible before refresh
const treeItemBeforeRefresh = page.locator('.ydb-tree-view').filter({hasText: tableName});
await expect(treeItemBeforeRefresh).not.toBeVisible();

// Click refresh button to update tree view
await objectSummary.clickRefreshButton();

// Verify table appears in tree
const treeItemAfterRefresh = page.locator('.ydb-tree-view').filter({hasText: tableName});
await expect(treeItemAfterRefresh).toBeVisible();
});
});
1 change: 1 addition & 0 deletions tests/suites/tenant/summary/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export enum RowTableAction {
UpsertQuery = 'Upsert query...',
AddIndex = 'Add index...',
CreateChangefeed = 'Create changefeed...',
CreateDirectory = 'Create directory',
}
38 changes: 38 additions & 0 deletions tests/utils/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {Page} from '@playwright/test';

export const getClipboardContent = async (page: Page): Promise<string> => {
await page.context().grantPermissions(['clipboard-read']);

// First try the modern Clipboard API
const clipboardText = await page.evaluate(async () => {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (error) {
return null;
}
});

if (clipboardText !== null) {
return clipboardText;
}

// Fallback: Create a contenteditable element, focus it, and send keyboard shortcuts
return page.evaluate(async () => {
const el = document.createElement('div');
el.contentEditable = 'true';
document.body.appendChild(el);
el.focus();

try {
// Send paste command
document.execCommand('paste');
const text = el.textContent || '';
document.body.removeChild(el);
return text;
} catch (error) {
document.body.removeChild(el);
return '';
}
});
};

0 comments on commit 38c88e5

Please sign in to comment.