Skip to content
12 changes: 12 additions & 0 deletions packages/ui/client/composables/explorer/expand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ function expandAllNodes(nodes: UITaskTreeNode[], updateState: boolean) {
}
}

/**
* Build the complete next explorer entry list by emitting an expanded node and its filtered
* subtree at the node's current position.
*
* `children` contains only entries produced by filtering the expanded subtree. This function
* walks the complete current `uiEntries`. When it reaches `node`, it emits the node unless
* `children` already contains it, followed by the children. Unrelated entries are copied
* unchanged, while existing entries with the same IDs are skipped to avoid duplicates.
*
* TODO: Make this a pure splice over explicit entries and keep expansion state changes in
* `runExpandNode`.
*/
function* collectExpandedNode(
node: UITaskTreeNode,
children: Set<UITaskTreeNode>,
Expand Down
11 changes: 9 additions & 2 deletions packages/ui/client/composables/explorer/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ export function* filterNode(
search: SearchMatcher,
filter: Filter,
) {
// Parent node IDs that match or contain a matching descendant.
const treeNodes = new Set<string>()

// Direct match state of visited parents, used to include their immediate children.
const parentsMap = new Map<string, boolean>()
// Visited nodes with their effective match state, consumed bottom-up by filterParents.
const list: FilterResult[] = []

// Matching file ID, or owning file ID when filtering an expanded subtree.
let fileId: string | undefined

if (filter.onlyTests) {
Expand Down Expand Up @@ -92,9 +94,14 @@ export function* filterNode(
// when expanding a non-file node
if (!fileId && !isFileNode(node) && 'fileId' in node) {
fileId = node.fileId as string
const file = explorerTree.nodes.get(fileId)
if (file && matcher(file, search, filter)) {
treeNodes.add(fileId)
}
Comment on lines +97 to +100

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is the three liners.

}
}

// TODO: Let filterParents own this traversal-local state.
const filesToShow = new Set<string>()

const entries = [...filterParents(
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/client/composables/explorer/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { createTagsFilter } from '../../../../vitest/src/runtime/runner/utils/ta
import { config } from '../client'
import { explorerTree } from './index'

/** All root file nodes, independent of the current filter and expansion state. */
export const uiFiles = shallowRef<FileTreeNode[]>([])
/** Flattened rows currently rendered by the explorer's virtual scroller. */
export const uiEntries = shallowRef<UITaskTreeNode[]>([])
/** Persisted node IDs used to restore individually expanded branches. */
export const openedTreeItems = useLocalStorage<string[]>(
'vitest-ui_task-tree-opened',
[],
Expand Down
4 changes: 4 additions & 0 deletions test/ui/test/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,10 @@ async function testError(page: Page) {
async function testSuiteReport(page: Page) {
const report = page.getByTestId('report')

await expect(getExplorerItem(page, 'sample.test.ts')).toBeVisible()
await page.getByPlaceholder('Search...').fill('suite-report')
await expect(getExplorerItem(page, 'sample.test.ts')).toHaveCount(0)
Comment on lines +504 to +506

@hi-ogawa hi-ogawa Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets up search to stabilize the tree and later we have assertion of suite collapse/expand:

  // test that the suite can be collapsed and expanded
  await expect(getExplorerItem(page, 'successful child')).toBeVisible()
  await successfulSuite.getByRole('button', { name: 'Collapse successful suite', exact: true }).click()
  await expect(getExplorerItem(page, 'successful child')).not.toBeVisible()
  await successfulSuite.getByRole('button', { name: 'Expand successful suite', exact: true }).click()
  await expect(getExplorerItem(page, 'successful child')).toBeVisible()


await getExplorerItem(page, 'suite-report.test.ts').click()
await expect(report).toContainText('before-all-marker')
await expect(report).toContainText('direct-child-marker')
Expand Down
Loading