UNPKG

@allurereport/web-awesome

Version:

The static files for Allure Awesome Report

372 lines (291 loc) 9.45 kB
import { flattenVisibleTree, moveFocus, router, type FlatTreeNode, type MoveDirection, type MoveFocusResult, type SubtreeToggleState, } from "@allurereport/web-commons"; import type { RecursiveTree } from "@allurereport/web-components/global"; import { computed, effect, signal } from "@preact/signals"; import { statsByEnvStore } from "@/stores"; import { collapsedEnvironments, currentEnvironment, environmentsStore } from "@/stores/env"; import { isSplitMode } from "@/stores/layout"; import { rootTabRoute, testResultRoute } from "@/stores/router"; import { currentSection } from "@/stores/sections"; import { currentTrId } from "@/stores/testResult"; import { collapsedTrees, expandedTrees, filteredTree, isTreeOpened, noTests, noTestsFound, setTreeOpened, } from "@/stores/tree"; export type ActivePane = "tree" | "testResult"; export const activePane = signal<ActivePane>("tree"); export const treeFocusId = signal<string | undefined>(undefined); export const hotkeysHelpOpen = signal(false); export const pendingVimKey = signal<string | null>(null); /** Last subtree cycle action per focused group/step (matches header chevron button memory). */ export const lastSubtreeToggleByScope = signal<Record<string, SubtreeToggleState>>({}); /** When true, the next tree focus scroll snaps the list pane to scrollTop 0 (Home / gg / zt). */ export const treeScrollPaneToTopPending = signal(false); export const focusTreePane = () => { activePane.value = "tree"; }; export const focusTestResultPane = () => { activePane.value = "testResult"; }; effect(() => { document.documentElement.setAttribute("data-active-pane", activePane.value); }); export const toggleHotkeysHelp = () => { hotkeysHelpOpen.value = !hotkeysHelpOpen.value; }; export const isSearchInput = (element: Element | null): element is HTMLInputElement => element instanceof HTMLInputElement && (element.name === "search" || element.dataset.testid === "search-input"); const releaseDetachedFocus = () => { const active = document.activeElement; if (isSearchInput(active)) { return; } if (active instanceof HTMLElement && active !== document.body) { active.blur(); } }; /** Main report tabs (Results, Categories, …) are on screen and hotkeys may switch them. */ export const isReportRootTabsContext = (): boolean => { if (currentSection.value !== "default") { return false; } if (isSplitMode.value) { return true; } if (testResultRoute.value.matches) { return false; } return true; }; /** Test-results tree is visible (Results tab or split left pane), not Categories/other tabs. */ export const isReportResultsTreeVisible = (): boolean => { if (currentSection.value !== "default") { return false; } if (testResultRoute.value.matches && !isSplitMode.value) { return false; } if (rootTabRoute.value.matches) { const rootTab = rootTabRoute.value.params.rootTab; return rootTab === "results"; } return true; }; export const isTreeNavigationContext = (): boolean => { if (!isReportResultsTreeVisible()) { return false; } if (isSplitMode.value) { return activePane.value === "tree"; } return true; }; export const isTestResultHotkeysContext = (): boolean => { if (!currentTrId.value) { return false; } if (isSplitMode.value) { return activePane.value === "testResult"; } return testResultRoute.value.matches || Boolean(rootTabRoute.value.params.testResultId); }; export const syncKeyboardStateFromRoute = () => { pendingVimKey.value = null; releaseDetachedFocus(); if (currentSection.value !== "default") { return; } ensureTreeFocusId(); const hasTest = Boolean(currentTrId.value); const split = isSplitMode.value; const fullPageTest = testResultRoute.value.matches && !split; if (fullPageTest) { focusTestResultPane(); return; } if (split && !hasTest) { focusTreePane(); return; } if (!split && !hasTest) { focusTreePane(); } }; export const isHotkeyScopeActive = (scope: "global" | "tree" | "testResult"): boolean => { if (isSplitMode.value) { return scope === "global" || scope === activePane.value; } if (scope === "tree") { return isReportRootTabsContext() && (isSplitMode.value ? activePane.value === "tree" : true); } if (scope === "testResult") { return isTestResultHotkeysContext(); } return true; }; const buildEnvSections = () => { const envs = environmentsStore.value.data; return Object.entries(filteredTree.value) .map(([envId, tree]) => { const stats = statsByEnvStore.value.data[envId]; if ((stats?.total ?? 0) === 0) { return null; } return { id: envId, opened: !collapsedEnvironments.value.includes(envId), tree: tree as RecursiveTree, statistic: stats, }; }) .filter((section): section is NonNullable<typeof section> => section !== null); }; const flattenTreeForKeyboard = (options: { tree: Parameters<typeof flattenVisibleTree>[0]["tree"]; isRoot?: boolean; rootStatistic?: Parameters<typeof flattenVisibleTree>[0]["rootStatistic"]; envSections?: Parameters<typeof flattenVisibleTree>[0]["envSections"]; }) => { collapsedTrees.value; expandedTrees.value; return flattenVisibleTree({ collapsedTrees: collapsedTrees.value, isGroupOpened: (scopedNodeId, openedByDefault) => isTreeOpened(scopedNodeId, openedByDefault), ...options, }); }; export const flatTree = computed((): FlatTreeNode[] => { if (noTests.value || noTestsFound.value) { return []; } const envs = environmentsStore.value.data; const trees = filteredTree.value; if (envs.length === 1) { const soleId = envs[0]!.id; const tree = trees[soleId]; if (!tree) { return []; } return flattenTreeForKeyboard({ tree, isRoot: true, rootStatistic: statsByEnvStore.value.data[soleId], }); } const currentTree = currentEnvironment.value ? trees[currentEnvironment.value] : undefined; if (currentTree) { return flattenTreeForKeyboard({ tree: currentTree, isRoot: true, rootStatistic: statsByEnvStore.value.data[currentEnvironment.value], }); } return flattenTreeForKeyboard({ envSections: buildEnvSections(), }); }); export const getFlatTreeNode = (id: string | undefined) => flatTree.value.find((node) => node.id === id); export const moveTreeFocus = (direction: MoveDirection): MoveFocusResult => { return moveFocus(flatTree.value, treeFocusId.value, direction); }; export const setTreeFocusId = (id: string | undefined) => { treeFocusId.value = id; }; export const ensureTreeFocusId = () => { const flat = flatTree.value; if (flat.length === 0) { treeFocusId.value = undefined; return; } // Use peek() so that changes to treeFocusId itself do not re-trigger this effect — // treeFocusId is the *output* here, not the trigger. const currentId = treeFocusId.peek(); const currentExists = currentId ? flat.some((node) => node.id === currentId) : false; if (currentExists) { return; } const routeId = currentTrId.value; const routeNode = routeId ? flat.find((node) => node.testResultId === routeId || node.id === routeId) : undefined; const firstLeaf = flat.find((node) => node.kind === "leaf"); treeFocusId.value = routeNode?.id ?? firstLeaf?.id ?? flat[0]?.id; }; effect(() => { flatTree.value; ensureTreeFocusId(); }); effect(() => { currentSection.value; currentTrId.value; router.value.path; isSplitMode.value; syncKeyboardStateFromRoute(); }); const expandPathToLeaf = (tree: RecursiveTree, targetNodeId: string, prefix: string | undefined): boolean => { for (const leaf of tree.leaves) { if (leaf.nodeId === targetNodeId) { return true; } } for (const sub of tree.trees) { if (expandPathToLeaf(sub, targetNodeId, prefix)) { const scopedId = prefix ? `${prefix}${sub.nodeId}` : sub.nodeId; const openedByDefault = !sub.statistic || Boolean(sub.statistic.failed || sub.statistic.broken); const isOpen = openedByDefault ? !collapsedTrees.peek().has(scopedId) : expandedTrees.peek().has(scopedId); if (!isOpen) { setTreeOpened(scopedId, true, openedByDefault); } return true; } } return false; }; const expandAndFocusCurrentTest = () => { const testResultId = currentTrId.peek(); if (!testResultId) { return; } const flat = flatTree.peek(); const existing = flat.find((n) => n.kind === "leaf" && n.testResultId === testResultId); if (existing) { treeFocusId.value = existing.id; return; } const envs = environmentsStore.peek().data ?? []; const trees = filteredTree.peek(); const curEnv = currentEnvironment.peek(); const usePrefix = envs.length > 1 && !curEnv; for (const env of envs) { const envTree = trees[env.id]; if (!envTree) { continue; } const prefix = usePrefix ? `${env.id}:` : undefined; if (expandPathToLeaf(envTree, testResultId, prefix)) { treeFocusId.value = prefix ? `${prefix}${testResultId}` : testResultId; return; } } }; let prevIsSplitMode = isSplitMode.peek(); effect(() => { const nowSplit = isSplitMode.value; try { if (nowSplit && !prevIsSplitMode) { expandAndFocusCurrentTest(); } } finally { prevIsSplitMode = nowSplit; } });