From 439c91fd7a56ad5ad3e0f51818ab5c58da29ef8d Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 13:52:54 +0300 Subject: [PATCH 01/12] feat: release query settings dialog --- .github/workflows/ci.yml | 93 +++++++++++- playwright.config.ts | 9 ++ .../QueryExecutionStatus.tsx | 4 +- .../Query/ExecuteResult/ExecuteResult.tsx | 6 +- .../Query/ExplainResult/ExplainResult.tsx | 7 +- .../Query/QueriesHistory/QueriesHistory.tsx | 25 +-- .../Tenant/Query/QueryEditor/QueryEditor.tsx | 104 ++++++------- .../QueryEditorControls.tsx | 100 +----------- .../QuerySettingsDialog.tsx | 52 ++++--- src/containers/UserSettings/i18n/en.json | 4 +- src/containers/UserSettings/settings.tsx | 12 +- src/services/settings.ts | 6 +- src/store/reducers/executeQuery.ts | 37 ++--- .../reducers/explainQuery/explainQuery.ts | 12 +- src/utils/constants.ts | 6 +- src/utils/hooks/useQueryExecutionSettings.ts | 19 ++- .../suites/tenant/queryEditor/QueryEditor.ts | 143 ++++++++++++++---- .../tenant/queryEditor/queryEditor.test.ts | 96 +++++++++++- 18 files changed, 453 insertions(+), 282 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7152493b..ceb4bede8 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: @@ -58,8 +62,95 @@ 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 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: 30 + + - 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: | + TOTAL=$(grep -oP 'total="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + PASSED=$(grep -oP 'passed="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + FAILED=$(grep -oP 'failed="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + SKIPPED=$(grep -oP 'skipped="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + echo "total=$TOTAL" >> $GITHUB_OUTPUT + echo "passed=$PASSED" >> $GITHUB_OUTPUT + echo "failed=$FAILED" >> $GITHUB_OUTPUT + echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT + + - name: Comment PR + 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 }}, + passed: ${{ steps.test-results.outputs.passed }}, + failed: ${{ steps.test-results.outputs.failed }}, + skipped: ${{ steps.test-results.outputs.skipped }} + }; + const status = testResults.failed > 0 ? '❌ FAILED' : '✅ PASSED'; + const statusColor = testResults.failed > 0 ? 'red' : 'green'; + + const comment = `## 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. + +
`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }) diff --git a/playwright.config.ts b/playwright.config.ts index f47feb143..0e6d9fb86 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: process.env.PLAYWRIGHT_VIDEO === 'on' ? 'on' : 'off', + 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/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..ecbad65de 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, LAST_USED_QUERY_ACTION_KEY, - QUERY_SETTINGS, QUERY_USE_MULTI_SCHEMA_KEY, + TRACING_LEVEL_VERBOSITY_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 [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_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, + tracingLevelVerbosity, }); 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, + tracingLevelVerbosity, 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, - ], - ); + tracingLevelVerbosity, + }); + setIsResultLoaded(true); + setShowPreview(false); + dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); + }, [ + executeQuery, + lastQueryExecutionSettings, + querySettings, + resetBanner, + tracingLevelVerbosity, + 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..6a39a1877 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 {TRACING_LEVEL_VERBOSITY_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 [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); + return (
@@ -76,7 +80,7 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm -
+
- - -
- ( - - )} - /> -
-
+ {tracingLevelVerbosity && ( + + +
+ ( + + )} + /> +
+
+ )} -
+
(
diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index f2446c8a8..0e1c5b981 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.tracingLevelVerbosity.title": "Enable tracing level select", + "settings.tracingLevelVerbosity.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..f734e64d7 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -8,10 +8,10 @@ import { ENABLE_AUTOCOMPLETE, INVERTED_DISKS_KEY, LANGUAGE_KEY, - QUERY_SETTINGS, QUERY_USE_MULTI_SCHEMA_KEY, SHOW_DOMAIN_DATABASE_KEY, THEME_KEY, + TRACING_LEVEL_VERBOSITY_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, USE_PAGINATED_TABLES_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 tracingLevelVerbosity: SettingProps = { + settingKey: TRACING_LEVEL_VERBOSITY_KEY, + title: i18n('settings.tracingLevelVerbosity.title'), + description: i18n('settings.tracingLevelVerbosity.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, tracingLevelVerbosity], }; export const aboutSettingsSection: SettingsSection = { diff --git a/src/services/settings.ts b/src/services/settings.ts index 9e77194e3..396dbbd2a 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -13,14 +13,15 @@ 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, SHOW_DOMAIN_DATABASE_KEY, TENANT_INITIAL_PAGE_KEY, THEME_KEY, + TRACING_LEVEL_VERBOSITY_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, + USE_DIRECTORY_OPERATIONS, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, USE_PAGINATED_TABLES_KEY, } from '../utils/constants'; @@ -48,7 +49,8 @@ export const DEFAULT_USER_SETTINGS = { [AUTOCOMPLETE_ON_ENTER]: true, [IS_HOTKEYS_HELP_HIDDEN_KEY]: false, [AUTO_REFRESH_INTERVAL]: 0, - [QUERY_SETTINGS]: false, + [USE_DIRECTORY_OPERATIONS]: false, + [TRACING_LEVEL_VERBOSITY_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..8bbf22dba 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 + tracingLevelVerbosity?: 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', + tracingLevelVerbosity, + }) => { 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 && tracingLevelVerbosity + ? 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..e3a4088c1 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 + tracingLevelVerbosity?: boolean; } export const explainQueryApi = api.injectEndpoints({ endpoints: (build) => ({ explainQuery: build.mutation({ - queryFn: async ({query, database, querySettings}) => { + queryFn: async ({query, database, querySettings, tracingLevelVerbosity}) => { 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 && tracingLevelVerbosity + ? 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..4a1d97964 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -159,4 +159,8 @@ export const AUTOCOMPLETE_ON_ENTER = 'autocompleteOnEnter'; export const IS_HOTKEYS_HELP_HIDDEN_KEY = 'isHotKeysHelpHidden'; -export const QUERY_SETTINGS = 'query_settings'; +export const USE_SEPARATE_DISKS_PAGES_KEY = 'useSeparateDisksPages'; + +export const USE_DIRECTORY_OPERATIONS = 'useDirectoryOperations'; + +export const TRACING_LEVEL_VERBOSITY_KEY = 'tracingLevelVerbosity'; diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts index 107b529c5..48aea6103 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, + QUERY_EXECUTION_SETTINGS_KEY, + TRACING_LEVEL_VERBOSITY_KEY, +} from '../constants'; import {useSetting} from './useSetting'; export const useQueryExecutionSettings = () => { - return useSetting(QUERY_EXECUTION_SETTINGS_KEY); + const [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); + const [setting, setSetting] = useSetting(QUERY_EXECUTION_SETTINGS_KEY); + + return [ + { + ...setting, + tracingLevel: tracingLevelVerbosity + ? 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..1ed6a0aa9 100644 --- a/tests/suites/tenant/queryEditor/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/QueryEditor.ts @@ -4,80 +4,161 @@ import {BaseModel} from '../../../models/BaseModel'; import {selectContentTable} from '../../../utils/selectContentTable'; type QueryMode = 'YQL Script' | 'Scan'; -type ExplainResultType = 'Schema' | 'Simplified' | 'JSON' | 'AST'; - -const queryModeSelectorQa = 'query-mode-selector'; -const queryModeSelectorPopupQa = 'query-mode-selector-popup'; +type ExplainResultType = 'Schema' | 'JSON' | 'AST'; export class QueryEditor extends BaseModel { protected readonly editorTextArea: Locator; - protected readonly runButton: Locator; - protected readonly explainButton: Locator; + protected runButton: Locator; + protected explainButton: Locator; + protected readonly gearButton: Locator; + protected readonly indicatorIcon: Locator; + protected readonly settingsDialog: Locator; + protected readonly banner: Locator; constructor(page: Page) { super(page, page.locator('.query-editor')); this.editorTextArea = this.selector.locator('.query-editor__monaco textarea'); - this.runButton = this.selector.getByRole('button', {name: /Run/}); this.explainButton = this.selector.getByRole('button', {name: /Explain/}); + this.gearButton = this.selector.locator('.ydb-query-editor-controls__gear-button'); + this.indicatorIcon = this.selector.locator( + '.ydb-query-editor-controls__query-settings-icon', + ); + this.settingsDialog = this.page.locator('.ydb-query-settings-dialog'); + this.banner = this.page.locator('.ydb-query-settings-banner'); } + async run(query: string, mode: QueryMode) { - await this.selectQueryMode(mode); + await this.clickGearButton(); + await this.changeQueryMode(mode); + await this.clickSaveInSettingsDialog(); await this.editorTextArea.fill(query); await this.runButton.click(); } async explain(query: string, mode: QueryMode) { - await this.selectQueryMode(mode); + await this.clickGearButton(); + await this.changeQueryMode(mode); + await this.clickSaveInSettingsDialog(); await this.editorTextArea.fill(query); await this.explainButton.click(); } - // eslint-disable-next-line consistent-return + async gearButtonContainsText(text: string) { + return this.gearButton.locator(`text=${text}`).isVisible(); + } + + async isBannerVisible() { + return this.banner.isVisible(); + } + + async isBannerHidden() { + return this.banner.isHidden(); + } + + async isRunButtonEnabled() { + return this.runButton.isEnabled(); + } + + async isRunButtonDisabled() { + return this.runButton.isDisabled(); + } + + async isExplainButtonEnabled() { + return this.explainButton.isEnabled(); + } + + async isExplainButtonDisabled() { + return this.explainButton.isDisabled(); + } + + async clickRunButton() { + await this.runButton.click(); + } + + async clickExplainButton() { + await this.explainButton.click(); + } + async getExplainResult(type: ExplainResultType) { await this.selectExplainResultType(type); const resultArea = this.selector.locator('.ydb-query-explain-result__result'); switch (type) { - case 'Schema': { + case 'Schema': return resultArea.locator('.canvas-container'); - } - case 'Simplified': { - return resultArea.locator('.ydb-query-explain-simplified-plan'); - } - case 'JSON': { + case 'JSON': return resultArea.locator('.json-inspector'); - } - case 'AST': { + case 'AST': return resultArea.locator('.ydb-query-explain-ast'); - } } } getRunResultTable() { const runResult = this.selector.locator('.ydb-query-execute-result__result'); + return selectContentTable(runResult); + } - // Result table is present only on successful not empty results - const resultTable = selectContentTable(runResult); + async settingsDialogIsVisible() { + return this.settingsDialog.isVisible(); + } - return resultTable; + async settingsDialogIsNotVisible() { + return this.settingsDialog.isHidden(); } - protected async selectExplainResultType(type: ExplainResultType) { - const radio = this.selector.locator('.ydb-query-explain-result__controls .g-radio-button'); - await radio.getByLabel(type).click(); + async clickGearButton() { + await this.gearButton.click(); + } + + async clickCancelInSettingsDialog() { + await this.settingsDialog.getByRole('button', {name: /Cancel/}).click(); + } + + async clickSaveInSettingsDialog() { + await this.settingsDialog.getByRole('button', {name: /Save/}).click(); + } + + async changeQueryMode(mode: QueryMode) { + const dropdown = this.settingsDialog.locator( + '.ydb-query-settings-dialog__control-wrapper_queryMode', + ); + await dropdown.click(); + const popup = this.page.locator('.ydb-query-settings-select__popup'); + await popup.getByText(mode).first().click(); + await this.page.waitForTimeout(1000); } - protected async selectQueryMode(type: QueryMode) { - const queryModeSelector = this.selector.getByTestId(queryModeSelectorQa); + async changeIsolationLevel(level: string) { + const dropdown = this.settingsDialog.locator( + '.ydb-query-settings-dialog__control-wrapper_isolationLevel', + ); + 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 closeBanner() { + await this.banner.locator('button').click(); + } - await queryModeSelector.click(); + async isIndicatorIconVisible() { + return this.indicatorIcon.isVisible(); + } - const queryModeSelectorPopup = this.page.getByTestId(queryModeSelectorPopupQa); + async hoverGearButton() { + await this.gearButton.hover(); + } - await queryModeSelectorPopup.waitFor({state: 'visible'}); - await queryModeSelectorPopup.getByText(type).click(); + async setQuery(query: string) { + await this.editorTextArea.fill(query); + } + + protected async selectExplainResultType(type: ExplainResultType) { + const radio = this.selector.locator('.ydb-query-explain-result__controls .g-radio-button'); + await radio.getByLabel(type).click(); } } diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts index e3d4d259a..9040dc076 100644 --- a/tests/suites/tenant/queryEditor/queryEditor.test.ts +++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts @@ -19,21 +19,41 @@ 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.clickGearButton(); + + await expect(queryEditor.settingsDialogIsVisible()).toBeTruthy(); + + await queryEditor.clickCancelInSettingsDialog(); + await expect(queryEditor.settingsDialogIsNotVisible()).toBeTruthy(); + }); + + test('Settings dialog saves changes and updates Gear button', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await queryEditor.changeQueryMode('Scan'); + await queryEditor.clickSaveInSettingsDialog(); + + await expect(queryEditor.gearButtonContainsText('(1)')).toBeTruthy(); + }); + + test('Run button executes YQL script', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.run(testQuery, 'YQL Script'); await expect(queryEditor.getRunResultTable()).toBeVisible(); }); - test('Can run scan', async ({page}) => { + test('Run button executes Scan', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.run(testQuery, 'Scan'); await expect(queryEditor.getRunResultTable()).toBeVisible(); }); - 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'); @@ -44,7 +64,7 @@ test.describe('Test Query Editor', async () => { await expect(explainJSON).toBeVisible(); }); - 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'); @@ -57,4 +77,72 @@ test.describe('Test Query Editor', async () => { const explainAST = await queryEditor.getExplainResult('AST'); await expect(explainAST).toBeVisible(); }); + + test('Banner appears after executing script with changed settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Change a setting + await queryEditor.clickGearButton(); + await queryEditor.changeQueryMode('Scan'); + await queryEditor.clickSaveInSettingsDialog(); + + // Execute a script + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + // Check if banner appears + await expect(queryEditor.isBannerVisible()).toBeTruthy(); + }); + + test('Indicator icon appears after closing banner', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Change a setting + await queryEditor.clickGearButton(); + await queryEditor.changeQueryMode('Scan'); + await queryEditor.clickSaveInSettingsDialog(); + + // Execute a script to make the banner appear + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + // Close the banner + await queryEditor.closeBanner(); + + // Check tooltip on hover + await queryEditor.hoverGearButton(); + await expect(queryEditor.isIndicatorIconVisible()).toBeTruthy(); + }); + + test('Gear button shows number of changed settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + await queryEditor.clickGearButton(); + + await queryEditor.changeQueryMode('Scan'); + await queryEditor.changeIsolationLevel('Serializable'); + await queryEditor.clickSaveInSettingsDialog(); + + await expect(queryEditor.gearButtonContainsText('(2)')).toBeTruthy(); + }); + + test('Run and Explain buttons are disabled when query is empty', async ({page}) => { + const queryEditor = new QueryEditor(page); + + await expect(queryEditor.isRunButtonDisabled()).toBeTruthy(); + await expect(queryEditor.isExplainButtonDisabled()).toBeTruthy(); + + await queryEditor.setQuery(testQuery); + + await expect(queryEditor.isRunButtonEnabled()).toBeTruthy(); + await expect(queryEditor.isExplainButtonEnabled()).toBeTruthy(); + }); + + test('Banner does not appear when executing script with default settings', async ({page}) => { + const queryEditor = new QueryEditor(page); + + await queryEditor.setQuery(testQuery); + await queryEditor.clickRunButton(); + + await expect(queryEditor.isBannerHidden()).toBeTruthy(); + }); }); From 415d8eb777cbf8dd3c6dda795d6925ace629f073 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 16:59:44 +0300 Subject: [PATCH 02/12] fix: ci --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceb4bede8..b01485369 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,10 +95,10 @@ jobs: if: always() id: test-results run: | - TOTAL=$(grep -oP 'total="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") - PASSED=$(grep -oP 'passed="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") - FAILED=$(grep -oP 'failed="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") - SKIPPED=$(grep -oP 'skipped="\K[^"]+' playwright-artifacts/playwright-report/index.html || echo "0") + TOTAL=$(grep -c 'class="test-file-test' playwright-artifacts/playwright-report/index.html || echo "0") + PASSED=$(grep -c 'class="test-file-test test-file-test-outcome-expected"' playwright-artifacts/playwright-report/index.html || echo "0") + FAILED=$(grep -c 'class="test-file-test test-file-test-outcome-unexpected"' playwright-artifacts/playwright-report/index.html || echo "0") + SKIPPED=0 # В данной структуре нет явного указания на пропущенные тесты echo "total=$TOTAL" >> $GITHUB_OUTPUT echo "passed=$PASSED" >> $GITHUB_OUTPUT echo "failed=$FAILED" >> $GITHUB_OUTPUT From 2f8a454a11119c5caf9d25582148663d090f0714 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 17:35:27 +0300 Subject: [PATCH 03/12] fix: tests --- playwright.config.ts | 2 +- .../suites/tenant/queryEditor/QueryEditor.ts | 106 +++++++++--------- .../tenant/queryEditor/queryEditor.test.ts | 60 ++++++---- 3 files changed, 89 insertions(+), 79 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 0e6d9fb86..c12f258e8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -23,7 +23,7 @@ const config: PlaywrightTestConfig = { baseURL: baseUrl || 'http://localhost:3000/', testIdAttribute: 'data-qa', trace: 'on-first-retry', - video: process.env.PLAYWRIGHT_VIDEO === 'on' ? 'on' : 'off', + video: 'retain-on-failure', screenshot: 'only-on-failure', }, projects: [ diff --git a/tests/suites/tenant/queryEditor/QueryEditor.ts b/tests/suites/tenant/queryEditor/QueryEditor.ts index 1ed6a0aa9..29615cb7a 100644 --- a/tests/suites/tenant/queryEditor/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/QueryEditor.ts @@ -6,14 +6,16 @@ import {selectContentTable} from '../../../utils/selectContentTable'; type QueryMode = 'YQL Script' | 'Scan'; type ExplainResultType = 'Schema' | 'JSON' | 'AST'; +export const VISIBILITY_TIMEOUT = 5000; + export class QueryEditor extends BaseModel { - protected readonly editorTextArea: Locator; - protected runButton: Locator; - protected explainButton: Locator; - protected readonly gearButton: Locator; - protected readonly indicatorIcon: Locator; - protected readonly settingsDialog: Locator; - protected readonly banner: Locator; + editorTextArea: Locator; + runButton: Locator; + explainButton: Locator; + gearButton: Locator; + indicatorIcon: Locator; + settingsDialog: Locator; + banner: Locator; constructor(page: Page) { super(page, page.locator('.query-editor')); @@ -23,7 +25,7 @@ export class QueryEditor extends BaseModel { this.explainButton = this.selector.getByRole('button', {name: /Explain/}); this.gearButton = this.selector.locator('.ydb-query-editor-controls__gear-button'); this.indicatorIcon = this.selector.locator( - '.ydb-query-editor-controls__query-settings-icon', + '.kv-query-execution-status__query-settings-icon', ); this.settingsDialog = this.page.locator('.ydb-query-settings-dialog'); this.banner = this.page.locator('.ydb-query-settings-banner'); @@ -33,51 +35,30 @@ export class QueryEditor extends BaseModel { await this.clickGearButton(); await this.changeQueryMode(mode); await this.clickSaveInSettingsDialog(); - await this.editorTextArea.fill(query); - await this.runButton.click(); + await this.setQuery(query); + await this.clickRunButton(); } async explain(query: string, mode: QueryMode) { await this.clickGearButton(); await this.changeQueryMode(mode); await this.clickSaveInSettingsDialog(); - await this.editorTextArea.fill(query); - await this.explainButton.click(); - } - - async gearButtonContainsText(text: string) { - return this.gearButton.locator(`text=${text}`).isVisible(); - } - - async isBannerVisible() { - return this.banner.isVisible(); - } - - async isBannerHidden() { - return this.banner.isHidden(); - } - - async isRunButtonEnabled() { - return this.runButton.isEnabled(); - } - - async isRunButtonDisabled() { - return this.runButton.isDisabled(); + await this.setQuery(query); + await this.clickExplainButton(); } - async isExplainButtonEnabled() { - return this.explainButton.isEnabled(); - } - - async isExplainButtonDisabled() { - return this.explainButton.isDisabled(); + 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(); } @@ -101,30 +82,28 @@ export class QueryEditor extends BaseModel { return selectContentTable(runResult); } - async settingsDialogIsVisible() { - return this.settingsDialog.isVisible(); - } - - async settingsDialogIsNotVisible() { - return this.settingsDialog.isHidden(); - } - async clickGearButton() { + await this.gearButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); await this.gearButton.click(); } async clickCancelInSettingsDialog() { - await this.settingsDialog.getByRole('button', {name: /Cancel/}).click(); + const cancelButton = this.settingsDialog.getByRole('button', {name: /Cancel/}); + await cancelButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await cancelButton.click(); } async clickSaveInSettingsDialog() { - await this.settingsDialog.getByRole('button', {name: /Save/}).click(); + const saveButton = this.settingsDialog.getByRole('button', {name: /Save/}); + await saveButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await saveButton.click(); } async changeQueryMode(mode: QueryMode) { const dropdown = this.settingsDialog.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(); @@ -135,6 +114,7 @@ export class QueryEditor extends BaseModel { const dropdown = this.settingsDialog.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(); @@ -142,23 +122,39 @@ export class QueryEditor extends BaseModel { } async closeBanner() { - await this.banner.locator('button').click(); - } - - async isIndicatorIconVisible() { - return this.indicatorIcon.isVisible(); + const closeButton = this.banner.locator('button'); + await closeButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await closeButton.click(); } async hoverGearButton() { + await this.gearButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); await this.gearButton.hover(); } 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(); + } + + 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 9040dc076..3d680ca00 100644 --- a/tests/suites/tenant/queryEditor/queryEditor.test.ts +++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts @@ -3,12 +3,22 @@ import {expect, test} from '@playwright/test'; import {tenantName} from '../../../utils/constants'; import {TenantPage} from '../TenantPage'; -import {QueryEditor} from './QueryEditor'; +import {QueryEditor, VISIBILITY_TIMEOUT} from './QueryEditor'; test.describe('Test Query Editor', async () => { const testQuery = 'SELECT 1, 2, 3, 4, 5;'; - test.beforeEach(async ({page}) => { + test.beforeEach(async ({context, page}) => { + // Clear all browser storage + await context.clearCookies(); + await context.clearPermissions(); + + // Clear localStorage and sessionStorage + await context.addInitScript(() => { + window.localStorage.clear(); + window.sessionStorage.clear(); + }); + const pageQueryParams = { schema: tenantName, name: tenantName, @@ -23,10 +33,10 @@ test.describe('Test Query Editor', async () => { const queryEditor = new QueryEditor(page); await queryEditor.clickGearButton(); - await expect(queryEditor.settingsDialogIsVisible()).toBeTruthy(); + await expect(queryEditor.settingsDialog).toBeVisible({timeout: VISIBILITY_TIMEOUT}); await queryEditor.clickCancelInSettingsDialog(); - await expect(queryEditor.settingsDialogIsNotVisible()).toBeTruthy(); + await expect(queryEditor.settingsDialog).toBeHidden({timeout: VISIBILITY_TIMEOUT}); }); test('Settings dialog saves changes and updates Gear button', async ({page}) => { @@ -36,21 +46,24 @@ test.describe('Test Query Editor', async () => { await queryEditor.changeQueryMode('Scan'); await queryEditor.clickSaveInSettingsDialog(); - await expect(queryEditor.gearButtonContainsText('(1)')).toBeTruthy(); + 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, 'YQL Script'); - await expect(queryEditor.getRunResultTable()).toBeVisible(); + await expect(queryEditor.getRunResultTable()).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); test('Run button executes Scan', async ({page}) => { const queryEditor = new QueryEditor(page); await queryEditor.run(testQuery, 'Scan'); - await expect(queryEditor.getRunResultTable()).toBeVisible(); + await expect(queryEditor.getRunResultTable()).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); test('Explain button executes YQL script explanation', async ({page}) => { @@ -58,10 +71,10 @@ test.describe('Test Query Editor', async () => { await queryEditor.explain(testQuery, 'YQL Script'); const explainSchema = await queryEditor.getExplainResult('Schema'); - await expect(explainSchema).toBeVisible(); + await expect(explainSchema).toBeVisible({timeout: VISIBILITY_TIMEOUT}); const explainJSON = await queryEditor.getExplainResult('JSON'); - await expect(explainJSON).toBeVisible(); + await expect(explainJSON).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); test('Explain button executes Scan explanation', async ({page}) => { @@ -69,13 +82,13 @@ test.describe('Test Query Editor', async () => { await queryEditor.explain(testQuery, 'Scan'); const explainSchema = await queryEditor.getExplainResult('Schema'); - await expect(explainSchema).toBeVisible(); + await expect(explainSchema).toBeVisible({timeout: VISIBILITY_TIMEOUT}); const explainJSON = await queryEditor.getExplainResult('JSON'); - await expect(explainJSON).toBeVisible(); + await expect(explainJSON).toBeVisible({timeout: VISIBILITY_TIMEOUT}); const explainAST = await queryEditor.getExplainResult('AST'); - await expect(explainAST).toBeVisible(); + await expect(explainAST).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); test('Banner appears after executing script with changed settings', async ({page}) => { @@ -91,7 +104,7 @@ test.describe('Test Query Editor', async () => { await queryEditor.clickRunButton(); // Check if banner appears - await expect(queryEditor.isBannerVisible()).toBeTruthy(); + await expect(queryEditor.banner).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); test('Indicator icon appears after closing banner', async ({page}) => { @@ -109,9 +122,7 @@ test.describe('Test Query Editor', async () => { // Close the banner await queryEditor.closeBanner(); - // Check tooltip on hover - await queryEditor.hoverGearButton(); - await expect(queryEditor.isIndicatorIconVisible()).toBeTruthy(); + await expect(queryEditor.indicatorIcon).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); test('Gear button shows number of changed settings', async ({page}) => { @@ -119,22 +130,25 @@ test.describe('Test Query Editor', async () => { await queryEditor.clickGearButton(); await queryEditor.changeQueryMode('Scan'); - await queryEditor.changeIsolationLevel('Serializable'); + await queryEditor.changeIsolationLevel('Snapshot'); await queryEditor.clickSaveInSettingsDialog(); - await expect(queryEditor.gearButtonContainsText('(2)')).toBeTruthy(); + await expect(async () => { + const text = await queryEditor.gearButtonText(); + expect(text).toContain('(2)'); + }).toPass({timeout: 10000}); }); test('Run and Explain buttons are disabled when query is empty', async ({page}) => { const queryEditor = new QueryEditor(page); - await expect(queryEditor.isRunButtonDisabled()).toBeTruthy(); - await expect(queryEditor.isExplainButtonDisabled()).toBeTruthy(); + await expect(queryEditor.runButton).toBeDisabled({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.explainButton).toBeDisabled({timeout: VISIBILITY_TIMEOUT}); await queryEditor.setQuery(testQuery); - await expect(queryEditor.isRunButtonEnabled()).toBeTruthy(); - await expect(queryEditor.isExplainButtonEnabled()).toBeTruthy(); + await expect(queryEditor.runButton).toBeEnabled({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.explainButton).toBeEnabled({timeout: VISIBILITY_TIMEOUT}); }); test('Banner does not appear when executing script with default settings', async ({page}) => { @@ -143,6 +157,6 @@ test.describe('Test Query Editor', async () => { await queryEditor.setQuery(testQuery); await queryEditor.clickRunButton(); - await expect(queryEditor.isBannerHidden()).toBeTruthy(); + await expect(queryEditor.banner).toBeHidden({timeout: VISIBILITY_TIMEOUT}); }); }); From 04ccd9949bb4fae8f9400e4d102ecacf25929cba Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 18:03:07 +0300 Subject: [PATCH 04/12] fix: ci --- .github/workflows/ci.yml | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b01485369..89e43d7f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,14 +95,21 @@ jobs: if: always() id: test-results run: | - TOTAL=$(grep -c 'class="test-file-test' playwright-artifacts/playwright-report/index.html || echo "0") - PASSED=$(grep -c 'class="test-file-test test-file-test-outcome-expected"' playwright-artifacts/playwright-report/index.html || echo "0") - FAILED=$(grep -c 'class="test-file-test test-file-test-outcome-unexpected"' playwright-artifacts/playwright-report/index.html || echo "0") - SKIPPED=0 # В данной структуре нет явного указания на пропущенные тесты - echo "total=$TOTAL" >> $GITHUB_OUTPUT - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "failed=$FAILED" >> $GITHUB_OUTPUT - echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT + if [ -f "playwright-artifacts/test-results.json" ]; then + total=$(jq '.stats.total' playwright-artifacts/test-results.json) + passed=$(jq '.stats.passed' playwright-artifacts/test-results.json) + failed=$(jq '.stats.failed' playwright-artifacts/test-results.json) + skipped=$(jq '.stats.skipped' playwright-artifacts/test-results.json) + else + 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: Comment PR if: always() @@ -112,10 +119,10 @@ jobs: script: | const reportUrl = `https://${context.repo.owner}.github.io/${context.repo.repo}/${context.issue.number}/`; const testResults = { - total: ${{ steps.test-results.outputs.total }}, - passed: ${{ steps.test-results.outputs.passed }}, - failed: ${{ steps.test-results.outputs.failed }}, - skipped: ${{ steps.test-results.outputs.skipped }} + 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'; @@ -153,4 +160,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: comment - }) + }); From c377efb820ecd42586e9e89dc05bab6f11ffa1d5 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 18:17:24 +0300 Subject: [PATCH 05/12] fix: test --- .github/workflows/ci.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89e43d7f7..44744c31f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,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 @@ -66,6 +68,7 @@ jobs: run: npm run test:e2e:install - name: Run Playwright tests + id: run_tests run: npm run test:e2e env: CI: true @@ -95,17 +98,33 @@ jobs: 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 "Content of test-results.json:" + cat playwright-artifacts/test-results.json + + echo "Parsing JSON file:" total=$(jq '.stats.total' playwright-artifacts/test-results.json) passed=$(jq '.stats.passed' playwright-artifacts/test-results.json) failed=$(jq '.stats.failed' 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 From 5744c51caf23e77e536a6336b7273844da9df4da Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 18:31:34 +0300 Subject: [PATCH 06/12] fix: tests --- .github/workflows/ci.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44744c31f..04bd0684d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,14 +102,11 @@ jobs: echo "Listing playwright-artifacts directory:" ls -R playwright-artifacts - if [ -f "playwright-artifacts/test-results.json" ]; then - echo "Content of test-results.json:" - cat playwright-artifacts/test-results.json - + if [ -f "playwright-artifacts/test-results.json" ]; then echo "Parsing JSON file:" - total=$(jq '.stats.total' playwright-artifacts/test-results.json) - passed=$(jq '.stats.passed' playwright-artifacts/test-results.json) - failed=$(jq '.stats.failed' playwright-artifacts/test-results.json) + 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:" From e2ecfb737586da5581fa2c731f7aa64ee8c1994f Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 18:44:21 +0300 Subject: [PATCH 07/12] fix: save results for 5 days --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04bd0684d..41eecffa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: with: name: playwright-artifacts path: playwright-artifacts - retention-days: 30 + retention-days: 5 - name: Setup Pages if: always() From 6cba73edbc276ae70ca85a3fcd6c766518308515 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 19:29:15 +0300 Subject: [PATCH 08/12] fix: better tests --- .gitignore | 1 + .../suites/tenant/queryEditor/QueryEditor.ts | 206 ++++++++++++------ .../tenant/queryEditor/queryEditor.test.ts | 70 +++--- 3 files changed, 180 insertions(+), 97 deletions(-) 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/tests/suites/tenant/queryEditor/QueryEditor.ts b/tests/suites/tenant/queryEditor/QueryEditor.ts index 29615cb7a..318fde7a9 100644 --- a/tests/suites/tenant/queryEditor/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/QueryEditor.ts @@ -1,48 +1,142 @@ 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' | 'JSON' | 'AST'; +export enum QueryMode { + YQLScript = 'YQL Script', + Scan = 'Scan', +} -export const VISIBILITY_TIMEOUT = 5000; +export enum ExplainResultType { + Schema = 'Schema', + JSON = 'JSON', + AST = 'AST', +} + +export enum ButtonNames { + Run = 'Run', + Explain = 'Explain', + Cancel = 'Cancel', + Save = 'Save', +} -export class QueryEditor extends BaseModel { - editorTextArea: Locator; - runButton: Locator; - explainButton: Locator; - gearButton: Locator; - indicatorIcon: Locator; - settingsDialog: Locator; - banner: 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'); + } + + 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; + } +} + +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: /Run/}); - this.explainButton = this.selector.getByRole('button', {name: /Explain/}); + 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.settingsDialog = this.page.locator('.ydb-query-settings-dialog'); 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.clickGearButton(); - await this.changeQueryMode(mode); - await this.clickSaveInSettingsDialog(); + 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.clickGearButton(); - await this.changeQueryMode(mode); - await this.clickSaveInSettingsDialog(); + await this.settingsDialog.changeQueryMode(mode); + await this.settingsDialog.clickButton(ButtonNames.Save); await this.setQuery(query); await this.clickExplainButton(); } @@ -64,63 +158,22 @@ export class QueryEditor extends BaseModel { 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 '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'); - return selectContentTable(runResult); - } - async clickGearButton() { await this.gearButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); await this.gearButton.click(); } - async clickCancelInSettingsDialog() { - const cancelButton = this.settingsDialog.getByRole('button', {name: /Cancel/}); - await cancelButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); - await cancelButton.click(); - } - - async clickSaveInSettingsDialog() { - const saveButton = this.settingsDialog.getByRole('button', {name: /Save/}); - await saveButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); - await saveButton.click(); - } - - async changeQueryMode(mode: QueryMode) { - const dropdown = this.settingsDialog.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.settingsDialog.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 closeBanner() { const closeButton = this.banner.locator('button'); await closeButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); @@ -144,6 +197,29 @@ export class QueryEditor extends BaseModel { await typeButton.click(); } + async isRunButtonEnabled() { + return this.runButton.isEnabled({timeout: VISIBILITY_TIMEOUT}); + } + + async isExplainButtonEnabled() { + return this.explainButton.isEnabled({timeout: VISIBILITY_TIMEOUT}); + } + + 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; + } + + 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 { diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts index 3d680ca00..29ca8809f 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, VISIBILITY_TIMEOUT} 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;'; @@ -33,18 +39,18 @@ test.describe('Test Query Editor', async () => { const queryEditor = new QueryEditor(page); await queryEditor.clickGearButton(); - await expect(queryEditor.settingsDialog).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.settingsDialog.isVisible()).resolves.toBe(true); - await queryEditor.clickCancelInSettingsDialog(); - await expect(queryEditor.settingsDialog).toBeHidden({timeout: VISIBILITY_TIMEOUT}); + 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.changeQueryMode('Scan'); - await queryEditor.clickSaveInSettingsDialog(); + await queryEditor.settingsDialog.changeQueryMode(QueryMode.Scan); + await queryEditor.settingsDialog.clickButton(ButtonNames.Save); await expect(async () => { const text = await queryEditor.gearButtonText(); @@ -54,40 +60,40 @@ test.describe('Test Query Editor', async () => { test('Run button executes YQL script', async ({page}) => { const queryEditor = new QueryEditor(page); - await queryEditor.run(testQuery, 'YQL Script'); + await queryEditor.run(testQuery, QueryMode.YQLScript); - await expect(queryEditor.getRunResultTable()).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true); }); 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({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true); }); 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'); + const explainSchema = await queryEditor.getExplainResult(ExplainResultType.Schema); await expect(explainSchema).toBeVisible({timeout: VISIBILITY_TIMEOUT}); - const explainJSON = await queryEditor.getExplainResult('JSON'); + const explainJSON = await queryEditor.getExplainResult(ExplainResultType.JSON); await expect(explainJSON).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); 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('Schema'); + const explainSchema = await queryEditor.getExplainResult(ExplainResultType.Schema); await expect(explainSchema).toBeVisible({timeout: VISIBILITY_TIMEOUT}); - const explainJSON = await queryEditor.getExplainResult('JSON'); + const explainJSON = await queryEditor.getExplainResult(ExplainResultType.JSON); await expect(explainJSON).toBeVisible({timeout: VISIBILITY_TIMEOUT}); - const explainAST = await queryEditor.getExplainResult('AST'); + const explainAST = await queryEditor.getExplainResult(ExplainResultType.AST); await expect(explainAST).toBeVisible({timeout: VISIBILITY_TIMEOUT}); }); @@ -96,15 +102,15 @@ test.describe('Test Query Editor', async () => { // Change a setting await queryEditor.clickGearButton(); - await queryEditor.changeQueryMode('Scan'); - await queryEditor.clickSaveInSettingsDialog(); + 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.banner).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.isBannerVisible()).resolves.toBe(true); }); test('Indicator icon appears after closing banner', async ({page}) => { @@ -112,8 +118,8 @@ test.describe('Test Query Editor', async () => { // Change a setting await queryEditor.clickGearButton(); - await queryEditor.changeQueryMode('Scan'); - await queryEditor.clickSaveInSettingsDialog(); + await queryEditor.settingsDialog.changeQueryMode(QueryMode.Scan); + await queryEditor.settingsDialog.clickButton(ButtonNames.Save); // Execute a script to make the banner appear await queryEditor.setQuery(testQuery); @@ -122,33 +128,33 @@ test.describe('Test Query Editor', async () => { // Close the banner await queryEditor.closeBanner(); - await expect(queryEditor.indicatorIcon).toBeVisible({timeout: VISIBILITY_TIMEOUT}); + 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.changeQueryMode('Scan'); - await queryEditor.changeIsolationLevel('Snapshot'); - await queryEditor.clickSaveInSettingsDialog(); + 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: 10000}); + }).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.runButton).toBeDisabled({timeout: VISIBILITY_TIMEOUT}); - await expect(queryEditor.explainButton).toBeDisabled({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.isRunButtonEnabled()).resolves.toBe(false); + await expect(queryEditor.isExplainButtonEnabled()).resolves.toBe(false); await queryEditor.setQuery(testQuery); - await expect(queryEditor.runButton).toBeEnabled({timeout: VISIBILITY_TIMEOUT}); - await expect(queryEditor.explainButton).toBeEnabled({timeout: VISIBILITY_TIMEOUT}); + 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}) => { @@ -157,6 +163,6 @@ test.describe('Test Query Editor', async () => { await queryEditor.setQuery(testQuery); await queryEditor.clickRunButton(); - await expect(queryEditor.banner).toBeHidden({timeout: VISIBILITY_TIMEOUT}); + await expect(queryEditor.isBannerHidden()).resolves.toBe(true); }); }); From c10cc411450a2bd4a2332052b00fd90eaae42d2e Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 1 Aug 2024 19:34:18 +0300 Subject: [PATCH 09/12] fix: review fixes --- src/containers/Authentication/Authentication.tsx | 2 +- .../Tenant/Query/QueryEditor/QueryEditor.tsx | 12 ++++++------ .../QuerySettingsDialog/QuerySettingsDialog.tsx | 8 ++++---- src/containers/UserSettings/i18n/en.json | 4 ++-- src/containers/UserSettings/settings.tsx | 12 ++++++------ src/services/settings.ts | 6 ++---- src/store/reducers/executeQuery.ts | 6 +++--- src/store/reducers/explainQuery/explainQuery.ts | 6 +++--- src/utils/constants.ts | 6 +----- src/utils/hooks/useQueryExecutionSettings.ts | 6 +++--- 10 files changed, 31 insertions(+), 37 deletions(-) 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/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index ecbad65de..a83b61d99 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -27,9 +27,9 @@ 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_USE_MULTI_SCHEMA_KEY, - TRACING_LEVEL_VERBOSITY_KEY, } from '../../../../utils/constants'; import {useQueryExecutionSettings, useSetting} from '../../../../utils/hooks'; import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings'; @@ -108,7 +108,7 @@ function QueryEditor(props: QueryEditorProps) { const [resultType, setResultType] = React.useState(RESULT_TYPES.EXECUTE); const [isResultLoaded, setIsResultLoaded] = React.useState(false); const [querySettings] = useQueryExecutionSettings(); - const [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); + const [enableTracingLevel] = useSetting(ENABLE_TRACING_LEVEL_KEY); const [lastQueryExecutionSettings, setLastQueryExecutionSettings] = useLastQueryExecutionSettings(); const {resetBanner} = useChangedQuerySettings(); @@ -212,7 +212,7 @@ function QueryEditor(props: QueryEditorProps) { database: tenantName, querySettings, schema, - tracingLevelVerbosity, + enableTracingLevel, }); setIsResultLoaded(true); setShowPreview(false); @@ -228,7 +228,7 @@ function QueryEditor(props: QueryEditorProps) { }, [ executeQuery, - tracingLevelVerbosity, + enableTracingLevel, useMultiSchema, setLastUsedQueryAction, lastQueryExecutionSettings, @@ -262,7 +262,7 @@ function QueryEditor(props: QueryEditorProps) { query: input, database: tenantName, querySettings, - tracingLevelVerbosity, + enableTracingLevel, }); setIsResultLoaded(true); setShowPreview(false); @@ -272,7 +272,7 @@ function QueryEditor(props: QueryEditorProps) { lastQueryExecutionSettings, querySettings, resetBanner, - tracingLevelVerbosity, + enableTracingLevel, sendExplainQuery, setLastQueryExecutionSettings, setLastUsedQueryAction, diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx index 6a39a1877..66a0b575b 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx @@ -3,7 +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 {TRACING_LEVEL_VERBOSITY_KEY} from '../../../../lib'; +import {ENABLE_TRACING_LEVEL_KEY} from '../../../../lib'; import { selectQueryAction, setQueryAction, @@ -71,7 +71,7 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm defaultValues: initialValues, }); - const [tracingLevelVerbosity] = useSetting(TRACING_LEVEL_VERBOSITY_KEY); + const [enableTracingLevel] = useSetting(ENABLE_TRACING_LEVEL_KEY); return ( @@ -118,7 +118,7 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm />
- {tracingLevelVerbosity && ( + {enableTracingLevel && (