diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7152493b..4491a195f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,10 @@ jobs: e2e_tests: name: Playwright Tests runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + pages: write services: backend: @@ -50,7 +54,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 with: node-version: 18 cache: npm @@ -58,8 +64,134 @@ jobs: - name: Install dependencies run: npm ci - - name: Install Plawright deps + - name: Install Playwright deps run: npm run test:e2e:install - name: Run Playwright tests + id: run_tests run: npm run test:e2e + env: + CI: true + PLAYWRIGHT_VIDEO: 'on' + + - name: Upload Playwright artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: playwright-artifacts + path: playwright-artifacts + retention-days: 5 + + - name: Setup Pages + if: always() + uses: actions/configure-pages@v3 + + - name: Deploy report to GitHub Pages + if: always() + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./playwright-artifacts/playwright-report + destination_dir: ${{ github.event.pull_request.number }} + + - name: Get test results + if: always() + id: test-results + run: | + echo "Current directory: $(pwd)" + echo "Listing playwright-artifacts directory:" + ls -R playwright-artifacts + + if [ -f "playwright-artifacts/test-results.json" ]; then + echo "Parsing JSON file:" + total=$(jq '.stats.expected + .stats.unexpected + .stats.skipped' playwright-artifacts/test-results.json) + passed=$(jq '.stats.expected' playwright-artifacts/test-results.json) + failed=$(jq '.stats.unexpected' playwright-artifacts/test-results.json) + skipped=$(jq '.stats.skipped' playwright-artifacts/test-results.json) + + echo "Parsed values:" + echo "Total: $total" + echo "Passed: $passed" + echo "Failed: $failed" + echo "Skipped: $skipped" + else + echo "test-results.json file not found" + total=0 + passed=0 + failed=0 + skipped=0 + fi + + echo "total=$total" >> $GITHUB_OUTPUT + echo "passed=$passed" >> $GITHUB_OUTPUT + echo "failed=$failed" >> $GITHUB_OUTPUT + echo "skipped=$skipped" >> $GITHUB_OUTPUT + + - name: Update PR description + if: always() + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const reportUrl = `https://${context.repo.owner}.github.io/${context.repo.repo}/${context.issue.number}/`; + const testResults = { + total: ${{ steps.test-results.outputs.total || 0 }}, + passed: ${{ steps.test-results.outputs.passed || 0 }}, + failed: ${{ steps.test-results.outputs.failed || 0 }}, + skipped: ${{ steps.test-results.outputs.skipped || 0 }} + }; + const status = testResults.failed > 0 ? '❌ FAILED' : '✅ PASSED'; + const statusColor = testResults.failed > 0 ? 'red' : 'green'; + + const testResultsSection = `## Playwright Test Results + + **Status**: ${status} + + | Total | Passed | Failed | Skipped | + |-------|--------|--------|---------| + | ${testResults.total} | ${testResults.passed} | ${testResults.failed} | ${testResults.skipped} | + + ### 📊 Test Report + + For detailed results, please check the [Playwright Report](${reportUrl}) + + ### đŸŽĨ Test Recordings + + Video recordings of failed tests (if any) are available in the report. + + --- + +
+ ℹī¸ How to use this report + + 1. Click on the "Playwright Report" link above to view the full test results. + 2. In the report, you can see a breakdown of all tests, including any failures. + 3. For failed tests, you can view screenshots and video recordings to help debug the issues. + 4. Use the filters in the report to focus on specific test statuses or suites. + +
`; + + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + const currentBody = pullRequest.body || ''; + const testResultsRegex = /## Playwright Test Results[\s\S]*?(?=\n## |$)/; + + let newBody; + if (testResultsRegex.test(currentBody)) { + // If the section exists, replace it + newBody = currentBody.replace(testResultsRegex, testResultsSection); + } else { + // If the section doesn't exist, add it to the end + newBody = currentBody + '\n\n' + testResultsSection; + } + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body: newBody, + }); diff --git a/.gitignore b/.gitignore index 4e6afaad9..9b0cfc591 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # testing /coverage +playwright-artifacts # production /build diff --git a/playwright.config.ts b/playwright.config.ts index f47feb143..c12f258e8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,12 @@ const baseUrl = process.env.PLAYWRIGHT_BASE_URL; const config: PlaywrightTestConfig = { testDir: 'tests/suites', timeout: 2 * 60 * 1000, + outputDir: './playwright-artifacts/test-results', + reporter: [ + ['html', {outputFolder: './playwright-artifacts/playwright-report'}], + ['json', {outputFile: './playwright-artifacts/test-results.json'}], + ], + // If there is no url provided, playwright starts webServer with the app in dev mode webServer: baseUrl ? undefined @@ -16,6 +22,9 @@ const config: PlaywrightTestConfig = { use: { baseURL: baseUrl || 'http://localhost:3000/', testIdAttribute: 'data-qa', + trace: 'on-first-retry', + video: 'retain-on-failure', + screenshot: 'only-on-failure', }, projects: [ { diff --git a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx index f6a202ccb..7471c5f34 100644 --- a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx +++ b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx @@ -5,7 +5,6 @@ import {Icon, Tooltip} from '@gravity-ui/uikit'; import {isAxiosError} from 'axios'; import i18n from '../../containers/Tenant/Query/i18n'; -import {QUERY_SETTINGS, useSetting} from '../../lib'; import {cn} from '../../utils/cn'; import {useChangedQuerySettings} from '../../utils/hooks/useChangedQuerySettings'; import QuerySettingsDescription from '../QuerySettingsDescription/QuerySettingsDescription'; @@ -20,10 +19,9 @@ interface QueryExecutionStatusProps { } const QuerySettingsIndicator = () => { - const [useQuerySettings] = useSetting(QUERY_SETTINGS); const {isIndicatorShown, changedLastExecutionSettingsDescriptions} = useChangedQuerySettings(); - if (!isIndicatorShown || !useQuerySettings) { + if (!isIndicatorShown) { return null; } diff --git a/src/containers/Authentication/Authentication.tsx b/src/containers/Authentication/Authentication.tsx index 7fd8f935b..3e3f66c7a 100644 --- a/src/containers/Authentication/Authentication.tsx +++ b/src/containers/Authentication/Authentication.tsx @@ -90,7 +90,7 @@ function Authentication({closable = false}: AuthenticationProps) { YDB - + Documentation diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index f977cac0c..0125f830b 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -11,14 +11,13 @@ import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import {YDBGraph} from '../../../../components/Graph/Graph'; import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus'; import {QueryResultTable} from '../../../../components/QueryResultTable/QueryResultTable'; -import {QUERY_SETTINGS} from '../../../../lib'; import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import type {ColumnType, KeyValueRow} from '../../../../types/api/query'; import type {ValueOf} from '../../../../types/common'; import type {IQueryResult} from '../../../../types/store/query'; import {getArray} from '../../../../utils'; import {cn} from '../../../../utils/cn'; -import {useSetting, useTypedDispatch} from '../../../../utils/hooks'; +import {useTypedDispatch} from '../../../../utils/hooks'; import {parseQueryError} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {SimplifiedPlan} from '../ExplainResult/components/SimplifiedPlan/SimplifiedPlan'; @@ -63,7 +62,6 @@ export function ExecuteResult({ const [selectedResultSet, setSelectedResultSet] = React.useState(0); const [activeSection, setActiveSection] = React.useState(resultOptionsIds.result); const dispatch = useTypedDispatch(); - const [useQuerySettings] = useSetting(QUERY_SETTINGS); const stats = data?.stats; const resultsSetsCount = data?.resultSets?.length; @@ -237,7 +235,7 @@ export function ExecuteResult({ /> - {useQuerySettings && } + {renderResultSection()} ); diff --git a/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx b/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx index 015279884..7a8dff67d 100644 --- a/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx +++ b/src/containers/Tenant/Query/ExplainResult/ExplainResult.tsx @@ -7,12 +7,11 @@ import EnableFullscreenButton from '../../../../components/EnableFullscreenButto import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus'; -import {QUERY_SETTINGS} from '../../../../lib'; import type {PreparedExplainResponse} from '../../../../store/reducers/explainQuery/types'; import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import type {ValueOf} from '../../../../types/common'; import {cn} from '../../../../utils/cn'; -import {useSetting, useTypedDispatch} from '../../../../utils/hooks'; +import {useTypedDispatch} from '../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner'; @@ -82,8 +81,6 @@ export function ExplainResult({ ); const [isPending, startTransition] = React.useTransition(); - const [useQuerySettings] = useSetting(QUERY_SETTINGS); - React.useEffect(() => { return () => { dispatch(disableFullscreen()); @@ -167,7 +164,7 @@ export function ExplainResult({ )} - {useQuerySettings && } + {renderContent()} diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx index 295dbf010..1f5e600ea 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx @@ -7,12 +7,7 @@ import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants' import {setQueryTab} from '../../../../store/reducers/tenant/tenant'; import type {QueryInHistory} from '../../../../types/store/executeQuery'; import {cn} from '../../../../utils/cn'; -import { - useQueryExecutionSettings, - useTypedDispatch, - useTypedSelector, -} from '../../../../utils/hooks'; -import {QUERY_MODES, QUERY_SYNTAX} from '../../../../utils/query'; +import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants'; import i18n from '../i18n'; @@ -29,19 +24,10 @@ interface QueriesHistoryProps { function QueriesHistory({changeUserInput}: QueriesHistoryProps) { const dispatch = useTypedDispatch(); - const [settings, setQuerySettings] = useQueryExecutionSettings(); - const queriesHistory = useTypedSelector(selectQueriesHistory); const reversedHistory = [...queriesHistory].reverse(); const onQueryClick = (query: QueryInHistory) => { - if (query.syntax === QUERY_SYNTAX.pg && settings.queryMode !== QUERY_MODES.pg) { - setQuerySettings({...settings, queryMode: QUERY_MODES.pg}); - } else if (query.syntax !== QUERY_SYNTAX.pg && settings.queryMode === QUERY_MODES.pg) { - // Set query mode for queries with yql syntax - setQuerySettings({...settings, queryMode: QUERY_MODES.script}); - } - changeUserInput({input: query.queryText}); dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); }; @@ -60,15 +46,6 @@ function QueriesHistory({changeUserInput}: QueriesHistoryProps) { sortable: false, width: 600, }, - { - name: 'syntax', - header: 'Syntax', - render: ({row}) => { - return row.syntax === QUERY_SYNTAX.pg ? 'PostgreSQL' : 'YQL'; - }, - sortable: false, - width: 200, - }, ]; return ( diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index 84d619261..a83b61d99 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -22,20 +22,20 @@ import {setShowPreview} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import type {ValueOf} from '../../../../types/common'; import type {ExecuteQueryState} from '../../../../types/store/executeQuery'; -import type {IQueryResult, QueryAction, QuerySettings} from '../../../../types/store/query'; +import type {IQueryResult, QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { DEFAULT_IS_QUERY_RESULT_COLLAPSED, DEFAULT_SIZE_RESULT_PANE_KEY, + ENABLE_TRACING_LEVEL_KEY, LAST_USED_QUERY_ACTION_KEY, - QUERY_SETTINGS, QUERY_USE_MULTI_SCHEMA_KEY, } from '../../../../utils/constants'; import {useQueryExecutionSettings, useSetting} from '../../../../utils/hooks'; import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings'; import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings'; import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats'; -import {QUERY_ACTIONS, STATISTICS_MODES} from '../../../../utils/query'; +import {QUERY_ACTIONS} from '../../../../utils/query'; import type {InitialPaneState} from '../../utils/paneVisibilityToggleHelpers'; import { PaneVisibilityActionTypes, @@ -106,9 +106,9 @@ function QueryEditor(props: QueryEditorProps) { const {tenantPath: savedPath} = executeQuery; const [resultType, setResultType] = React.useState(RESULT_TYPES.EXECUTE); - const [querySettingsFlag] = useSetting(QUERY_SETTINGS); const [isResultLoaded, setIsResultLoaded] = React.useState(false); - const [querySettings, setQuerySettings] = useQueryExecutionSettings(); + const [querySettings] = useQueryExecutionSettings(); + const [enableTracingLevel] = useSetting(ENABLE_TRACING_LEVEL_KEY); const [lastQueryExecutionSettings, setLastQueryExecutionSettings] = useLastQueryExecutionSettings(); const {resetBanner} = useChangedQuerySettings(); @@ -193,32 +193,26 @@ function QueryEditor(props: QueryEditorProps) { }, [executeQuery]); const handleSendExecuteClick = React.useCallback( - (settings: QuerySettings, text?: string) => { + (text?: string) => { const {input, history} = executeQuery; const schema = useMultiSchema ? 'multi' : 'modern'; - const query = text ?? input; + const query = text && typeof text === 'string' ? text : input; setLastUsedQueryAction(QUERY_ACTIONS.execute); - if (!isEqual(lastQueryExecutionSettings, settings)) { + if (!isEqual(lastQueryExecutionSettings, querySettings)) { resetBanner(); - setLastQueryExecutionSettings(settings); + setLastQueryExecutionSettings(querySettings); } - const executeQuerySettings = querySettingsFlag - ? querySettings - : { - queryMode: querySettings.queryMode, - statisticsMode: STATISTICS_MODES.full, - }; - setResultType(RESULT_TYPES.EXECUTE); sendExecuteQuery({ query, database: tenantName, - querySettings: executeQuerySettings, + querySettings, schema, + enableTracingLevel, }); setIsResultLoaded(true); setShowPreview(false); @@ -227,17 +221,17 @@ function QueryEditor(props: QueryEditorProps) { if (!text) { const {queries, currentIndex} = history; if (query !== queries[currentIndex]?.queryText) { - saveQueryToHistory(input, querySettings.queryMode); + saveQueryToHistory(input); } } dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); }, [ executeQuery, + enableTracingLevel, useMultiSchema, setLastUsedQueryAction, lastQueryExecutionSettings, - querySettingsFlag, querySettings, sendExecuteQuery, saveQueryToHistory, @@ -253,46 +247,38 @@ function QueryEditor(props: QueryEditorProps) { props.setShowPreview(false); }; - const handleGetExplainQueryClick = React.useCallback( - (settings: QuerySettings) => { - const {input} = executeQuery; - - setLastUsedQueryAction(QUERY_ACTIONS.explain); + const handleGetExplainQueryClick = React.useCallback(() => { + const {input} = executeQuery; - if (!isEqual(lastQueryExecutionSettings, settings)) { - resetBanner(); - setLastQueryExecutionSettings(settings); - } + setLastUsedQueryAction(QUERY_ACTIONS.explain); - const explainQuerySettings = querySettingsFlag - ? querySettings - : { - queryMode: querySettings.queryMode, - }; + if (!isEqual(lastQueryExecutionSettings, querySettings)) { + resetBanner(); + setLastQueryExecutionSettings(querySettings); + } - setResultType(RESULT_TYPES.EXPLAIN); - sendExplainQuery({ - query: input, - database: tenantName, - querySettings: explainQuerySettings, - }); - setIsResultLoaded(true); - setShowPreview(false); - dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); - }, - [ - executeQuery, - lastQueryExecutionSettings, + setResultType(RESULT_TYPES.EXPLAIN); + sendExplainQuery({ + query: input, + database: tenantName, querySettings, - querySettingsFlag, - resetBanner, - sendExplainQuery, - setLastQueryExecutionSettings, - setLastUsedQueryAction, - setShowPreview, - tenantName, - ], - ); + enableTracingLevel, + }); + setIsResultLoaded(true); + setShowPreview(false); + dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); + }, [ + executeQuery, + lastQueryExecutionSettings, + querySettings, + resetBanner, + enableTracingLevel, + sendExplainQuery, + setLastQueryExecutionSettings, + setLastUsedQueryAction, + setShowPreview, + tenantName, + ]); React.useEffect(() => { if (monacoHotKey === null) { @@ -302,9 +288,9 @@ function QueryEditor(props: QueryEditorProps) { switch (monacoHotKey) { case MONACO_HOT_KEY_ACTIONS.sendQuery: { if (lastUsedQueryAction === QUERY_ACTIONS.explain) { - handleGetExplainQueryClick(querySettings); + handleGetExplainQueryClick(); } else { - handleSendExecuteClick(querySettings); + handleSendExecuteClick(); } break; } @@ -318,7 +304,7 @@ function QueryEditor(props: QueryEditorProps) { endLineNumber: selection.getPosition().lineNumber, endColumn: selection.getPosition().column, }); - handleSendExecuteClick(querySettings, text); + handleSendExecuteClick(text); } break; } @@ -417,8 +403,6 @@ function QueryEditor(props: QueryEditorProps) { onExplainButtonClick={handleGetExplainQueryClick} explainIsLoading={explainQueryResult.isLoading} disabled={!executeQuery.input} - onUpdateQueryMode={(queryMode) => setQuerySettings({...querySettings, queryMode})} - querySettings={querySettings} highlightedAction={lastUsedQueryAction} /> ); diff --git a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx index 42f2382d6..1105c1f4f 100644 --- a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx +++ b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx @@ -1,49 +1,18 @@ -import React from 'react'; - -import {ChevronDown, Gear, PlayFill} from '@gravity-ui/icons'; +import {Gear, PlayFill} from '@gravity-ui/icons'; import type {ButtonView} from '@gravity-ui/uikit'; -import {Button, DropdownMenu, Icon, Tooltip} from '@gravity-ui/uikit'; +import {Button, Icon, Tooltip} from '@gravity-ui/uikit'; -import {LabelWithPopover} from '../../../../components/LabelWithPopover'; import QuerySettingsDescription from '../../../../components/QuerySettingsDescription/QuerySettingsDescription'; -import {QUERY_SETTINGS, useSetting} from '../../../../lib'; -import type {QueryAction, QueryMode, QuerySettings} from '../../../../types/store/query'; +import type {QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings'; -import {QUERY_MODES, QUERY_MODES_TITLES} from '../../../../utils/query'; import {SaveQuery} from '../SaveQuery/SaveQuery'; import i18n from '../i18n'; import './QueryEditorControls.scss'; -const queryModeSelectorQa = 'query-mode-selector'; -const queryModeSelectorPopupQa = 'query-mode-selector-popup'; - const b = cn('ydb-query-editor-controls'); -const QueryModeSelectorOptions = { - [QUERY_MODES.script]: { - title: QUERY_MODES_TITLES[QUERY_MODES.script], - description: i18n('method-description.script'), - }, - [QUERY_MODES.scan]: { - title: QUERY_MODES_TITLES[QUERY_MODES.scan], - description: i18n('method-description.scan'), - }, - [QUERY_MODES.data]: { - title: QUERY_MODES_TITLES[QUERY_MODES.data], - description: i18n('method-description.data'), - }, - [QUERY_MODES.query]: { - title: QUERY_MODES_TITLES[QUERY_MODES.query], - description: i18n('method-description.query'), - }, - [QUERY_MODES.pg]: { - title: QUERY_MODES_TITLES[QUERY_MODES.pg], - description: i18n('method-description.pg'), - }, -} as const; - interface SettingsButtonProps { onClick: () => void; runIsLoading: boolean; @@ -85,59 +54,33 @@ const SettingsButton = ({onClick, runIsLoading}: SettingsButtonProps) => { }; interface QueryEditorControlsProps { - onRunButtonClick: (querySettings: QuerySettings) => void; + onRunButtonClick: () => void; onSettingsButtonClick: () => void; runIsLoading: boolean; - onExplainButtonClick: (querySettings: QuerySettings) => void; + onExplainButtonClick: () => void; explainIsLoading: boolean; disabled: boolean; - onUpdateQueryMode: (mode: QueryMode) => void; - querySettings: QuerySettings; highlightedAction: QueryAction; } export const QueryEditorControls = ({ onRunButtonClick, onSettingsButtonClick, - onUpdateQueryMode, runIsLoading, onExplainButtonClick, explainIsLoading, disabled, - querySettings, highlightedAction, }: QueryEditorControlsProps) => { - const [useQuerySettings] = useSetting(QUERY_SETTINGS); - const runView: ButtonView | undefined = highlightedAction === 'execute' ? 'action' : undefined; const explainView: ButtonView | undefined = highlightedAction === 'explain' ? 'action' : undefined; - const querySelectorMenuItems = React.useMemo(() => { - return Object.entries(QueryModeSelectorOptions).map(([mode, {title, description}]) => { - return { - text: ( - - ), - action: () => { - onUpdateQueryMode(mode as QueryMode); - }, - }; - }); - }, [onUpdateQueryMode]); - return (
- {useQuerySettings ? ( - - ) : ( -
- - - {`${i18n('controls.query-mode-selector_type')} ${ - QueryModeSelectorOptions[querySettings.queryMode].title - }`} - - - - } - /> -
- )} +
diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx index f7f61a12a..66a0b575b 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {Dialog, Link as ExternalLink, Flex, TextInput} from '@gravity-ui/uikit'; import {Controller, useForm} from 'react-hook-form'; +import {ENABLE_TRACING_LEVEL_KEY} from '../../../../lib'; import { selectQueryAction, setQueryAction, @@ -11,6 +12,7 @@ import type {QuerySettings} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { useQueryExecutionSettings, + useSetting, useTypedDispatch, useTypedSelector, } from '../../../../utils/hooks'; @@ -69,6 +71,8 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm defaultValues: initialValues, }); + const [enableTracingLevel] = useSetting(ENABLE_TRACING_LEVEL_KEY); + return (
@@ -76,7 +80,7 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm -
+
- - -
- ( - - )} - /> -
-
+ {enableTracingLevel && ( + + +
+ ( + + )} + /> +
+
+ )} -
+
(
diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index f2446c8a8..48034eeed 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -39,8 +39,8 @@ "settings.showDomainDatabase.title": "Show domain database", - "settings.useQuerySettings.title": "Use query settings", - "settings.useQuerySettings.description": "Use query settings", + "settings.enableTracingLevel.title": "Enable tracing level select", + "settings.enableTracingLevel.description": "Caution: Enabling this setting may break running of queries", "settings.queryUseMultiSchema.title": "Allow queries with multiple result sets", "settings.queryUseMultiSchema.description": "Use 'multi' schema for queries. It enables queries with multiple result sets. It returns nothing on versions 23-3 and older", diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index 217dca2f5..90dc2220c 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -6,9 +6,9 @@ import { AUTOCOMPLETE_ON_ENTER, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, ENABLE_AUTOCOMPLETE, + ENABLE_TRACING_LEVEL_KEY, INVERTED_DISKS_KEY, LANGUAGE_KEY, - QUERY_SETTINGS, QUERY_USE_MULTI_SCHEMA_KEY, SHOW_DOMAIN_DATABASE_KEY, THEME_KEY, @@ -113,10 +113,10 @@ export const showDomainDatabase: SettingProps = { title: i18n('settings.showDomainDatabase.title'), }; -export const useQuerySettings: SettingProps = { - settingKey: QUERY_SETTINGS, - title: i18n('settings.useQuerySettings.title'), - description: i18n('settings.useQuerySettings.description'), +export const enableTracingLevel: SettingProps = { + settingKey: ENABLE_TRACING_LEVEL_KEY, + title: i18n('settings.enableTracingLevel.title'), + description: i18n('settings.enableTracingLevel.description'), }; export const queryUseMultiSchemaSetting: SettingProps = { @@ -167,7 +167,7 @@ export const experimentsSection: SettingsSection = { export const devSettingsSection: SettingsSection = { id: 'devSettingsSection', title: i18n('section.dev-setting'), - settings: [enableAutocompleteSetting, autocompleteOnEnterSetting], + settings: [enableAutocompleteSetting, autocompleteOnEnterSetting, enableTracingLevel], }; export const aboutSettingsSection: SettingsSection = { diff --git a/src/services/settings.ts b/src/services/settings.ts index 9e77194e3..3191c8e50 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -6,6 +6,7 @@ import { BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, DEFAULT_QUERY_SETTINGS, ENABLE_AUTOCOMPLETE, + ENABLE_TRACING_LEVEL_KEY, INVERTED_DISKS_KEY, IS_HOTKEYS_HELP_HIDDEN_KEY, LANGUAGE_KEY, @@ -13,7 +14,6 @@ import { LAST_USED_QUERY_ACTION_KEY, PARTITIONS_HIDDEN_COLUMNS_KEY, QUERY_EXECUTION_SETTINGS_KEY, - QUERY_SETTINGS, QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY, QUERY_USE_MULTI_SCHEMA_KEY, SAVED_QUERIES_KEY, @@ -48,7 +48,7 @@ export const DEFAULT_USER_SETTINGS = { [AUTOCOMPLETE_ON_ENTER]: true, [IS_HOTKEYS_HELP_HIDDEN_KEY]: false, [AUTO_REFRESH_INTERVAL]: 0, - [QUERY_SETTINGS]: false, + [ENABLE_TRACING_LEVEL_KEY]: false, [SHOW_DOMAIN_DATABASE_KEY]: false, [LAST_QUERY_EXECUTION_SETTINGS_KEY]: undefined, [QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY]: undefined, diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index 72458adbc..6809e7f61 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -11,18 +11,12 @@ import type { } from '../../types/store/executeQuery'; import type { IQueryResult, - QueryMode, QueryRequestParams, QuerySettings, QuerySyntax, } from '../../types/store/query'; import {QUERIES_HISTORY_KEY} from '../../utils/constants'; -import { - QUERY_MODES, - QUERY_SYNTAX, - isQueryErrorResponse, - parseQueryAPIExecuteResponse, -} from '../../utils/query'; +import {QUERY_SYNTAX, isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query'; import {isNumeric} from '../../utils/utils'; import {createRequestActionTypes} from '../utils'; @@ -72,12 +66,9 @@ const executeQuery: Reducer = ( } case SAVE_QUERY_TO_HISTORY: { - const queryText = action.data.queryText; + const queryText = action.data; - // Do not save explicit yql syntax value for easier further support (use yql by default) - const syntax = action.data.mode === QUERY_MODES.pg ? QUERY_SYNTAX.pg : undefined; - - const newQueries = [...state.history.queries, {queryText, syntax}].slice( + const newQueries = [...state.history.queries, {queryText}].slice( state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0, ); settingsManager.setUserSettingsValue(QUERIES_HISTORY_KEY, newQueries); @@ -144,12 +135,21 @@ const executeQuery: Reducer = ( interface SendQueryParams extends QueryRequestParams { querySettings?: Partial; schema?: Schemas; + // flag whether to send new tracing header or not + // default: not send + enableTracingLevel?: boolean; } export const executeQueryApi = api.injectEndpoints({ endpoints: (build) => ({ executeQuery: build.mutation({ - queryFn: async ({query, database, querySettings = {}, schema = 'modern'}) => { + queryFn: async ({ + query, + database, + querySettings = {}, + schema = 'modern', + enableTracingLevel, + }) => { let action: ExecuteActions = 'execute'; let syntax: QuerySyntax = QUERY_SYNTAX.yql; @@ -168,9 +168,10 @@ export const executeQueryApi = api.injectEndpoints({ action, syntax, stats: querySettings.statisticsMode, - tracingLevel: querySettings.tracingLevel - ? TracingLevelNumber[querySettings.tracingLevel] - : undefined, + tracingLevel: + querySettings.tracingLevel && enableTracingLevel + ? TracingLevelNumber[querySettings.tracingLevel] + : undefined, transaction_mode: querySettings.isolationLevel, timeout: isNumeric(querySettings.timeout) ? Number(querySettings.timeout) * 1000 @@ -192,10 +193,10 @@ export const executeQueryApi = api.injectEndpoints({ overrideExisting: 'throw', }); -export const saveQueryToHistory = (queryText: string, mode: QueryMode) => { +export const saveQueryToHistory = (queryText: string) => { return { type: SAVE_QUERY_TO_HISTORY, - data: {queryText, mode}, + data: queryText, } as const; }; diff --git a/src/store/reducers/explainQuery/explainQuery.ts b/src/store/reducers/explainQuery/explainQuery.ts index 3078122b1..1717b0648 100644 --- a/src/store/reducers/explainQuery/explainQuery.ts +++ b/src/store/reducers/explainQuery/explainQuery.ts @@ -10,12 +10,15 @@ import {prepareExplainResponse} from './utils'; interface ExplainQueryParams extends QueryRequestParams { querySettings?: Partial; + // flag whether to send new tracing header or not + // default: not send + enableTracingLevel?: boolean; } export const explainQueryApi = api.injectEndpoints({ endpoints: (build) => ({ explainQuery: build.mutation({ - queryFn: async ({query, database, querySettings}) => { + queryFn: async ({query, database, querySettings, enableTracingLevel}) => { let action: ExplainActions = 'explain'; let syntax: QuerySyntax = QUERY_SYNTAX.yql; @@ -33,9 +36,10 @@ export const explainQueryApi = api.injectEndpoints({ action, syntax, stats: querySettings?.statisticsMode, - tracingLevel: querySettings?.tracingLevel - ? TracingLevelNumber[querySettings?.tracingLevel] - : undefined, + tracingLevel: + querySettings?.tracingLevel && enableTracingLevel + ? TracingLevelNumber[querySettings?.tracingLevel] + : undefined, transaction_mode: querySettings?.isolationLevel, timeout: isNumeric(querySettings?.timeout) ? Number(querySettings?.timeout) * 1000 diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 0f9b3a33d..ef4812949 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -159,4 +159,4 @@ export const AUTOCOMPLETE_ON_ENTER = 'autocompleteOnEnter'; export const IS_HOTKEYS_HELP_HIDDEN_KEY = 'isHotKeysHelpHidden'; -export const QUERY_SETTINGS = 'query_settings'; +export const ENABLE_TRACING_LEVEL_KEY = 'enableTracingLevel'; diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts index 107b529c5..5bba35824 100644 --- a/src/utils/hooks/useQueryExecutionSettings.ts +++ b/src/utils/hooks/useQueryExecutionSettings.ts @@ -1,8 +1,23 @@ import type {QuerySettings} from '../../types/store/query'; -import {QUERY_EXECUTION_SETTINGS_KEY} from '../constants'; +import { + DEFAULT_QUERY_SETTINGS, + ENABLE_TRACING_LEVEL_KEY, + QUERY_EXECUTION_SETTINGS_KEY, +} from '../constants'; import {useSetting} from './useSetting'; export const useQueryExecutionSettings = () => { - return useSetting(QUERY_EXECUTION_SETTINGS_KEY); + const [enableTracingLevel] = useSetting(ENABLE_TRACING_LEVEL_KEY); + const [setting, setSetting] = useSetting(QUERY_EXECUTION_SETTINGS_KEY); + + return [ + { + ...setting, + tracingLevel: enableTracingLevel + ? setting.tracingLevel + : DEFAULT_QUERY_SETTINGS.tracingLevel, + }, + setSetting, + ] as const; }; diff --git a/tests/suites/tenant/queryEditor/QueryEditor.ts b/tests/suites/tenant/queryEditor/QueryEditor.ts index 16f222eb3..318fde7a9 100644 --- a/tests/suites/tenant/queryEditor/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/QueryEditor.ts @@ -1,83 +1,236 @@ import type {Locator, Page} from '@playwright/test'; -import {BaseModel} from '../../../models/BaseModel'; -import {selectContentTable} from '../../../utils/selectContentTable'; +export const VISIBILITY_TIMEOUT = 5000; -type QueryMode = 'YQL Script' | 'Scan'; -type ExplainResultType = 'Schema' | 'Simplified' | 'JSON' | 'AST'; +export enum QueryMode { + YQLScript = 'YQL Script', + Scan = 'Scan', +} + +export enum ExplainResultType { + Schema = 'Schema', + JSON = 'JSON', + AST = 'AST', +} -const queryModeSelectorQa = 'query-mode-selector'; -const queryModeSelectorPopupQa = 'query-mode-selector-popup'; +export enum ButtonNames { + Run = 'Run', + Explain = 'Explain', + Cancel = 'Cancel', + Save = 'Save', +} -export class QueryEditor extends BaseModel { - protected readonly editorTextArea: Locator; - protected readonly runButton: Locator; - protected readonly explainButton: Locator; +export class SettingsDialog { + private dialog: Locator; + private page: Page; constructor(page: Page) { - super(page, page.locator('.query-editor')); + this.page = page; + this.dialog = page.locator('.ydb-query-settings-dialog'); + } - this.editorTextArea = this.selector.locator('.query-editor__monaco textarea'); + async changeQueryMode(mode: QueryMode) { + const dropdown = this.dialog.locator( + '.ydb-query-settings-dialog__control-wrapper_queryMode', + ); + await dropdown.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await dropdown.click(); + const popup = this.page.locator('.ydb-query-settings-select__popup'); + await popup.getByText(mode).first().click(); + await this.page.waitForTimeout(1000); + } + + async changeIsolationLevel(level: string) { + const dropdown = this.dialog.locator( + '.ydb-query-settings-dialog__control-wrapper_isolationLevel', + ); + await dropdown.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await dropdown.click(); + const popup = this.page.locator('.ydb-query-settings-select__popup'); + await popup.getByText(level).first().click(); + await this.page.waitForTimeout(1000); + } + + async clickButton(buttonName: ButtonNames) { + const button = this.dialog.getByRole('button', {name: buttonName}); + await button.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await button.click(); + } + + async isVisible() { + await this.dialog.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async isHidden() { + await this.dialog.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT}); + return true; + } +} - this.runButton = this.selector.getByRole('button', {name: /Run/}); - this.explainButton = this.selector.getByRole('button', {name: /Explain/}); +export class ResultTable { + private table: Locator; + + constructor(selector: Locator) { + this.table = selector.locator('.ydb-query-execute-result__result'); + } + + async isVisible() { + await this.table.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async isHidden() { + await this.table.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async getRowCount() { + const rows = this.table.locator('tr'); + return rows.count(); + } + + async getCellValue(row: number, col: number) { + const cell = this.table.locator(`tr:nth-child(${row}) td:nth-child(${col})`); + return cell.innerText(); + } +} + +export class QueryEditor { + settingsDialog: SettingsDialog; + resultTable: ResultTable; + + private page: Page; + private selector: Locator; + private editorTextArea: Locator; + private runButton: Locator; + private explainButton: Locator; + private gearButton: Locator; + private indicatorIcon: Locator; + private banner: Locator; + + constructor(page: Page) { + this.page = page; + this.selector = page.locator('.query-editor'); + this.editorTextArea = this.selector.locator('.query-editor__monaco textarea'); + this.runButton = this.selector.getByRole('button', {name: ButtonNames.Run}); + this.explainButton = this.selector.getByRole('button', {name: ButtonNames.Explain}); + this.gearButton = this.selector.locator('.ydb-query-editor-controls__gear-button'); + this.indicatorIcon = this.selector.locator( + '.kv-query-execution-status__query-settings-icon', + ); + this.banner = this.page.locator('.ydb-query-settings-banner'); + + this.settingsDialog = new SettingsDialog(page); + this.resultTable = new ResultTable(this.selector); } + async run(query: string, mode: QueryMode) { - await this.selectQueryMode(mode); - await this.editorTextArea.fill(query); - await this.runButton.click(); + await this.clickGearButton(); + await this.settingsDialog.changeQueryMode(mode); + await this.settingsDialog.clickButton(ButtonNames.Save); + await this.setQuery(query); + await this.clickRunButton(); } async explain(query: string, mode: QueryMode) { - await this.selectQueryMode(mode); - await this.editorTextArea.fill(query); + await this.clickGearButton(); + await this.settingsDialog.changeQueryMode(mode); + await this.settingsDialog.clickButton(ButtonNames.Save); + await this.setQuery(query); + await this.clickExplainButton(); + } + + async gearButtonText() { + await this.gearButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return this.gearButton.innerText(); + } + + async clickRunButton() { + await this.runButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await this.runButton.click(); + } + + async clickExplainButton() { + await this.explainButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); await this.explainButton.click(); } - // eslint-disable-next-line consistent-return async getExplainResult(type: ExplainResultType) { await this.selectExplainResultType(type); - const resultArea = this.selector.locator('.ydb-query-explain-result__result'); - switch (type) { - case 'Schema': { + case ExplainResultType.Schema: return resultArea.locator('.canvas-container'); - } - case 'Simplified': { - return resultArea.locator('.ydb-query-explain-simplified-plan'); - } - case 'JSON': { + case ExplainResultType.JSON: return resultArea.locator('.json-inspector'); - } - case 'AST': { + case ExplainResultType.AST: return resultArea.locator('.ydb-query-explain-ast'); - } } } - getRunResultTable() { - const runResult = this.selector.locator('.ydb-query-execute-result__result'); + async clickGearButton() { + await this.gearButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await this.gearButton.click(); + } + + async closeBanner() { + const closeButton = this.banner.locator('button'); + await closeButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await closeButton.click(); + } - // Result table is present only on successful not empty results - const resultTable = selectContentTable(runResult); + async hoverGearButton() { + await this.gearButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await this.gearButton.hover(); + } - return resultTable; + async setQuery(query: string) { + await this.editorTextArea.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await this.editorTextArea.fill(query); } - protected async selectExplainResultType(type: ExplainResultType) { + async selectExplainResultType(type: ExplainResultType) { const radio = this.selector.locator('.ydb-query-explain-result__controls .g-radio-button'); - await radio.getByLabel(type).click(); + const typeButton = radio.getByLabel(type); + await typeButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await typeButton.click(); } - protected async selectQueryMode(type: QueryMode) { - const queryModeSelector = this.selector.getByTestId(queryModeSelectorQa); + async isRunButtonEnabled() { + return this.runButton.isEnabled({timeout: VISIBILITY_TIMEOUT}); + } - await queryModeSelector.click(); + async isExplainButtonEnabled() { + return this.explainButton.isEnabled({timeout: VISIBILITY_TIMEOUT}); + } - const queryModeSelectorPopup = this.page.getByTestId(queryModeSelectorPopupQa); + async isBannerVisible() { + await this.banner.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async isBannerHidden() { + await this.banner.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT}); + return true; + } - await queryModeSelectorPopup.waitFor({state: 'visible'}); - await queryModeSelectorPopup.getByText(type).click(); + async isIndicatorIconVisible() { + await this.indicatorIcon.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async retry(action: () => Promise, maxAttempts = 3, delay = 1000): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await action(); + } catch (error) { + if (attempt === maxAttempts) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + throw new Error('Max attempts reached'); } } diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts index e3d4d259a..525c89abd 100644 --- a/tests/suites/tenant/queryEditor/queryEditor.test.ts +++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts @@ -3,7 +3,13 @@ import {expect, test} from '@playwright/test'; import {tenantName} from '../../../utils/constants'; import {TenantPage} from '../TenantPage'; -import {QueryEditor} from './QueryEditor'; +import { + ButtonNames, + ExplainResultType, + QueryEditor, + QueryMode, + VISIBILITY_TIMEOUT, +} from './QueryEditor'; test.describe('Test Query Editor', async () => { const testQuery = 'SELECT 1, 2, 3, 4, 5;'; @@ -19,42 +25,134 @@ test.describe('Test Query Editor', async () => { await tenantPage.goto(pageQueryParams); }); - test('Can run scipt', async ({page}) => { + test('Settings dialog opens on Gear click and closes on Cancel', async ({page}) => { const queryEditor = new QueryEditor(page); - await queryEditor.run(testQuery, 'YQL Script'); + await queryEditor.clickGearButton(); - await expect(queryEditor.getRunResultTable()).toBeVisible(); + await expect(queryEditor.settingsDialog.isVisible()).resolves.toBe(true); + + await queryEditor.settingsDialog.clickButton(ButtonNames.Cancel); + await expect(queryEditor.settingsDialog.isHidden()).resolves.toBe(true); + }); + + test('Settings dialog saves changes and updates Gear button', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await queryEditor.settingsDialog.changeQueryMode(QueryMode.Scan); + await queryEditor.settingsDialog.clickButton(ButtonNames.Save); + + await expect(async () => { + const text = await queryEditor.gearButtonText(); + expect(text).toContain('(1)'); + }).toPass({timeout: VISIBILITY_TIMEOUT}); + }); + + test('Run button executes YQL script', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.run(testQuery, QueryMode.YQLScript); + + await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true); }); - test('Can run scan', async ({page}) => { + test('Run button executes Scan', async ({page}) => { const queryEditor = new QueryEditor(page); - await queryEditor.run(testQuery, 'Scan'); + await queryEditor.run(testQuery, QueryMode.Scan); - await expect(queryEditor.getRunResultTable()).toBeVisible(); + await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true); }); - test('Can get explain script', async ({page}) => { + test('Explain button executes YQL script explanation', async ({page}) => { const queryEditor = new QueryEditor(page); - await queryEditor.explain(testQuery, 'YQL Script'); + await queryEditor.explain(testQuery, QueryMode.YQLScript); - const explainSchema = await queryEditor.getExplainResult('Schema'); - await expect(explainSchema).toBeVisible(); + const explainSchema = await queryEditor.getExplainResult(ExplainResultType.Schema); + await expect(explainSchema).toBeVisible({timeout: VISIBILITY_TIMEOUT}); - const explainJSON = await queryEditor.getExplainResult('JSON'); - await expect(explainJSON).toBeVisible(); + const explainJSON = await queryEditor.getExplainResult(ExplainResultType.JSON); + await expect(explainJSON).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); - test('Can get explain scan', async ({page}) => { + test('Explain button executes Scan explanation', async ({page}) => { const queryEditor = new QueryEditor(page); - await queryEditor.explain(testQuery, 'Scan'); + await queryEditor.explain(testQuery, QueryMode.Scan); + + const explainSchema = await queryEditor.getExplainResult(ExplainResultType.Schema); + await expect(explainSchema).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + + const explainJSON = await queryEditor.getExplainResult(ExplainResultType.JSON); + await expect(explainJSON).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + + const explainAST = await queryEditor.getExplainResult(ExplainResultType.AST); + await expect(explainAST).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + }); + + test('Banner appears after executing script with changed settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Change a setting + await queryEditor.clickGearButton(); + await queryEditor.settingsDialog.changeQueryMode(QueryMode.Scan); + await queryEditor.settingsDialog.clickButton(ButtonNames.Save); + + // Execute a script + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + // Check if banner appears + await expect(queryEditor.isBannerVisible()).resolves.toBe(true); + }); - const explainSchema = await queryEditor.getExplainResult('Schema'); - await expect(explainSchema).toBeVisible(); + test('Indicator icon appears after closing banner', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Change a setting + await queryEditor.clickGearButton(); + await queryEditor.settingsDialog.changeQueryMode(QueryMode.Scan); + await queryEditor.settingsDialog.clickButton(ButtonNames.Save); + + // Execute a script to make the banner appear + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + // Close the banner + await queryEditor.closeBanner(); + + await expect(queryEditor.isIndicatorIconVisible()).resolves.toBe(true); + }); + + test('Gear button shows number of changed settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await queryEditor.settingsDialog.changeQueryMode(QueryMode.Scan); + await queryEditor.settingsDialog.changeIsolationLevel('Snapshot'); + await queryEditor.settingsDialog.clickButton(ButtonNames.Save); + + await expect(async () => { + const text = await queryEditor.gearButtonText(); + expect(text).toContain('(2)'); + }).toPass({timeout: VISIBILITY_TIMEOUT}); + }); + + test('Run and Explain buttons are disabled when query is empty', async ({page}) => { + const queryEditor = new QueryEditor(page); + + await expect(queryEditor.isRunButtonEnabled()).resolves.toBe(false); + await expect(queryEditor.isExplainButtonEnabled()).resolves.toBe(false); + + await queryEditor.setQuery(testQuery); + + await expect(queryEditor.isRunButtonEnabled()).resolves.toBe(true); + await expect(queryEditor.isExplainButtonEnabled()).resolves.toBe(true); + }); + + test('Banner does not appear when executing script with default settings', async ({page}) => { + const queryEditor = new QueryEditor(page); - const explainJSON = await queryEditor.getExplainResult('JSON'); - await expect(explainJSON).toBeVisible(); + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); - const explainAST = await queryEditor.getExplainResult('AST'); - await expect(explainAST).toBeVisible(); + await expect(queryEditor.isBannerHidden()).resolves.toBe(true); }); });