diff --git a/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx b/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx index 150ddc98f..3f8e92fc2 100644 --- a/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx +++ b/src/containers/Tenant/utils/paneVisibilityToggleHelpers.tsx @@ -91,6 +91,7 @@ export function PaneVisibilityToggleButtons({ className={b( { hidden: isCollapsed, + type: 'collapse', }, className, )} @@ -106,6 +107,7 @@ export function PaneVisibilityToggleButtons({ className={b( { hidden: !isCollapsed, + type: 'expand', }, className, )} diff --git a/tests/suites/tenant/TenantPage.ts b/tests/suites/tenant/TenantPage.ts index 15a82111c..de43bdf8f 100644 --- a/tests/suites/tenant/TenantPage.ts +++ b/tests/suites/tenant/TenantPage.ts @@ -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', diff --git a/tests/suites/tenant/queryEditor/models/QueryEditor.ts b/tests/suites/tenant/queryEditor/models/QueryEditor.ts index 67d387939..ce3aab38d 100644 --- a/tests/suites/tenant/queryEditor/models/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/models/QueryEditor.ts @@ -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 { @@ -41,6 +42,7 @@ export class QueryEditor { queryTabs: QueryTabsNavigation; resultTable: ResultTable; savedQueries: SavedQueriesTable; + historyQueries: QueriesHistoryTable; editorTextArea: Locator; private page: Page; @@ -48,6 +50,7 @@ export class QueryEditor { private runButton: Locator; private explainButton: Locator; private stopButton: Locator; + private saveButton: Locator; private gearButton: Locator; private indicatorIcon: Locator; private banner: Locator; @@ -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'); @@ -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) { @@ -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'); @@ -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); } @@ -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); } @@ -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}); } diff --git a/tests/suites/tenant/queryEditor/models/ResultTable.ts b/tests/suites/tenant/queryEditor/models/ResultTable.ts index 8798d307b..23b69b2c2 100644 --- a/tests/suites/tenant/queryEditor/models/ResultTable.ts +++ b/tests/suites/tenant/queryEditor/models/ResultTable.ts @@ -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() { @@ -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; + } + } } diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts index c02f8e9de..74352efe3 100644 --- a/tests/suites/tenant/queryEditor/queryEditor.test.ts +++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts @@ -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'; @@ -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;'; @@ -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'); + }); }); diff --git a/tests/suites/tenant/queryEditor/queryTemplates.test.ts b/tests/suites/tenant/queryEditor/queryTemplates.test.ts index ea17b6b58..56a17441a 100644 --- a/tests/suites/tenant/queryEditor/queryTemplates.test.ts +++ b/tests/suites/tenant/queryEditor/queryTemplates.test.ts @@ -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'; @@ -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', () => { diff --git a/tests/suites/tenant/queryEditor/utils.ts b/tests/suites/tenant/queryEditor/utils.ts new file mode 100644 index 000000000..67002cc1c --- /dev/null +++ b/tests/suites/tenant/queryEditor/utils.ts @@ -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); +}; diff --git a/tests/suites/tenant/queryHistory/models/QueriesHistoryTable.ts b/tests/suites/tenant/queryHistory/models/QueriesHistoryTable.ts new file mode 100644 index 000000000..cde7af3bc --- /dev/null +++ b/tests/suites/tenant/queryHistory/models/QueriesHistoryTable.ts @@ -0,0 +1,43 @@ +import type {Locator, Page} from '@playwright/test'; + +import {VISIBILITY_TIMEOUT} from '../../TenantPage'; + +export class QueriesHistoryTable { + private page: Page; + private container: Locator; + private searchInput: Locator; + private table: Locator; + + constructor(page: Page) { + this.page = page; + this.container = page.locator('.ydb-queries-history'); + this.searchInput = this.container.locator('.ydb-queries-history__search input'); + this.table = this.container.locator('.data-table'); + } + + async search(text: string) { + await this.searchInput.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await this.searchInput.fill(text); + } + + async getQueryRow(query: string) { + return this.table.locator('.ydb-queries-history__table-row', { + has: this.page.locator('.yql-highlighter', {hasText: query}), + }); + } + + async selectQuery(query: string) { + const row = await this.getQueryRow(query); + await row.click(); + } + + async getQueryText(index: number) { + const row = this.table.locator('.ydb-queries-history__table-row').nth(index); + return row.locator('.yql-highlighter').innerText(); + } + + async isVisible() { + await this.container.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } +} diff --git a/tests/suites/tenant/queryHistory/queryHistory.test.ts b/tests/suites/tenant/queryHistory/queryHistory.test.ts index 172777554..604432e1f 100644 --- a/tests/suites/tenant/queryHistory/queryHistory.test.ts +++ b/tests/suites/tenant/queryHistory/queryHistory.test.ts @@ -3,7 +3,8 @@ import {expect, test} from '@playwright/test'; import {QUERY_MODES} from '../../../../src/utils/query'; import {tenantName} from '../../../utils/constants'; import {TenantPage, VISIBILITY_TIMEOUT} from '../TenantPage'; -import {QueryEditor} from '../queryEditor/models/QueryEditor'; +import {QueryEditor, QueryTabs} from '../queryEditor/models/QueryEditor'; +import {UnsavedChangesModal} from '../queryEditor/models/UnsavedChangesModal'; import executeQueryWithKeybinding from './utils'; @@ -23,23 +24,22 @@ test.describe('Query History', () => { queryEditor = new QueryEditor(page); }); - test('New query appears in history after execution', async ({page}) => { + test('New query appears in history after execution', async () => { const testQuery = 'SELECT 1 AS test_column;'; // Execute the query await queryEditor.run(testQuery, QUERY_MODES.script); - // Navigate to the history tab - await page.click('text=History'); + // Navigate to the history tab using existing navigation method + await queryEditor.queryTabs.selectTab(QueryTabs.History); // Check if the query appears in the history - const historyTable = page.locator('.ydb-queries-history table'); - await expect(historyTable.locator('.yql-highlighter', {hasText: testQuery})).toBeVisible({ - timeout: VISIBILITY_TIMEOUT, - }); + await queryEditor.historyQueries.isVisible(); + const queryRow = await queryEditor.historyQueries.getQueryRow(testQuery); + await expect(queryRow).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); - test('Multiple queries appear in correct order in history', async ({page}) => { + test('Multiple queries appear in correct order in history', async () => { const queries = [ 'SELECT 1 AS first_query;', 'SELECT 2 AS second_query;', @@ -51,17 +51,19 @@ test.describe('Query History', () => { await queryEditor.run(query, QUERY_MODES.script); } - // Navigate to the history tab - await page.click('text=History'); + // Navigate to the history tab using existing navigation method + await queryEditor.queryTabs.selectTab(QueryTabs.History); // Check if queries appear in reverse order (most recent first) - const historyTable = page.locator('.ydb-queries-history table'); - const rows = historyTable.locator('tbody tr'); - - await expect(rows).toHaveCount(queries.length); + await queryEditor.historyQueries.isVisible(); for (let i = 0; i < queries.length; i++) { - await expect(rows.nth(i)).toContainText(queries[queries.length - 1 - i]); + const queryRow = await queryEditor.historyQueries.getQueryRow( + queries[queries.length - 1 - i], + ); + await expect(queryRow).toBeVisible(); + const queryText = await queryEditor.historyQueries.getQueryText(i); + expect(queryText).toContain(queries[queries.length - 1 - i]); } }); @@ -77,14 +79,69 @@ test.describe('Query History', () => { // Use the keybinding to execute the query await executeQueryWithKeybinding(page, browserName); - // Wait for the query to be executed - await page.waitForSelector('.ydb-query-result-sets-viewer__result', {timeout: 10000}); + // Wait for query results + await queryEditor.resultTable.isVisible(); - // Navigate to the history tab - await page.click('text=History'); + // Navigate to the history tab using existing navigation method + await queryEditor.queryTabs.selectTab(QueryTabs.History); // Check if the query appears in the history - const historyTable = page.locator('.ydb-queries-history table'); - await expect(historyTable.locator('.yql-highlighter', {hasText: testQuery})).toBeVisible(); + await queryEditor.historyQueries.isVisible(); + const queryRow = await queryEditor.historyQueries.getQueryRow(testQuery); + await expect(queryRow).toBeVisible(); + }); + + test('Can run query from history', async ({page}) => { + const testQuery = 'SELECT 42 AS history_run_test;'; + const unsavedChangesModal = new UnsavedChangesModal(page); + + // Execute the query first time + await queryEditor.run(testQuery, QUERY_MODES.script); + + // Navigate to the history tab using existing navigation method + await queryEditor.queryTabs.selectTab(QueryTabs.History); + + // Select query from history to load it into editor + await queryEditor.historyQueries.selectQuery(testQuery); + + // Handle unsaved changes modal by clicking "Don't save" + await unsavedChangesModal.clickDontSave(); + + // Run the query using the editor + await queryEditor.clickRunButton(); + + // Verify query was executed by checking results + await queryEditor.resultTable.isVisible(); + const value = await queryEditor.resultTable.getCellValue(1, 2); + expect(value).toBe('42'); + }); + + test('Can search in query history', async () => { + const queries = [ + 'SELECT 1 AS first_test;', + 'SELECT 2 AS second_test;', + 'SELECT 3 AS another_query;', + ]; + + // Execute multiple queries + for (const query of queries) { + await queryEditor.run(query, QUERY_MODES.script); + } + + // Navigate to the history tab + await queryEditor.queryTabs.selectTab(QueryTabs.History); + await queryEditor.historyQueries.isVisible(); + + // Search for "test" queries + await queryEditor.historyQueries.search('test'); + + // Verify only queries with "test" are visible + const firstQueryRow = await queryEditor.historyQueries.getQueryRow(queries[0]); + const secondQueryRow = await queryEditor.historyQueries.getQueryRow(queries[1]); + const otherQueryRow = await queryEditor.historyQueries.getQueryRow(queries[2]); + + await expect(firstQueryRow).toBeVisible(); + await expect(secondQueryRow).toBeVisible(); + await expect(otherQueryRow).not.toBeVisible(); }); }); diff --git a/tests/suites/tenant/queryEditor/models/SavedQueriesTable.ts b/tests/suites/tenant/savedQueries/models/SavedQueriesTable.ts similarity index 89% rename from tests/suites/tenant/queryEditor/models/SavedQueriesTable.ts rename to tests/suites/tenant/savedQueries/models/SavedQueriesTable.ts index 980700794..ae8d506e9 100644 --- a/tests/suites/tenant/queryEditor/models/SavedQueriesTable.ts +++ b/tests/suites/tenant/savedQueries/models/SavedQueriesTable.ts @@ -27,14 +27,18 @@ export class SavedQueriesTable { } async editQuery(name: string) { - const row = await this.getQueryRow(name); + const row = await this.waitForRow(name); + await row.hover(); const editButton = row.locator('button:has(svg)').first(); + await editButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); await editButton.click(); } async deleteQuery(name: string) { - const row = await this.getQueryRow(name); + const row = await this.waitForRow(name); + await row.hover(); const deleteButton = row.locator('button:has(svg)').nth(1); + await deleteButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); await deleteButton.click(); } diff --git a/tests/suites/tenant/savedQueries/savedQueries.test.ts b/tests/suites/tenant/savedQueries/savedQueries.test.ts new file mode 100644 index 000000000..b1d262f1a --- /dev/null +++ b/tests/suites/tenant/savedQueries/savedQueries.test.ts @@ -0,0 +1,96 @@ +import {expect, test} from '@playwright/test'; + +import {dsVslotsSchema, tenantName} from '../../../utils/constants'; +import {TenantPage} from '../TenantPage'; +import {QueryEditor, QueryTabs} from '../queryEditor/models/QueryEditor'; +import {SaveQueryDialog} from '../queryEditor/models/SaveQueryDialog'; +import {UnsavedChangesModal} from '../queryEditor/models/UnsavedChangesModal'; + +import {SavedQueriesTable} from './models/SavedQueriesTable'; + +test.describe('Saved Queries', () => { + let tenantPage: TenantPage; + let queryEditor: QueryEditor; + let saveQueryDialog: SaveQueryDialog; + let savedQueriesTable: SavedQueriesTable; + let unsavedChangesModal: UnsavedChangesModal; + + test.beforeEach(async ({page}) => { + const pageQueryParams = { + schema: dsVslotsSchema, + database: tenantName, + general: 'query', + }; + + tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + queryEditor = new QueryEditor(page); + saveQueryDialog = new SaveQueryDialog(page); + savedQueriesTable = new SavedQueriesTable(page); + unsavedChangesModal = new UnsavedChangesModal(page); + }); + + test('View list of saved queries', async () => { + // First save a query to ensure there's something in the list + const testQuery = 'SELECT 1 AS test_column;'; + const queryName = `Test Query ${Date.now()}`; + + await queryEditor.setQuery(testQuery); + await queryEditor.clickSaveButton(); + await saveQueryDialog.setQueryName(queryName); + await saveQueryDialog.clickSave(); + + // Navigate to saved queries tab + await queryEditor.queryTabs.selectTab(QueryTabs.Saved); + await savedQueriesTable.isVisible(); + + // Verify saved queries list is displayed and contains our query + const names = await savedQueriesTable.getQueryNames(); + expect(names).toContain(queryName); + }); + + test('Open saved query in the Editor', async () => { + // First save a query + const testQuery = 'SELECT 2 AS editor_test;'; + const queryName = `Editor Test ${Date.now()}`; + + await queryEditor.setQuery(testQuery); + await queryEditor.clickSaveButton(); + await saveQueryDialog.setQueryName(queryName); + await saveQueryDialog.clickSave(); + + // Navigate to saved queries tab + await queryEditor.queryTabs.selectTab(QueryTabs.Saved); + await savedQueriesTable.isVisible(); + + // Open the query in editor + await savedQueriesTable.editQuery(queryName); + + // Handle unsaved changes dialog + await unsavedChangesModal.clickDontSave(); + + // Verify query is loaded in editor + const editorValue = await queryEditor.editorTextArea.inputValue(); + expect(editorValue.trim()).toBe(testQuery.trim()); + }); + + test('Save a query from the Editor', async () => { + const testQuery = 'SELECT 3 AS new_query;'; + const queryName = `New Query ${Date.now()}`; + + // Write query in editor and save it + await queryEditor.setQuery(testQuery); + await queryEditor.clickSaveButton(); + await saveQueryDialog.setQueryName(queryName); + await saveQueryDialog.clickSave(); + + // Navigate to saved queries tab to verify + await queryEditor.queryTabs.selectTab(QueryTabs.Saved); + await savedQueriesTable.isVisible(); + + // Verify query was saved correctly + const row = await savedQueriesTable.getRowByName(queryName); + expect(row).not.toBe(null); + expect(row?.query.trim()).toBe(testQuery.trim()); + }); +}); diff --git a/tests/suites/tenant/summary/ObjectSummary.ts b/tests/suites/tenant/summary/ObjectSummary.ts index d5105a0f1..0489751c9 100644 --- a/tests/suites/tenant/summary/ObjectSummary.ts +++ b/tests/suites/tenant/summary/ObjectSummary.ts @@ -1,5 +1,6 @@ import type {Locator, Page} from '@playwright/test'; +import {isInViewport} from '../../../utils/dom'; import {VISIBILITY_TIMEOUT} from '../TenantPage'; import {ActionsMenu} from './ActionsMenu'; @@ -24,6 +25,11 @@ export class ObjectSummary { private createDirectoryInput: Locator; private createDirectoryButton: Locator; private refreshButton: Locator; + private infoCollapseButton: Locator; + private infoExpandButton: Locator; + private summaryCollapseButton: Locator; + private summaryExpandButton: Locator; + private overviewWrapper: Locator; constructor(page: Page) { this.tree = page.locator('.ydb-object-summary__tree'); @@ -41,6 +47,45 @@ export class ObjectSummary { ); this.createDirectoryButton = page.locator('button.g-button_view_action:has-text("Create")'); this.refreshButton = page.locator('.ydb-object-summary__refresh-button'); + + // Info panel collapse/expand buttons + this.infoCollapseButton = page.locator( + '.ydb-object-summary__info-controls .kv-pane-visibility-button_type_collapse', + ); + this.infoExpandButton = page.locator( + '.ydb-object-summary__info-controls .kv-pane-visibility-button_type_expand', + ); + this.summaryCollapseButton = page.locator( + '.ydb-object-summary__actions .kv-pane-visibility-button_type_collapse', + ); + this.summaryExpandButton = page.locator( + '.ydb-object-summary__actions .kv-pane-visibility-button_type_expand', + ); + this.overviewWrapper = page.locator('.ydb-object-summary__overview-wrapper'); + } + + async collapseInfoPanel(): Promise { + await this.infoCollapseButton.click(); + } + + async expandInfoPanel(): Promise { + await this.infoExpandButton.click(); + } + + async isInfoPanelCollapsed(): Promise { + const expandButtonVisible = await this.infoExpandButton.isVisible(); + if (!expandButtonVisible) { + return false; + } + + const isVisible = await this.overviewWrapper.isVisible(); + if (!isVisible) { + return true; + } + + // Check if it's actually in the viewport + const elementInViewport = await isInViewport(this.overviewWrapper); + return !elementInViewport; } async isCreateDirectoryModalVisible(): Promise { @@ -201,4 +246,28 @@ export class ObjectSummary { async clickRefreshButton(): Promise { await this.refreshButton.click(); } + + async collapseSummary(): Promise { + await this.summaryCollapseButton.click(); + } + + async expandSummary(): Promise { + await this.summaryExpandButton.click(); + } + + async isSummaryCollapsed(): Promise { + const expandButtonVisible = await this.summaryExpandButton.isVisible(); + if (!expandButtonVisible) { + return false; + } + + const isVisible = await this.tree.isVisible(); + if (!isVisible) { + return true; + } + + // Check if it's actually in the viewport + const elementInViewport = await isInViewport(this.tree); + return !elementInViewport; + } } diff --git a/tests/suites/tenant/summary/objectSummary.test.ts b/tests/suites/tenant/summary/objectSummary.test.ts index 8afcda6de..e9893748f 100644 --- a/tests/suites/tenant/summary/objectSummary.test.ts +++ b/tests/suites/tenant/summary/objectSummary.test.ts @@ -285,4 +285,28 @@ test.describe('Object Summary', async () => { const treeItemAfterRefresh = page.locator('.ydb-tree-view').filter({hasText: tableName}); await expect(treeItemAfterRefresh).toBeVisible(); }); + + test('Info panel collapse and expand functionality', async ({page}) => { + const objectSummary = new ObjectSummary(page); + await expect(objectSummary.isTreeVisible()).resolves.toBe(true); + + // Test info panel collapse/expand + await objectSummary.collapseInfoPanel(); + await expect(objectSummary.isInfoPanelCollapsed()).resolves.toBe(true); + + await objectSummary.expandInfoPanel(); + await expect(objectSummary.isInfoPanelCollapsed()).resolves.toBe(false); + }); + + test('Summary collapse and expand functionality', async ({page}) => { + const objectSummary = new ObjectSummary(page); + await expect(objectSummary.isTreeVisible()).resolves.toBe(true); + + // Test summary collapse/expand + await objectSummary.collapseSummary(); + await expect(objectSummary.isSummaryCollapsed()).resolves.toBe(true); + + await objectSummary.expandSummary(); + await expect(objectSummary.isSummaryCollapsed()).resolves.toBe(false); + }); }); diff --git a/tests/utils/dom.ts b/tests/utils/dom.ts new file mode 100644 index 000000000..76eacd854 --- /dev/null +++ b/tests/utils/dom.ts @@ -0,0 +1,18 @@ +import type {Locator} from '@playwright/test'; + +/** + * Checks if an element is within the viewport + * @param locator - Playwright locator for the element to check + * @returns Promise - true if the element is within viewport bounds + */ +export const isInViewport = async (locator: Locator): Promise => { + return locator.evaluate((el) => { + const rect = el.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + }); +};