diff --git a/.eslintignore b/.eslintignore index 767a7ea22..ae31391e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,3 +17,4 @@ tmp **/playwright-report **/html-report/* examples/** +wt/** \ No newline at end of file diff --git a/lib/static/modules/query-params.js b/lib/static/modules/query-params.js index 3ab80bd97..0488ee39e 100644 --- a/lib/static/modules/query-params.js +++ b/lib/static/modules/query-params.js @@ -88,6 +88,18 @@ export function decodeBrowsers(browsers) { .map(decode); } +export function expandBrowserVersions(filteredBrowsers, availableBrowsers) { + return filteredBrowsers.map(filteredBrowser => { + if (isEmpty(filteredBrowser.versions)) { + const availableBrowser = availableBrowsers.find(b => b.id === filteredBrowser.id); + if (availableBrowser) { + return {id: filteredBrowser.id, versions: [...availableBrowser.versions]}; + } + } + return filteredBrowser; + }); +} + export function setFilteredBrowsers(browsers) { const urlExtendedWithBrowsers = appendQuery( window.location.href, diff --git a/lib/static/modules/reducers/filters.ts b/lib/static/modules/reducers/filters.ts index 5f2ec8ae6..03c920dd1 100644 --- a/lib/static/modules/reducers/filters.ts +++ b/lib/static/modules/reducers/filters.ts @@ -6,6 +6,7 @@ import {DiffModeId, DiffModes, ViewMode} from '@/constants'; import {BrowserItem} from '@/types'; import * as localStorageWrapper from '@/static/modules/local-storage-wrapper'; import {getViewQuery} from '@/static/modules/custom-queries'; +import {expandBrowserVersions} from '@/static/modules/query-params'; import {isEmpty} from 'lodash'; import {applyStateUpdate} from '@/static/modules/utils/state'; @@ -33,6 +34,8 @@ export default (state: State, action: FiltersAction | InitGuiReportAction | Init if (isEmpty(viewQuery.filteredBrowsers)) { viewQuery.filteredBrowsers = state.browsers; + } else { + viewQuery.filteredBrowsers = expandBrowserVersions(viewQuery.filteredBrowsers as BrowserItem[], state.browsers); } const newState = applyStateUpdate( diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 116728fab..461f131bc 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -110,4 +110,44 @@ a:hover { b { font-weight: 500; -} \ No newline at end of file +} + + +.toaster { + border: 1px solid rgb(229, 231, 235); + box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px !important; + margin-right: 10px; + margin-bottom: 20px !important; +} + +@keyframes toast-appear { + from { + opacity: 0; + scale: 0.95; + } + + to { + opacity: 1; + scale: 1; + } +} + +.toaster:global(.g-toast-animation-desktop_enter_active) { + animation: toast-appear .15s forwards ease; +} + +.toaster:global(.g-toast-animation-desktop_exit_active) { + animation: toast-appear .15s forwards ease reverse; +} + +.toaster :global(.g-toast__title) { + font-weight: 450; +} + +.toaster :global(.g-toast__content) { + color: var(--g-color-private-black-400); +} + +.toaster__icon--error { + color: var(--g-color-private-red-600-solid); +} diff --git a/lib/static/new-ui/components/BrowsersSelect/index.tsx b/lib/static/new-ui/components/BrowsersSelect/index.tsx index 21d9ee16e..97b119a3b 100644 --- a/lib/static/new-ui/components/BrowsersSelect/index.tsx +++ b/lib/static/new-ui/components/BrowsersSelect/index.tsx @@ -1,6 +1,6 @@ import {PlanetEarth} from '@gravity-ui/icons'; -import {Button, Flex, Icon, Select, SelectRenderControlProps, SelectRenderOption} from '@gravity-ui/uikit'; -import React, {ReactNode, Ref, useEffect, useState} from 'react'; +import {Button, Flex, Icon, Select, SelectRenderControlProps, SelectRenderOption, useSelectOptions} from '@gravity-ui/uikit'; +import React, {ReactNode, Ref, useEffect, useMemo, useState} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {selectBrowsers} from '@/static/modules/actions'; @@ -75,48 +75,40 @@ export function BrowsersSelect(): ReactNode { setSelectedBrowsers(selectedItems); }; - const renderOptions = (): React.JSX.Element | React.JSX.Element[] => { + const getOptionData = (browser: BrowserItem, version: string, content: string): {value: string; content: string; data: {id: string; version: string}} => ({ + value: serializeBrowserData(browser.id, version), + content, + data: {id: browser.id, version} + }); + + const rawOptions = useMemo(() => { const browsersWithMultipleVersions = browsers.filter(browser => browser.versions.length > 1); const browsersWithSingleVersion = browsers.filter(browser => browser.versions.length === 1); - const getOptionProps = (browser: BrowserItem, version: string): {value: string; content: string; data: Record} => ({ - value: serializeBrowserData(browser.id, version), - content: browser.id, - data: {id: browser.id, version} - }); - if (browsersWithMultipleVersions.length === 0) { - // If there are no browsers with multiple versions, we want to render a simple plain list - return browsers.map(browser => ); + // If there are no browsers with multiple versions, we want to render a simple flat list + return browsers.map(browser => getOptionData(browser, browser.versions[0], browser.id)); } else { - // Otherwise render browser version groups and place all browsers with single version into "Other" group - return ( - <> - {browsersWithMultipleVersions.map(browser => ( - - {browser.versions.map(version => ( - - ))} - - ))} - - {browsersWithSingleVersion.map(browser => ( - - ))} - - - ); + // Otherwise render browser version groups and place all browsers with a single version in the "Other" group + const groups: {label: string; options: ReturnType[]}[] = []; + + browsersWithMultipleVersions.forEach(browser => { + groups.push({ + label: browser.id, + options: browser.versions.map(version => getOptionData(browser, version, version)) + }); + }); + + groups.push({ + label: 'Other', + options: browsersWithSingleVersion.map(browser => getOptionData(browser, browser.versions[0], browser.id)) + }); + + return groups; } - }; + }, [browsers]); + + const options = useSelectOptions({options: rawOptions}); const isInitialized = useSelector(getIsInitialized); @@ -171,6 +163,7 @@ export function BrowsersSelect(): ReactNode { + /> ); } diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css b/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css index 882561da8..9549a7d45 100644 --- a/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css @@ -65,42 +65,3 @@ .modal-button-primary { composes: regular-button from global, action-button from global; } - -.error-toaster { - border: 1px solid rgb(229, 231, 235); - box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px !important; - margin-right: 10px; - margin-bottom: 20px !important; -} - -@keyframes toast-appear { - from { - opacity: 0; - scale: 0.95; - } - - to { - opacity: 1; - scale: 1; - } -} - -.error-toaster:global(.g-toast-animation-desktop_enter_active) { - animation: toast-appear .15s forwards ease; -} - -.error-toaster:global(.g-toast-animation-desktop_exit_active) { - animation: toast-appear .15s forwards ease reverse; -} - -.error-toaster :global(.g-toast__title) { - font-weight: 450; -} - -.error-toaster :global(.g-toast__content) { - color: var(--g-color-private-black-400); -} - -.error-toaster-icon { - color: var(--g-color-private-red-600-solid); -} diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx index ec045fcb6..0096d226b 100644 --- a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx @@ -93,8 +93,8 @@ export function GuiniToolbarOverlay(): ReactNode { content: result.error.message + '. See console for details.', isClosable: true, autoHiding: 5000, - renderIcon: () => , - className: styles.errorToaster + renderIcon: () => , + className: 'toaster' }); } }; diff --git a/lib/static/new-ui/components/SuiteTitle/index.tsx b/lib/static/new-ui/components/SuiteTitle/index.tsx index 9a4e4b4ea..2d8ba625f 100644 --- a/lib/static/new-ui/components/SuiteTitle/index.tsx +++ b/lib/static/new-ui/components/SuiteTitle/index.tsx @@ -34,7 +34,7 @@ export function SuiteTitle(props: SuiteTitleProps): ReactNode { ))} }
-

+

{suiteName ?? 'Unknown Suite'}

diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx index 0cac780e1..03fa280f6 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -37,11 +37,15 @@ import {getIconByStatus} from '@/static/new-ui/utils'; import {Page} from '@/constants'; import {usePage} from '@/static/new-ui/hooks/usePage'; import {useHotkey} from '@/static/new-ui/hooks/useHotkey'; +import {useLegacyUrlMigration} from '@/static/new-ui/hooks/useLegacyUrlMigration'; import {changeTestRetry, setCurrentTreeNode, setStrictMatchFilter} from '@/static/modules/actions'; import {getUrl} from '@/static/new-ui/utils/getUrl'; export function SuitesPage(): ReactNode { const page = usePage(); + + useLegacyUrlMigration(); + const currentResult = useSelector(getCurrentResult); const treeData = useSelector(getSuitesTreeViewData); const resultImages = useSelector(getCurrentResultImages); diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts b/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts index 95fe0166f..912d16105 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts +++ b/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts @@ -2,6 +2,7 @@ import {createSelector} from 'reselect'; import { getAllRootGroupIds, getBrowsers, + getBrowsersList, getBrowsersState, getGroups, getImages, @@ -18,14 +19,14 @@ import {State} from '@/static/new-ui/types/store'; // Converts the existing store structure to the one that can be consumed by GravityUI export const getSuitesTreeViewData = createSelector( - [getGroups, getSuites, getAllRootGroupIds, getBrowsers, getBrowsersState, getResults, getImages, getTreeViewMode, getSortTestsData], - (groups, suites, rootGroupIds, browsers, browsersState, results, images, treeViewMode, sortTestsData): TreeViewData => { + [getGroups, getSuites, getAllRootGroupIds, getBrowsers, getBrowsersState, getResults, getImages, getTreeViewMode, getSortTestsData, getBrowsersList], + (groups, suites, rootGroupIds, browsers, browsersState, results, images, treeViewMode, sortTestsData, browsersList): TreeViewData => { const currentSortDirection = sortTestsData.currentDirection; const currentSortExpression = sortTestsData.availableExpressions .find(expr => expr.id === sortTestsData.currentExpressionIds[0]) ?? sortTestsData.availableExpressions[0]; - const entitiesContext = {results, images, suites, treeViewMode, browsersState, browsers, groups, currentSortDirection, currentSortExpression}; + const entitiesContext = {results, images, suites, treeViewMode, browsersState, browsers, groups, currentSortDirection, currentSortExpression, browsersList}; const isGroupingEnabled = rootGroupIds.length > 0; if (isGroupingEnabled) { diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/utils.ts b/lib/static/new-ui/features/suites/components/SuitesPage/utils.ts index 0e609ef5d..3dac0de20 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/utils.ts +++ b/lib/static/new-ui/features/suites/components/SuitesPage/utils.ts @@ -21,6 +21,7 @@ import { import {EntityType, TreeNode, TreeRoot} from '@/static/new-ui/features/suites/components/SuitesPage/types'; import {getEntityType, getGroupId} from '@/static/new-ui/features/suites/utils'; import {isAcceptable} from '@/static/modules/utils'; +import {BrowserItem} from '@/types'; export const getTitlePath = (suites: Record, entity: SuiteEntity | BrowserEntity | undefined): string[] => { if (!entity) { @@ -44,9 +45,10 @@ export interface EntitiesContext { treeViewMode: TreeViewMode; currentSortDirection: SortDirection; currentSortExpression: SortByExpression; + browsersList: BrowserItem[]; } -export const formatEntityToTreeNodeData = ({results, images, suites, treeViewMode}: EntitiesContext, entity: SuiteEntity | BrowserEntity | GroupEntity, id: string, parentData?: TreeNode['data']): TreeNode['data'] => { +export const formatEntityToTreeNodeData = ({results, images, suites, treeViewMode, browsersList}: EntitiesContext, entity: SuiteEntity | BrowserEntity | GroupEntity, id: string, parentData?: TreeNode['data']): TreeNode['data'] => { if (isSuiteEntity(entity)) { return { id, @@ -86,6 +88,16 @@ export const formatEntityToTreeNodeData = ({results, images, suites, treeViewMod errorStack = stackLines.slice(0, 3).join('\n'); } + // Add version tag for browsers with multiple versions + const tags: string[] = []; + const browserItem = browsersList.find(b => b.id === entity.name); + if (browserItem && browserItem.versions.length > 1) { + const version = lastResult.metaInfo?.browserVersion; + if (version) { + tags.push(version); + } + } + return { id, entityType: getEntityType(entity), @@ -97,7 +109,7 @@ export const formatEntityToTreeNodeData = ({results, images, suites, treeViewMod errorStack, parentData, skipReason: lastResult.skipReason, - tags: [] + tags }; }; diff --git a/lib/static/new-ui/hooks/useLegacyUrlMigration.tsx b/lib/static/new-ui/hooks/useLegacyUrlMigration.tsx new file mode 100644 index 000000000..4275f059c --- /dev/null +++ b/lib/static/new-ui/hooks/useLegacyUrlMigration.tsx @@ -0,0 +1,178 @@ +import React, {useEffect, useRef} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {useNavigate} from 'react-router-dom'; +import {useToaster} from '@gravity-ui/uikit'; +import {TriangleExclamation} from '@gravity-ui/icons'; + +import {getIsInitialized, getBrowsersList} from '@/static/new-ui/store/selectors'; +import {Page} from '@/constants'; +import {State} from '@/static/new-ui/types/store'; +import {getUrl} from '@/static/new-ui/utils/getUrl'; +import {changeTestRetry, changeViewMode, selectBrowsers, setCurrentTreeNode, setStrictMatchFilter, updateNameFilter} from '@/static/modules/actions'; +import {getSuitesTreeViewData} from '@/static/new-ui/features/suites/components/SuitesPage/selectors'; +import {findTreeNodeByBrowserId, getGroupId} from '@/static/new-ui/features/suites/utils'; + +interface LegacyUrlParams { + testNameFilter?: string; + browser?: string; + strictMatchFilter?: boolean; + retryIndex?: number; +} + +const LEGACY_URL_PARAMS_TO_REMOVE = ['testNameFilter', 'strictMatchFilter', 'retryIndex', 'expand', 'viewModes']; + +const parseLegacyUrlParams = (search: string): LegacyUrlParams => { + const params = new URLSearchParams(search); + const result: LegacyUrlParams = {}; + + if (params.has('testNameFilter')) { + result.testNameFilter = params.get('testNameFilter') ?? undefined; + } + + if (params.has('browser')) { + result.browser = params.get('browser') ?? undefined; + } + + if (params.has('strictMatchFilter')) { + result.strictMatchFilter = params.get('strictMatchFilter') === 'true'; + } + + if (params.has('retryIndex')) { + const retryIndex = parseInt(params.get('retryIndex') ?? '', 10); + if (!isNaN(retryIndex)) { + result.retryIndex = retryIndex; + } + } + + return result; +}; + +const hasLegacyUrlParams = (search: string): boolean => { + const params = new URLSearchParams(search); + return LEGACY_URL_PARAMS_TO_REMOVE.some(param => params.has(param)); +}; + +const cleanLegacyUrlParams = (search: string): string => { + const params = new URLSearchParams(search); + LEGACY_URL_PARAMS_TO_REMOVE.forEach(param => params.delete(param)); + params.delete('browser'); + return params.toString(); +}; + +/** + * Hook that handles migration from old UI URL format to new UI URL format. + * + * Old format: ?testNameFilter=...&browser=...&strictMatchFilter=true&retryIndex=0&expand=all + * New format: /#/suites/:hash/:browser/:attempt/:stateName + * + * When old format is detected, this hook: + * 1. Finds the matching test based on testNameFilter and browser + * 2. Selects that test in the tree + * 3. Navigates to the new URL format + * 4. Removes old query parameters from the URL + */ +export const useLegacyUrlMigration = (): void => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const toaster = useToaster(); + + const isInitialized = useSelector(getIsInitialized); + const treeData = useSelector(getSuitesTreeViewData); + const browsers = useSelector((state: State) => state.tree.browsers.byId); + const suites = useSelector((state: State) => state.tree.suites.byId); + const allBrowsersList = useSelector(getBrowsersList); + const currentViewMode = useSelector((state: State) => state.app[Page.suitesPage].viewMode); + + const migrationAttemptedRef = useRef(false); + + useEffect(() => { + if (!isInitialized || migrationAttemptedRef.current) { + return; + } + + const search = window.location.search; + + if (!hasLegacyUrlParams(search)) { + return; + } + + migrationAttemptedRef.current = true; + + const legacyParams = parseLegacyUrlParams(search); + + let matchedBrowserId: string | null = null; + + if (legacyParams.testNameFilter) { + // TODO: use more explicit ID generation + matchedBrowserId = browsers[`${legacyParams.testNameFilter} ${legacyParams.browser}`]?.id ?? null; + + if (!matchedBrowserId) { + toaster.add({ + name: 'legacy-url-migration-warning', + title: 'Test not found', + content: `Could not find a test named "${legacyParams.testNameFilter}". Showing all tests instead.`, + isClosable: true, + autoHiding: 10000, + renderIcon: () => , + className: 'toaster' + }); + + dispatch(selectBrowsers({page: Page.suitesPage, data: allBrowsersList})); + dispatch(updateNameFilter({page: Page.suitesPage, data: ''})); + dispatch(setStrictMatchFilter(false)); + dispatch(changeViewMode({page: Page.suitesPage, data: currentViewMode})); + } + } + + if (matchedBrowserId) { + dispatch(selectBrowsers({page: Page.suitesPage, data: allBrowsersList})); + const browser = browsers[matchedBrowserId]; + const suite = suites[browser.parentId]; + + if (suite && browser) { + const treeNode = findTreeNodeByBrowserId(treeData.tree, matchedBrowserId); + + if (treeNode) { + const groupId = getGroupId(treeNode); + + dispatch(setCurrentTreeNode({ + browserId: matchedBrowserId, + treeNodeId: treeNode.id, + groupId + })); + + const retryIndex = legacyParams.retryIndex ?? browser.resultIds.length - 1; + const validRetryIndex = Math.max(0, Math.min(retryIndex, browser.resultIds.length - 1)); + + dispatch(changeTestRetry({ + browserId: matchedBrowserId, + retryIndex: validRetryIndex + })); + + const newUrl = getUrl({ + page: Page.suitesPage, + hash: suite.hash, + browser: browser.name, + attempt: validRetryIndex + }); + + navigate(newUrl, {replace: true}); + + dispatch(updateNameFilter({page: Page.suitesPage, data: ''})); + dispatch(setStrictMatchFilter(false)); + // This one's for ensuring "expanded" states of tree nodes are inited + dispatch(changeViewMode({page: Page.suitesPage, data: currentViewMode})); + } + } + } + + const cleanedSearch = cleanLegacyUrlParams(search); + const newSearchString = cleanedSearch ? `?${cleanedSearch}` : ''; + + if (newSearchString !== search) { + const currentUrl = new URL(window.location.href); + currentUrl.search = newSearchString; + window.history.replaceState(null, '', currentUrl.toString()); + } + }, [isInitialized, browsers, suites, treeData, allBrowsersList, currentViewMode, dispatch, navigate, toaster]); +}; diff --git a/lib/static/new-ui/store/selectors.ts b/lib/static/new-ui/store/selectors.ts index c465a05bf..6e4a669cb 100644 --- a/lib/static/new-ui/store/selectors.ts +++ b/lib/static/new-ui/store/selectors.ts @@ -8,6 +8,7 @@ import { BrowserState, GroupEntity, TreeViewMode } from '@/static/new-ui/types/store'; import {EditScreensFeature, RunTestsFeature} from '@/constants'; +import {BrowserItem} from '@/types'; export const getToolName = (state: State): string => state.apiValues.toolName; export const getAllRootSuiteIds = (state: State): string[] => state.tree.suites.allRootIds; @@ -28,3 +29,4 @@ export const getTreeViewMode = (state: State): TreeViewMode => state.ui.suitesPa export const getSortTestsData = (state: State): State['app']['sortTestsData'] => state.app.sortTestsData; export const getAreCheckboxesNeeded = (state: State): boolean => state.app.availableFeatures.includes(RunTestsFeature) || state.app.availableFeatures.includes(EditScreensFeature); +export const getBrowsersList = (state: State): BrowserItem[] => state.browsers; diff --git a/test/func/tests/.testplane.conf.js b/test/func/tests/.testplane.conf.js index f9736295f..160f6533b 100644 --- a/test/func/tests/.testplane.conf.js +++ b/test/func/tests/.testplane.conf.js @@ -74,6 +74,10 @@ if (isRunningAnalyticsTests) { if (!isRunningGuiTests && !isRunningAnalyticsTests) { _.set(config.plugins, ['hermione-global-hook', 'beforeEach'], async function({browser}) { + if (/Legacy URL migration/i.test(browser.executionContext.ctx.currentTest.titlePath().join(' '))) { + return; + } + if (/new ui/i.test(browser.executionContext.ctx.currentTest.titlePath().join(' '))) { await browser.url(this.browser.options.baseUrl + 'new-ui.html'); } else { diff --git a/test/func/tests/common/new-ui/suites-page/legacy-url-migration.testplane.js b/test/func/tests/common/new-ui/suites-page/legacy-url-migration.testplane.js new file mode 100644 index 000000000..0bfd9f6cd --- /dev/null +++ b/test/func/tests/common/new-ui/suites-page/legacy-url-migration.testplane.js @@ -0,0 +1,100 @@ +const {getTestSectionByNameSelector} = require('../../../utils'); + +if (process.env.TOOL === 'testplane') { + describe(process.env.TOOL || 'Default', () => { + describe('New UI', () => { + describe('Suites page', () => { + describe('Legacy URL migration', () => { + const getHash = async (browser) => { + const currentUrl = new URL(await browser.getUrl()); + return currentUrl.hash.slice(1); + }; + + const getQueryParams = async (browser) => { + const currentUrl = await browser.getUrl(); + const url = new URL(currentUrl); + return url.search; + }; + + const interceptClipboard = async (browser) => { + await browser.execute(() => { + window.__copiedText = null; + const originalExecCommand = document.execCommand.bind(document); + document.execCommand = (command, ...args) => { + if (command === 'copy') { + const activeEl = document.activeElement; + if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) { + window.__copiedText = activeEl.value; + } + } + return originalExecCommand(command, ...args); + }; + }); + }; + + const getCopiedText = async (browser) => { + return browser.execute(() => window.__copiedText); + }; + + it('should migrate from old UI link to new UI with correct test, browser and attempt', async ({browser}) => { + await browser.url(browser.options.baseUrl); + + await interceptClipboard(browser); + + const testSection = await browser.$(getTestSectionByNameSelector('test with image comparison diff')); + const copyButton = await testSection.$('button[title="copy test link"]'); + await copyButton.waitForDisplayed(); + await copyButton.click(); + + const copiedUrl = await getCopiedText(browser); + expect(copiedUrl).toBeTruthy(); + + const newUiUrl = new URL(copiedUrl); + newUiUrl.pathname += 'new-ui.html'; + await browser.url(newUiUrl.toString()); + + const titleElement = await browser.$('[data-qa="suite-title"]'); + await expect(titleElement).toHaveText('test with image comparison diff'); + + const retrySwitcher = await browser.$('[data-qa="retry-switcher"][data-qa-active="true"]'); + await expect(retrySwitcher).toHaveText('2'); + + const hash = await getHash(browser); + expect(hash).toMatch(/^\/suites\/[a-z0-9]+\/chrome\/\d+/); + + const queryString = await getQueryParams(browser); + expect(queryString).not.toContain('testNameFilter'); + expect(queryString).not.toContain('strictMatchFilter'); + expect(queryString).not.toContain('retryIndex'); + }); + + it('should show notification when test not found', async ({browser}) => { + const baseUrl = browser.options.baseUrl; + const params = new URLSearchParams({ + browser: 'chrome', + testNameFilter: 'non-existent test that does not exist', + strictMatchFilter: 'true', + retryIndex: '0', + viewModes: 'all', + expand: 'all' + }); + + await browser.url(baseUrl + 'new-ui.html?' + params.toString()); + + const toast = await browser.$('.g-toaster'); + await browser.waitUntil(async () => { + const text = await toast.getText(); + return text.includes('Test not found'); + }); + + const queryString = await getQueryParams(browser); + expect(queryString).not.toContain('testNameFilter'); + expect(queryString).not.toContain('strictMatchFilter'); + + await expect(browser.$('[data-qa="tree-view-item"]')).toBeDisplayed(); + }); + }); + }); + }); + }); +} diff --git a/test/unit/lib/report-builder/static.js b/test/unit/lib/report-builder/static.js index b9f2c2ec4..98e6b0f86 100644 --- a/test/unit/lib/report-builder/static.js +++ b/test/unit/lib/report-builder/static.js @@ -152,7 +152,7 @@ describe('StaticReportBuilder', () => { }); }); - describe.only('add badge attachment', () => { + describe('add badge attachment', () => { it('should add attachment with badge using callback from config', async () => { const mockBadges = [ { diff --git a/test/unit/lib/static/modules/reducers/filters.js b/test/unit/lib/static/modules/reducers/filters.js index 9a07f5af1..86a51cc61 100644 --- a/test/unit/lib/static/modules/reducers/filters.js +++ b/test/unit/lib/static/modules/reducers/filters.js @@ -58,30 +58,81 @@ describe('lib/static/modules/reducers/view', () => { assert.deepStrictEqual(newState.app[page].filteredBrowsers, []); }); - it('should set "filteredBrowsers" property to specified browsers', () => { + it('should set "filteredBrowsers" property to specified browsers with expanded versions', () => { global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari${hash}`); + const state = { + ...defaultState, + browsers: [ + {id: 'firefox', versions: ['v1', 'v2']}, + {id: 'safari', versions: ['v3']} + ] + }; const action = {type, payload: _mkInitialState()}; - const newState = reducer(defaultState, action); + const newState = reducer(state, action); + + assert.deepStrictEqual(newState.app[page].filteredBrowsers, [ + {id: 'firefox', versions: ['v1', 'v2']}, + {id: 'safari', versions: ['v3']} + ]); + }); + + it('should keep empty versions for browsers not found in available browsers', () => { + global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari${hash}`); + const state = { + ...defaultState, + browsers: [ + {id: 'firefox', versions: ['v1', 'v2']} + ] + }; + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(state, action); assert.deepStrictEqual(newState.app[page].filteredBrowsers, [ - {id: 'firefox', versions: []}, + {id: 'firefox', versions: ['v1', 'v2']}, {id: 'safari', versions: []} ]); }); - it('should set "filteredBrowsers" property to specified browsers and versions', () => { + it('should set "filteredBrowsers" property to specified browsers and versions without expansion', () => { global.window.location = new URL(`${baseUrl}?browser=firefox&browser=safari:23,11.2${hash}`); + const state = { + ...defaultState, + browsers: [ + {id: 'firefox', versions: ['v1', 'v2']}, + {id: 'safari', versions: ['23', '11.2', '10.0']} + ] + }; const action = {type, payload: _mkInitialState()}; - const newState = reducer(defaultState, action); + const newState = reducer(state, action); assert.deepStrictEqual(newState.app[page].filteredBrowsers, [ - {id: 'firefox', versions: []}, + {id: 'firefox', versions: ['v1', 'v2']}, {id: 'safari', versions: ['23', '11.2']} ]); }); + it('should expand versions only for browsers without explicit versions', () => { + global.window.location = new URL(`${baseUrl}?browser=chrome-phone&browser=safari:23${hash}`); + const state = { + ...defaultState, + browsers: [ + {id: 'chrome-phone', versions: ['phone-124.0', 'phone-101.0']}, + {id: 'safari', versions: ['23', '11.2']} + ] + }; + const action = {type, payload: _mkInitialState()}; + + const newState = reducer(state, action); + + assert.deepStrictEqual(newState.app[page].filteredBrowsers, [ + {id: 'chrome-phone', versions: ['phone-124.0', 'phone-101.0']}, + {id: 'safari', versions: ['23']} + ]); + }); + it('should be able to encode and decode browser ids and versions', () => { const url = appendQuery(baseUrl, { browser: encodeBrowsers([{id: 'safari:some', versions: ['v:1', 'v,2']}]) diff --git a/test/unit/lib/static/new-ui/features/suites/components/SuitesTreeView/utils.ts b/test/unit/lib/static/new-ui/features/suites/components/SuitesTreeView/utils.ts index cc3faddd4..83263eec9 100644 --- a/test/unit/lib/static/new-ui/features/suites/components/SuitesTreeView/utils.ts +++ b/test/unit/lib/static/new-ui/features/suites/components/SuitesTreeView/utils.ts @@ -90,7 +90,8 @@ describe('formatEntityToTreeNodeData', () => { groups: {}, treeViewMode: TreeViewMode.Tree, currentSortDirection: SortDirection.Asc, - currentSortExpression: SORT_BY_NAME + currentSortExpression: SORT_BY_NAME, + browsersList: [] }; describe('when formatting Suite entity', () => { @@ -283,7 +284,8 @@ describe('buildTreeBottomUp', () => { groups: {}, treeViewMode: TreeViewMode.Tree, currentSortDirection: SortDirection.Asc, - currentSortExpression: SORT_BY_NAME + currentSortExpression: SORT_BY_NAME, + browsersList: [] }; let entitiesTree: TreeEntity; @@ -552,7 +554,8 @@ describe('sortTreeNodes', () => { groups: {}, treeViewMode: TreeViewMode.Tree, currentSortDirection: SortDirection.Asc, - currentSortExpression: SORT_BY_NAME + currentSortExpression: SORT_BY_NAME, + browsersList: [] }; let entitiesTree: TreeEntity; diff --git a/test/unit/lib/tests-tree-builder/base.js b/test/unit/lib/tests-tree-builder/base.js index 3997aa300..d1f013226 100644 --- a/test/unit/lib/tests-tree-builder/base.js +++ b/test/unit/lib/tests-tree-builder/base.js @@ -87,7 +87,8 @@ describe('ResultsTreeBuilder', () => { root: true, suitePath: ['s1'], status: SUCCESS, - suiteIds: ['s1 s2'] + suiteIds: ['s1 s2'], + hash: '8ddf878' } ); }); @@ -104,7 +105,8 @@ describe('ResultsTreeBuilder', () => { root: false, suitePath: ['s1', 's2'], status: SUCCESS, - browserIds: ['s1 s2 b1'] + browserIds: ['s1 s2 b1'], + hash: 'c8304d9' } ); });