Skip to content
2 changes: 2 additions & 0 deletions src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function PaneVisibilityToggleButtons({
className={b(
{
hidden: isCollapsed,
type: 'collapse',
},
className,
)}
Expand All @@ -106,6 +107,7 @@ export function PaneVisibilityToggleButtons({
className={b(
{
hidden: !isCollapsed,
type: 'expand',
},
className,
)}
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
74 changes: 73 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,36 @@ export class QueryEditor {
return true;
}

async collapseResultsControls() {
const collapseButton = this.resultsControls.locator(
'.kv-pane-visibility-button_type_collapse',
);
await collapseButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await collapseButton.click();
}

async expandResultsControls() {
const expandButton = this.resultsControls.locator('.kv-pane-visibility-button_type_expand');
await expandButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
await expandButton.click();
}

async isResultsControlsCollapsed() {
const expandButton = this.resultsControls.locator('.kv-pane-visibility-button_type_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