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: more tests #1805

Merged
merged 13 commits into from
Jan 13, 2025
2 changes: 2 additions & 0 deletions src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function PaneVisibilityToggleButtons({
<React.Fragment>
<ActionTooltip title="Collapse">
<Button
title="Collapse"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not use ActionTooltip together with native tooltip:
Screenshot 2025-01-11 at 11 34 11

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added type subclass to buttons

view="flat-secondary"
onClick={onCollapse}
className={b(
Expand All @@ -101,6 +102,7 @@ export function PaneVisibilityToggleButtons({

<ActionTooltip title="Expand">
<Button
title="Expand"
view="flat-secondary"
onClick={onExpand}
className={b(
Expand Down
2 changes: 1 addition & 1 deletion tests/suites/tenant/TenantPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {Locator, Page} from '@playwright/test';
import {PageModel} from '../../models/PageModel';
import {tenantPage} from '../../utils/constants';

export const VISIBILITY_TIMEOUT = 10000;
export const VISIBILITY_TIMEOUT = 10 * 1000;

export enum NavigationTabs {
Query = 'Query',
Expand Down
72 changes: 71 additions & 1 deletion tests/suites/tenant/queryEditor/models/QueryEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import type {Locator, Page} from '@playwright/test';

import type {QUERY_MODES} from '../../../../../src/utils/query';
import {VISIBILITY_TIMEOUT} from '../../TenantPage';
import {QueriesHistoryTable} from '../../queryHistory/models/QueriesHistoryTable';
import {SavedQueriesTable} from '../../savedQueries/models/SavedQueriesTable';

import {QueryTabsNavigation} from './QueryTabsNavigation';
import {PaneWrapper, ResultTable} from './ResultTable';
import {SavedQueriesTable} from './SavedQueriesTable';
import {SettingsDialog} from './SettingsDialog';

export enum ExplainResultType {
Expand Down Expand Up @@ -41,13 +42,15 @@ export class QueryEditor {
queryTabs: QueryTabsNavigation;
resultTable: ResultTable;
savedQueries: SavedQueriesTable;
historyQueries: QueriesHistoryTable;
editorTextArea: Locator;

private page: Page;
private selector: Locator;
private runButton: Locator;
private explainButton: Locator;
private stopButton: Locator;
private saveButton: Locator;
private gearButton: Locator;
private indicatorIcon: Locator;
private banner: Locator;
Expand All @@ -63,6 +66,7 @@ export class QueryEditor {
this.runButton = this.selector.getByRole('button', {name: ButtonNames.Run});
this.stopButton = this.selector.getByRole('button', {name: ButtonNames.Stop});
this.explainButton = this.selector.getByRole('button', {name: ButtonNames.Explain});
this.saveButton = this.selector.getByRole('button', {name: ButtonNames.Save});
this.gearButton = this.selector.locator('.ydb-query-editor-controls__gear-button');
this.executionStatus = this.selector.locator('.kv-query-execution-status');
this.resultsControls = this.selector.locator('.ydb-query-result__controls');
Expand All @@ -78,6 +82,7 @@ export class QueryEditor {
this.paneWrapper = new PaneWrapper(page);
this.queryTabs = new QueryTabsNavigation(page);
this.savedQueries = new SavedQueriesTable(page);
this.historyQueries = new QueriesHistoryTable(page);
}

async run(query: string, mode: keyof typeof QUERY_MODES) {
Expand Down Expand Up @@ -116,6 +121,11 @@ export class QueryEditor {
await this.explainButton.click();
}

async clickSaveButton() {
await this.saveButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await this.saveButton.click();
}

async getExplainResult(type: ExplainResultType) {
await this.selectResultTypeRadio(type);
const resultArea = this.selector.locator('.ydb-query-result__result');
Expand Down Expand Up @@ -144,6 +154,37 @@ export class QueryEditor {
await this.editorTextArea.focus();
}

async selectText(startLine: number, startColumn: number, endLine: number, endColumn: number) {
await this.editorTextArea.evaluate(
(_, coords) => {
const editor = window.ydbEditor;
if (editor) {
editor.setSelection({
startLineNumber: coords.startLine,
startColumn: coords.startColumn,
endLineNumber: coords.endLine,
endColumn: coords.endColumn,
});
}
},
{startLine, startColumn, endLine, endColumn},
);
}

async pressKeys(key: string) {
await this.editorTextArea.press(key);
}

async runSelectedQueryViaContextMenu() {
await this.editorTextArea.evaluate(() => {
const editor = window.ydbEditor;
if (editor) {
// Trigger the sendSelectedQuery action directly
editor.trigger('contextMenu', 'sendSelectedQuery', null);
}
});
}

async closeSettingsDialog() {
await this.settingsDialog.clickButton(ButtonNames.Cancel);
}
Expand All @@ -166,6 +207,7 @@ export class QueryEditor {

async setQuery(query: string) {
await this.editorTextArea.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await this.editorTextArea.clear();
await this.editorTextArea.fill(query);
}

Expand Down Expand Up @@ -205,6 +247,34 @@ export class QueryEditor {
return true;
}

async collapseResultsControls() {
const collapseButton = this.resultsControls.locator('button[title="Collapse"]');
await collapseButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await collapseButton.click();
}

async expandResultsControls() {
const expandButton = this.resultsControls.locator('button[title="Expand"]');
await expandButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await expandButton.click();
}

async isResultsControlsCollapsed() {
const expandButton = this.resultsControls.locator('button[title="Expand"]');
try {
await expandButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return true;
} catch {
return false;
}
}

async clickCopyResultButton() {
const copyButton = this.resultsControls.locator('button[title="Copy result"]');
await copyButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await copyButton.click();
}

async isRunButtonEnabled() {
return this.runButton.isEnabled({timeout: VISIBILITY_TIMEOUT});
}
Expand Down
32 changes: 32 additions & 0 deletions tests/suites/tenant/queryEditor/models/ResultTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export class ResultTable {
private table: Locator;
private preview: Locator;
private resultHead: Locator;
private resultWrapper: Locator;

constructor(selector: Locator) {
this.table = selector.locator('.ydb-query-result-sets-viewer__result');
this.preview = selector.locator('.kv-preview__result');
this.resultHead = selector.locator('.ydb-query-result-sets-viewer__head');
this.resultWrapper = selector.locator('.ydb-query-result-sets-viewer__result-wrapper');
}

async isVisible() {
Expand Down Expand Up @@ -70,4 +72,34 @@ export class ResultTable {
await this.resultHead.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return this.resultHead.innerText();
}

async getResultTabs() {
const tabs = this.resultWrapper.locator(
'.ydb-query-result-sets-viewer__tabs .g-tabs__item',
);
await tabs.first().waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return tabs;
}

async getResultTabsCount() {
const tabs = await this.getResultTabs();
return tabs.count();
}

async getResultTabTitle(index: number) {
const tabs = await this.getResultTabs();
const tab = tabs.nth(index);
await tab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return tab.getAttribute('title');
}

async hasMultipleResultTabs() {
const tabs = this.resultWrapper.locator('.ydb-query-result-sets-viewer__tabs');
try {
await tabs.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return true;
} catch {
return false;
}
}
}
106 changes: 106 additions & 0 deletions tests/suites/tenant/queryEditor/queryEditor.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {expect, test} from '@playwright/test';

import {QUERY_MODES, STATISTICS_MODES} from '../../../../src/utils/query';
import {getClipboardContent} from '../../../utils/clipboard';
import {tenantName} from '../../../utils/constants';
import {NavigationTabs, TenantPage, VISIBILITY_TIMEOUT} from '../TenantPage';
import {createTableQuery, longRunningQuery, longTableSelect} from '../constants';
Expand All @@ -12,6 +13,7 @@ import {
QueryTabs,
ResultTabNames,
} from './models/QueryEditor';
import {executeSelectedQueryWithKeybinding} from './utils';

test.describe('Test Query Editor', async () => {
const testQuery = 'SELECT 1, 2, 3, 4, 5;';
Expand Down Expand Up @@ -241,4 +243,108 @@ test.describe('Test Query Editor', async () => {

await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);
});

test('Running selected query via keyboard shortcut executes only selected part', async ({
page,
}) => {
const queryEditor = new QueryEditor(page);
const multiQuery = 'SELECT 1;\nSELECT 2;';

// First verify running the entire query produces two results
await queryEditor.setQuery(multiQuery);
await queryEditor.clickRunButton();
await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);

// Verify there are two result tabs
await expect(queryEditor.resultTable.getResultTabsCount()).resolves.toBe(2);
await expect(queryEditor.resultTable.getResultTabTitle(0)).resolves.toBe('Result #1');
await expect(queryEditor.resultTable.getResultTabTitle(1)).resolves.toBe('Result #2');

// Then verify running only selected part produces one result
await queryEditor.focusEditor();
await queryEditor.selectText(1, 1, 1, 9);

// Use keyboard shortcut to run selected query
await executeSelectedQueryWithKeybinding(page);

await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);
await expect(queryEditor.resultTable.hasMultipleResultTabs()).resolves.toBe(false);
await expect(queryEditor.resultTable.getResultHeadText()).resolves.toBe('Result(1)');
});

test('Running selected query via context menu executes only selected part', async ({page}) => {
const queryEditor = new QueryEditor(page);
const multiQuery = 'SELECT 1;\nSELECT 2;';

// First verify running the entire query produces two results with tabs
await queryEditor.setQuery(multiQuery);
await queryEditor.clickRunButton();
await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);

// Verify there are two result tabs
await expect(queryEditor.resultTable.getResultTabsCount()).resolves.toBe(2);
await expect(queryEditor.resultTable.getResultTabTitle(0)).resolves.toBe('Result #1');
await expect(queryEditor.resultTable.getResultTabTitle(1)).resolves.toBe('Result #2');

// Then verify running only selected part produces one result without tabs
await queryEditor.focusEditor();
await queryEditor.selectText(1, 1, 1, 9);

// Use context menu to run selected query
await queryEditor.runSelectedQueryViaContextMenu();

await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);
await expect(queryEditor.resultTable.hasMultipleResultTabs()).resolves.toBe(false);
await expect(queryEditor.resultTable.getResultHeadText()).resolves.toBe('Result(1)');
});

test('Results controls collapse and expand functionality', async ({page}) => {
const queryEditor = new QueryEditor(page);

// Run a query to show results
await queryEditor.setQuery('SELECT 1;');
await queryEditor.clickRunButton();
await queryEditor.waitForStatus('Completed');

// Verify controls are initially visible
await expect(queryEditor.isResultsControlsVisible()).resolves.toBe(true);
await expect(queryEditor.isResultsControlsCollapsed()).resolves.toBe(false);

// Test collapse
await queryEditor.collapseResultsControls();
await expect(queryEditor.isResultsControlsCollapsed()).resolves.toBe(true);

// Test expand
await queryEditor.expandResultsControls();
await expect(queryEditor.isResultsControlsCollapsed()).resolves.toBe(false);
});

test('Copy result button copies to clipboard', async ({page}) => {
const queryEditor = new QueryEditor(page);
const query = 'SELECT 42 as answer;';

// Run query to get results
await queryEditor.setQuery(query);
await queryEditor.clickRunButton();
await queryEditor.waitForStatus('Completed');

// Click copy button
await queryEditor.clickCopyResultButton();

// 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);
}

// Verify clipboard contains the query result
expect(clipboardContent).toContain('42');
});
});
2 changes: 1 addition & 1 deletion tests/suites/tenant/queryEditor/queryTemplates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {expect, test} from '@playwright/test';

import {dsVslotsSchema, dsVslotsTableName, tenantName} from '../../../utils/constants';
import {TenantPage} from '../TenantPage';
import {SavedQueriesTable} from '../savedQueries/models/SavedQueriesTable';
import {ObjectSummary} from '../summary/ObjectSummary';
import {RowTableAction} from '../summary/types';

Expand All @@ -13,7 +14,6 @@ import {
} from './models/NewSqlDropdownMenu';
import {QueryEditor, QueryTabs} from './models/QueryEditor';
import {SaveQueryDialog} from './models/SaveQueryDialog';
import {SavedQueriesTable} from './models/SavedQueriesTable';
import {UnsavedChangesModal} from './models/UnsavedChangesModal';

test.describe('Query Templates', () => {
Expand Down
20 changes: 20 additions & 0 deletions tests/suites/tenant/queryEditor/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {Page} from '@playwright/test';

export const executeSelectedQueryWithKeybinding = async (page: Page) => {
const isMac = process.platform === 'darwin';
const browserName = page.context().browser()?.browserType().name() ?? 'chromium';
const modifierKey = browserName === 'webkit' ? 'Meta' : 'Control';

if (browserName !== 'webkit' || isMac) {
await page.keyboard.down(modifierKey);
await page.keyboard.down('Shift');
await page.keyboard.press('Enter');
await page.keyboard.up('Shift');
await page.keyboard.up(modifierKey);
} else {
await page.keyboard.press('Meta+Shift+Enter');
}

// Add a small delay to ensure the event is processed
await page.waitForTimeout(1000);
};
Loading
Loading