Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: e2e tests part 2 #1799

Merged
merged 9 commits into from
Dec 28, 2024
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
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 '';
}
});
};
Loading