From afec5d7374cbdad951350be124175b59b70894ac Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 18 Aug 2026 15:48:33 +0900 Subject: [PATCH] refactor(ui): remove explorer `queueMicrotask` logic (#10980) Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: OpenCode --- .../ui/client/composables/client/index.ts | 5 - .../client/composables/explorer/collector.ts | 91 ++++++--------- .../ui/client/composables/explorer/tree.ts | 106 +++++++----------- packages/ui/explorer.md | 2 +- 4 files changed, 75 insertions(+), 129 deletions(-) diff --git a/packages/ui/client/composables/client/index.ts b/packages/ui/client/composables/client/index.ts index ae90da9a0..7ab6f8280 100644 --- a/packages/ui/client/composables/client/index.ts +++ b/packages/ui/client/composables/client/index.ts @@ -68,11 +68,6 @@ function createVitestClient(): VitestClient { }, onFinished(_files, errors, _coverage, executionTime) { explorerTree.endRun(executionTime) - // don't change the testRunState.value here: - // - when saving the file in the codemirror requires explorer tree endRun to finish (multiple microtasks) - // - if we change here the state before the tasks states are updated, the cursor position will be lost - // - line moved to composables/explorer/collector.ts::refreshExplorer after calling updateRunningTodoTests - // testRunState.value = 'idle' unhandledErrors.value = (errors || []).map(parseError) }, onFinishedReportCoverage() { diff --git a/packages/ui/client/composables/explorer/collector.ts b/packages/ui/client/composables/explorer/collector.ts index 72cf36fe1..a2ad8fb19 100644 --- a/packages/ui/client/composables/explorer/collector.ts +++ b/packages/ui/client/composables/explorer/collector.ts @@ -47,24 +47,22 @@ export function runLoadFiles( } export function preparePendingTasks(packs: TaskResultPack[]) { - queueMicrotask(() => { - const pending = explorerTree.pendingTasks - const idMap = client.state.idMap - for (const pack of packs) { - const result = pack[1] - if (result) { - const task = idMap.get(pack[0]) - if (task) { - let file = pending.get(task.file.id) - if (!file) { - file = new Set() - pending.set(task.file.id, file) - } - file.add(task.id) + const pending = explorerTree.pendingTasks + const idMap = client.state.idMap + for (const pack of packs) { + const result = pack[1] + if (result) { + const task = idMap.get(pack[0]) + if (task) { + let file = pending.get(task.file.id) + if (!file) { + file = new Set() + pending.set(task.file.id, file) } + file.add(task.id) } } - }) + } } export function recordTestArtifact( @@ -104,31 +102,23 @@ export function runCollect( } const collect = !start - queueMicrotask(() => { - if (end) { - traverseFiles(collect) - } - else { - traverseReceivedFiles(collect) - } - }) + if (end) { + traverseFiles(collect) + } + else { + traverseReceivedFiles(collect) + } - queueMicrotask(() => { - collectData(summary, executionTime) - }) + collectData(summary, executionTime) - queueMicrotask(() => { - if (end) { - summary.failedSnapshot = uiFiles.value && hasFailedSnapshot( - uiFiles.value.map(f => findById(f.id)!), - ) - summary.failedSnapshotEnabled = true - } - }) + if (end) { + summary.failedSnapshot = uiFiles.value && hasFailedSnapshot( + uiFiles.value.map(f => findById(f.id)!), + ) + summary.failedSnapshotEnabled = true + } - queueMicrotask(() => { - doRunFilter(search, filter, end) - }) + doRunFilter(search, filter, end) } function* collectRunningTodoTests() { @@ -229,32 +219,23 @@ function doRunFilter( const ids = new Set(openedTreeItems.value) const applyExpandNodes = (ids.size > 0 && expandAll === false) || resetExpandAll - // refresh explorer - queueMicrotask(() => { - refreshExplorer(search, filter, end) - }) + refreshExplorer(search, filter, end) // initialize the explorer if (!initialized.value) { - queueMicrotask(() => { - if (uiEntries.value.length || end) { - initialized.value = true - } - }) + if (uiEntries.value.length || end) { + initialized.value = true + } } if (applyExpandNodes) { // expand all nodes - queueMicrotask(() => { - expandNodesOnEndRun(ids, end) - if (resetExpandAll) { - treeFilter.value.expandAll = false - } - }) + expandNodesOnEndRun(ids, end) + if (resetExpandAll) { + treeFilter.value.expandAll = false + } // refresh explorer - queueMicrotask(() => { - refreshExplorer(search, filter, end) - }) + refreshExplorer(search, filter, end) } } diff --git a/packages/ui/client/composables/explorer/tree.ts b/packages/ui/client/composables/explorer/tree.ts index b8a8ae663..f509274fa 100644 --- a/packages/ui/client/composables/explorer/tree.ts +++ b/packages/ui/client/composables/explorer/tree.ts @@ -78,6 +78,7 @@ export class ExplorerTree { startRun() { this.startTime = performance.now() + // TODO: Replace this bootstrap/run fallback with explicit lifecycle handling. this.resumeEndRunId = setTimeout(() => this.endRun(), this.resumeEndTimeout) this.collect(true, false) } @@ -87,7 +88,7 @@ export class ExplorerTree { if (!this.onTaskUpdateCalled) { clearTimeout(this.resumeEndRunId) this.onTaskUpdateCalled = true - this.collect(true, false, false) + this.collect(true, false) this.rafCollector.resume() } } @@ -97,7 +98,7 @@ export class ExplorerTree { if (!this.onTaskUpdateCalled) { clearTimeout(this.resumeEndRunId) this.onTaskUpdateCalled = true - this.collect(true, false, false) + this.collect(true, false) this.rafCollector.resume() } } @@ -139,43 +140,22 @@ export class ExplorerTree { * * @param start Reset summary counters before updates when true; skip the reset when false. * @param end Traverse every file and finalize the run when true; process only pending files when false. - * @param task Invoke the collector in a microtask when true; invoke it immediately when false. */ - private collect(start: boolean, end: boolean, task = true) { - if (task) { - queueMicrotask(() => { - runCollect( - start, - end, - this.summary, - searchMatcher.value.matcher, - { - failed: filter.failed, - success: filter.success, - skipped: filter.skipped, - slow: filter.slow, - onlyTests: filter.onlyTests, - }, - end ? this.executionTime : performance.now() - this.startTime, - ) - }) - } - else { - runCollect( - start, - end, - this.summary, - searchMatcher.value.matcher, - { - failed: filter.failed, - success: filter.success, - skipped: filter.skipped, - slow: filter.slow, - onlyTests: filter.onlyTests, - }, - end ? this.executionTime : performance.now() - this.startTime, - ) - } + private collect(start: boolean, end: boolean) { + runCollect( + start, + end, + this.summary, + searchMatcher.value.matcher, + { + failed: filter.failed, + success: filter.success, + skipped: filter.skipped, + slow: filter.slow, + onlyTests: filter.onlyTests, + }, + end ? this.executionTime : performance.now() - this.startTime, + ) } collectTestsTotal( @@ -194,50 +174,40 @@ export class ExplorerTree { } collapseNode(id: string) { - queueMicrotask(() => { - runCollapseNode(id) - }) + runCollapseNode(id) } expandNode(id: string) { - queueMicrotask(() => { - runExpandNode(id, searchMatcher.value.matcher, { - failed: filter.failed, - success: filter.success, - skipped: filter.skipped, - slow: filter.slow, - onlyTests: filter.onlyTests, - }) + runExpandNode(id, searchMatcher.value.matcher, { + failed: filter.failed, + success: filter.success, + skipped: filter.skipped, + slow: filter.slow, + onlyTests: filter.onlyTests, }) } collapseAllNodes() { - queueMicrotask(() => { - runCollapseAllTask() - }) + runCollapseAllTask() } expandAllNodes() { - queueMicrotask(() => { - runExpandAll(searchMatcher.value.matcher, { - failed: filter.failed, - success: filter.success, - skipped: filter.skipped, - slow: filter.slow, - onlyTests: filter.onlyTests, - }) + runExpandAll(searchMatcher.value.matcher, { + failed: filter.failed, + success: filter.success, + skipped: filter.skipped, + slow: filter.slow, + onlyTests: filter.onlyTests, }) } filterNodes() { - queueMicrotask(() => { - runFilter(searchMatcher.value.matcher, { - failed: filter.failed, - success: filter.success, - skipped: filter.skipped, - slow: filter.slow, - onlyTests: filter.onlyTests, - }) + runFilter(searchMatcher.value.matcher, { + failed: filter.failed, + success: filter.success, + skipped: filter.skipped, + slow: filter.slow, + onlyTests: filter.onlyTests, }) } } diff --git a/packages/ui/explorer.md b/packages/ui/explorer.md index 5e47cf77b..02f3e1286 100644 --- a/packages/ui/explorer.md +++ b/packages/ui/explorer.md @@ -14,7 +14,7 @@ The explorer will not use the `idsMap` and `filesMap` directly from the `ws-clie - [nodes](client/composables/explorer/tree.ts): changes in the `ws-client` state will be mapped here with tree structure. - [uiEntries](client/composables/explorer/state.ts): a shallow ref to represent the flat tree entries in the ui, the logic will use `nodes` to build it. -Any operation in the explorer using `queueMicrotask` to avoid blocking the main thread, and any operation on list/map using `generators`. +Explorer updates are throttled with `requestAnimationFrame`, and operations on lists and maps use generators. The explorer logic splits the actions in three main parts: - collecting tasks while running the tests -- 2.51.2