diff --git a/lib/static/constants/sort-tests.ts b/lib/static/constants/sort-tests.ts index 633e15d9f..ec5e7c2ac 100644 --- a/lib/static/constants/sort-tests.ts +++ b/lib/static/constants/sort-tests.ts @@ -1,5 +1,5 @@ import {SortByExpression, SortType} from '@/static/new-ui/types/store'; export const SORT_BY_NAME: SortByExpression = {id: 'by-name', label: 'Name', type: SortType.ByName}; -export const SORT_BY_FAILED_RETRIES: SortByExpression = {id: 'by-retries', label: 'Failed retries', type: SortType.ByFailedRetries}; +export const SORT_BY_FAILED_RETRIES: SortByExpression = {id: 'by-failed-runs', label: 'Failed runs count', type: SortType.ByFailedRuns}; export const SORT_BY_TESTS_COUNT: SortByExpression = {id: 'by-tests-count', label: 'Tests count', type: SortType.ByTestsCount}; diff --git a/lib/static/modules/reducers/sort-tests.ts b/lib/static/modules/reducers/sort-tests.ts index b0c19b560..1e7722332 100644 --- a/lib/static/modules/reducers/sort-tests.ts +++ b/lib/static/modules/reducers/sort-tests.ts @@ -43,6 +43,7 @@ export default (state: State, action: SomeAction): State => { } case actionNames.GROUP_TESTS_SET_CURRENT_EXPRESSION: { let availableExpressions: SortByExpression[]; + if (action.payload.expressionIds.length > 0) { availableExpressions = [ SORT_BY_NAME, @@ -55,6 +56,7 @@ export default (state: State, action: SomeAction): State => { SORT_BY_FAILED_RETRIES ]; } + return applyStateUpdate(state, { app: { sortTestsData: { diff --git a/lib/static/modules/reducers/suites-page.ts b/lib/static/modules/reducers/suites-page.ts index 009eb8f7d..d7067663a 100644 --- a/lib/static/modules/reducers/suites-page.ts +++ b/lib/static/modules/reducers/suites-page.ts @@ -10,10 +10,11 @@ export default (state: State, action: SomeAction): State => { case actionNames.INIT_STATIC_REPORT: case actionNames.INIT_GUI_REPORT: case actionNames.SUITES_PAGE_SET_TREE_VIEW_MODE: + case actionNames.CHANGE_VIEW_MODE as any: // eslint-disable-line @typescript-eslint/no-explicit-any case actionNames.GROUP_TESTS_SET_CURRENT_EXPRESSION: { const {allTreeNodeIds} = getTreeViewItems(state); - const expandedTreeNodesById: Record = {}; + const expandedTreeNodesById: Record = Object.assign({}, state.ui.suitesPage.expandedTreeNodesById); for (const nodeId of allTreeNodeIds) { expandedTreeNodesById[nodeId] = true; diff --git a/lib/static/new-ui/features/suites/components/SortBySelect/index.tsx b/lib/static/new-ui/features/suites/components/SortBySelect/index.tsx index c01d2de4f..6f9382fab 100644 --- a/lib/static/new-ui/features/suites/components/SortBySelect/index.tsx +++ b/lib/static/new-ui/features/suites/components/SortBySelect/index.tsx @@ -20,7 +20,7 @@ const getSortIcon = (sortByExpression: SortByExpression): ReactNode => { case SortType.ByName: iconData = FontCase; break; - case SortType.ByFailedRetries: + case SortType.ByFailedRuns: iconData = ArrowRotateLeft; break; case SortType.ByTestsCount: diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx index cd036e8e6..6cfb29f18 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx @@ -68,7 +68,10 @@ export const SuitesTreeView = forwardRef 73px in total // Regular items on average take 1 line -> 32px // Providing more precise estimates here greatly improves scrolling performance - return item.entityType === EntityType.Group ? 73 : 32; + const GROUP_ROW_HEIGHT = 73; + const REGULAR_ROW_HEIGHT = 32; + + return item.entityType === EntityType.Group ? GROUP_ROW_HEIGHT : REGULAR_ROW_HEIGHT; }, getItemKey: useCallback((index: number) => list.structure.visibleFlattenIds[index], [list]), overscan: 50 @@ -153,8 +156,8 @@ export const SuitesTreeView = forwardRef; + } interface TreeWeightedSortResult { sortedTreeNodes: TreeNode[]; weight: TreeNodeWeight; } + const createWeight = (value: TreeNodeWeightValue, metadata?: Partial): TreeNodeWeight => ({ + value, + metadata: metadata ?? {} + }); + const extractWeight = (treeNode: TreeNode, childrenWeight?: TreeNodeWeight): TreeNodeWeight => { const notifyOfUnsuccessfulWeightComputation = (): void => { console.warn('Failed to determine suite weight for tree node listed below. Please let us now at ' + NEW_ISSUE_LINK); @@ -215,37 +230,37 @@ export const sortTreeNodes = (entitiesContext: EntitiesContext, treeNodes: TreeN const browserEntities = group.browserIds.flatMap(browserId => browsersState[browserId].shouldBeShown ? [browsers[browserId]] : []); const testsCount = browserEntities.length; - const retriesCount = group.resultIds.filter(resultId => browsersState[results[resultId].parentId].shouldBeShown).length; + const runsCount = group.resultIds.filter(resultId => browsersState[results[resultId].parentId].shouldBeShown).length; if (currentSortExpression.type === SortType.ByTestsCount) { - return [0, testsCount, retriesCount]; + return createWeight([0, testsCount, runsCount], {testsCount, runsCount}); } else if (currentSortExpression.type === SortType.ByName) { - return [treeNode.data.title.join(' '), testsCount, retriesCount]; - } else if (currentSortExpression.type === SortType.ByFailedRetries) { + return createWeight([treeNode.data.title.join(' '), testsCount, runsCount], {testsCount, runsCount}); + } else if (currentSortExpression.type === SortType.ByFailedRuns) { if (!childrenWeight) { notifyOfUnsuccessfulWeightComputation(); - return [0, 0, 0]; + return createWeight([0, 0, 0]); } // For now, we assume there are no nested groups and suite/test weights are always 1 dimensional - return [childrenWeight[0], testsCount, retriesCount]; + return createWeight([childrenWeight.value[0], testsCount, runsCount], Object.assign({}, {testsCount, runsCount}, childrenWeight.metadata)); } break; } case EntityType.Suite: { if (currentSortExpression.type === SortType.ByName) { - return [treeNode.data.title.join(' ')]; - } else if (currentSortExpression.type === SortType.ByFailedRetries) { + return createWeight([treeNode.data.title.join(' ')]); + } else if (currentSortExpression.type === SortType.ByFailedRuns) { if (!childrenWeight) { notifyOfUnsuccessfulWeightComputation(); - return [0]; + return createWeight([0]); } return childrenWeight; } else if (currentSortExpression.type === SortType.ByTestsCount) { if (!childrenWeight) { notifyOfUnsuccessfulWeightComputation(); - return [0]; + return createWeight([0]); } return childrenWeight; @@ -254,46 +269,80 @@ export const sortTreeNodes = (entitiesContext: EntitiesContext, treeNodes: TreeN } case EntityType.Browser: { if (currentSortExpression.type === SortType.ByName) { - return [treeNode.data.title.join(' ')]; - } else if (currentSortExpression.type === SortType.ByFailedRetries) { + return createWeight([treeNode.data.title.join(' ')]); + } else if (currentSortExpression.type === SortType.ByFailedRuns) { const browser = browsers[treeNode.data.entityId]; const groupId = getGroupId(treeNode.data); - return [browser.resultIds.filter(resultId => + const failedRunsCount = browser.resultIds.filter(resultId => (isFailStatus(results[resultId].status) || isErrorStatus(results[resultId].status)) && (!groupId || groups[groupId].resultIds.includes(resultId)) - ).length]; + ).length; + + return createWeight([failedRunsCount], {failedRunsCount}); } else if (currentSortExpression.type === SortType.ByTestsCount) { const browser = browsers[treeNode.data.entityId]; const groupId = getGroupId(treeNode.data); - const retriesCount = groupId ? browser.resultIds.filter(resultId => groups[groupId].resultIds.includes(resultId)).length : browser.resultIds.length; + const runsCount = groupId ? browser.resultIds.filter(resultId => groups[groupId].resultIds.includes(resultId)).length : browser.resultIds.length; - return [1, retriesCount]; + return createWeight([1, runsCount], {runsCount}); } break; } } notifyOfUnsuccessfulWeightComputation(); - return [0]; + return createWeight([0]); }; const aggregateWeights = (weights: TreeNodeWeight[]): TreeNodeWeight => { if (!currentSortExpression || currentSortExpression.type === SortType.ByName) { - return [0]; + return createWeight([0]); } - if (currentSortExpression.type === SortType.ByFailedRetries || currentSortExpression.type === SortType.ByTestsCount) { - return weights.reduce((acc, weight) => { - const newAcc = acc.slice(0); - for (let i = 0; i < weight.length; i++) { - newAcc[i] = (acc[i] ?? 0) + weight[i]; + if (currentSortExpression.type === SortType.ByFailedRuns || currentSortExpression.type === SortType.ByTestsCount) { + return weights.reduce((accWeight, weight) => { + const newAccWeight = createWeight(accWeight.value.slice(0), accWeight.metadata); + for (let i = 0; i < weight.value.length; i++) { + newAccWeight.value[i] = Number(accWeight.value[i] ?? 0) + Number(weight.value[i]); + } + + if (weight.metadata.testsCount !== undefined) { + newAccWeight.metadata.testsCount = (newAccWeight.metadata.testsCount ?? 0) + weight.metadata.testsCount; } - return newAcc; - }, new Array(weights[0]?.length)); + if (weight.metadata.runsCount !== undefined) { + newAccWeight.metadata.runsCount = (newAccWeight.metadata.runsCount ?? 0) + weight.metadata.runsCount; + } + if (weight.metadata.failedRunsCount !== undefined) { + newAccWeight.metadata.failedRunsCount = (newAccWeight.metadata.failedRunsCount ?? 0) + weight.metadata.failedRunsCount; + } + + return newAccWeight; + }, createWeight(new Array(weights[0]?.value?.length))); + } + + return createWeight([0]); + }; + + const generateTagsForWeight = (weight: TreeNodeWeight): string[] => { + const tags: string[] = []; + + const testsCount = weight.metadata.testsCount; + if (testsCount !== undefined) { + tags.push(`${testsCount} ${(testsCount === 1 ? 'test' : 'tests')}`); + } + + const runsCount = weight.metadata.runsCount; + if (runsCount !== undefined) { + tags.push(`${runsCount} ${(runsCount === 1 ? 'run' : 'runs')}`); + } + + const failedRunsCount = weight.metadata.failedRunsCount; + if (failedRunsCount !== undefined) { + tags.push(`${failedRunsCount} ${(failedRunsCount === 1 ? 'failed run' : 'failed runs')}`); } - return [0]; + return tags; }; // Recursive tree sort. At each level of the tree, it does the following: @@ -308,53 +357,34 @@ export const sortTreeNodes = (entitiesContext: EntitiesContext, treeNodes: TreeN if (treeNode.data.entityType === EntityType.Group && treeNode.children?.length) { const sortResult = sortAndGetWeight(treeNode.children); - weights[treeNode.data.id] = extractWeight(treeNode, sortResult.weight); + const weight = extractWeight(treeNode, sortResult.weight); const newTreeNode = Object.assign({}, treeNode, { children: sortResult.sortedTreeNodes }); + newTreeNode.data.tags.push(...generateTagsForWeight(weight)); - const testsCount = weights[treeNode.data.id][1] as number; - const retriesCount = weights[treeNode.data.id][2] as number; - newTreeNode.data.tags.push(`${testsCount} ${(testsCount === 1 ? 'test' : ' tests')}`); - newTreeNode.data.tags.push(`${retriesCount} ${(retriesCount === 1 ? 'retry' : ' retries')}`); - - if (currentSortExpression.type === SortType.ByFailedRetries) { - const failedRetriesCount = weights[treeNode.data.id][0] as number; - newTreeNode.data.tags.push(`${failedRetriesCount} ${(failedRetriesCount === 1 ? 'failed retry' : ' failed retries')}`); - } - + weights[treeNode.data.id] = weight; treeNodesCopy[index] = newTreeNode; } else if (treeNode.data.entityType === EntityType.Suite && treeNode.children?.length) { const sortResult = sortAndGetWeight(treeNode.children); - weights[treeNode.data.id] = extractWeight(treeNode, sortResult.weight); + const weight = extractWeight(treeNode, sortResult.weight); const newTreeNode = Object.assign({}, treeNode, { children: sortResult.sortedTreeNodes }); + newTreeNode.data.tags.push(...generateTagsForWeight(weight)); - const retriesCount = Number(sortResult.weight[0]); - if (currentSortExpression?.type === SortType.ByFailedRetries && retriesCount > 0) { - newTreeNode.data.tags.push(`${retriesCount} ${(retriesCount === 1 ? 'failed retry' : 'failed retries')}`); - } else if (currentSortExpression.type === SortType.ByTestsCount) { - const testsCount = weights[treeNode.data.id][0] as number; - const retriesCount = weights[treeNode.data.id][1] as number; - newTreeNode.data.tags.push(`${testsCount} ${(testsCount === 1 ? 'test' : ' tests')}`); - newTreeNode.data.tags.push(`${retriesCount} ${(retriesCount === 1 ? 'retry' : ' retries')}`); - } - + weights[treeNode.data.id] = weight; treeNodesCopy[index] = newTreeNode; } else if (treeNode.data.entityType === EntityType.Browser) { - const newTreeNode = Object.assign({}, treeNode); + const weight = extractWeight(treeNode); - weights[treeNode.data.id] = extractWeight(treeNode); - - const retriesCount = weights[treeNode.data.id][0] as number; - if (currentSortExpression?.type === SortType.ByFailedRetries && retriesCount > 0) { - newTreeNode.data.tags.push(`${retriesCount} ${(retriesCount === 1 ? 'failed retry' : 'failed retries')}`); - } + const newTreeNode = Object.assign({}, treeNode); + newTreeNode.data.tags.push(...generateTagsForWeight(weight)); + weights[treeNode.data.id] = weight; treeNodesCopy[index] = newTreeNode; } }); @@ -362,9 +392,9 @@ export const sortTreeNodes = (entitiesContext: EntitiesContext, treeNodes: TreeN const sortedTreeNodes = treeNodesCopy.sort((a, b): number => { const direction = currentSortDirection === SortDirection.Desc ? -1 : 1; - for (let i = 0; i < weights[a.data.id].length; i++) { - const aWeight = weights[a.data.id][i]; - const bWeight = weights[b.data.id][i]; + for (let i = 0; i < weights[a.data.id].value.length; i++) { + const aWeight = weights[a.data.id].value[i]; + const bWeight = weights[b.data.id].value[i]; if (aWeight === bWeight) { continue; } diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css b/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css index 10d5c2472..7b4562744 100644 --- a/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css +++ b/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css @@ -53,12 +53,12 @@ padding: 0 4px; } -:global(.error) .tag { +:global(.error-tree-node) .tag { background: var(--g-color-private-red-50); color: var(--g-color-private-red-500-solid); } -:global(.current) .tag { +:global(.current-tree-node) .tag { background-color: rgba(255, 255, 255, .15); color: rgba(255, 255, 255, .7); } diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 4a34ee7cf..80746980f 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -211,7 +211,7 @@ export type GroupByExpression = GroupByMetaExpression | GroupByErrorExpression; export enum SortType { ByName, - ByFailedRetries, + ByFailedRuns, ByTestsCount }