🐛 fix(vscode): 切走标签再回来,调用树棵数不用重算,开着的弹框和选中的入口照旧

This commit is contained in:
cruldra committed 2026-10-13 11:00:06 +11:00
1 parent 6cf053dcd0
commit 32018e71f1
9 files changed
+542 -110

No files matched your search

@@ -0,0 +1,158 @@
// @vitest-environment happy-dom
import type { Root } from 'react-dom/client'
import type { BlockView, CallTree, DocumentMessage, ExtensionToWebview, WebviewToExtension } from './types'
import type { ViewState, ViewStateStore } from './viewState'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { App } from './App'
import { createViewStateStore } from './viewState'
/** 扩展端那头:页面发出去的消息、页面挂着的收消息回调,以及 webview 自己存的状态(标签页切回来页面重新加载时还在) */
const host = vi.hoisted(() => ({
posted: [] as unknown[],
listeners: new Set<(msg: unknown) => void>(),
store: undefined as ViewStateStore | undefined,
}))
vi.mock('./vscode', () => ({
postMessage: (msg: WebviewToExtension) => host.posted.push(msg),
onMessage: (handler: (msg: unknown) => void) => {
host.listeners.add(handler)
return () => host.listeners.delete(handler)
},
viewState: {
get: () => host.store!.get(),
update: (patch: Partial<ViewState>) => host.store!.update(patch),
},
}))
let container: HTMLDivElement
let root: Root
let saved: ViewState | undefined
/** 页面(重新)加载:从 webview 存的状态起步 */
async function load(): Promise<void> {
host.store = createViewStateStore({
getState: () => saved,
setState: (state) => {
saved = JSON.parse(JSON.stringify(state)) as ViewState
},
})
host.posted.length = 0
container = document.createElement('div')
document.body.append(container)
root = createRoot(container)
await act(async () => root.render(<App />))
}
async function unload(): Promise<void> {
await act(async () => root.unmount())
container.remove()
host.listeners.clear()
}
async function deliver(msg: ExtensionToWebview): Promise<void> {
await act(async () => {
for (const listener of host.listeners)
listener(msg)
})
}
function block(id: string, anchor: string): BlockView {
return {
id,
anchor,
kind: 'function',
callTree: {},
startLine: 0,
endLine: 1,
code: '',
codeTokens: [],
attributes: { kind: 'function' },
concepts: [{ id: 'function', label: 'function', icon: 'braces' }],
children: [],
translation: [{ text: anchor, role: 'plain' }],
}
}
function documentOf(version: number, blocks: BlockView[]): DocumentMessage {
return { type: 'document', version, languageId: 'python', blocks, attributes: [], translators: [], translatorId: '', names: [] }
}
function tree(id: string, name: string): CallTree {
return { id, name, hits: 1, root: { name, kind: 'function', where: `${name}.py:1`, jump: { uri: `file:///${name}.py`, line: 0, character: 0 }, current: false, children: [] } }
}
const TREES = [tree('read', '读取用户资料'), tree('night', '夜间对账')]
const dialog = () => document.body.querySelector('[role="dialog"]')
const selectedTab = () => document.body.querySelector('[role="tab"][aria-selected="true"]')?.textContent?.replace(/\s+/g, '')
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
saved = undefined
})
afterEach(async () => {
await unload()
vi.unstubAllGlobals()
})
describe('标签页切走再切回来,调用树弹框照旧开着', () => {
it('弹框开着、选了第二个入口:页面重新加载拿到文档后重新打开那块、要它的树,停回第二个入口;关掉后再加载就不开了', async () => {
await load()
await deliver(documentOf(3, [block('0', 'mask_phone')]))
await act(async () => document.body.querySelector<HTMLButtonElement>('[data-call-tree="mask_phone"]')!.click())
expect(host.posted).toContainEqual({ type: 'openCallTree', version: 3, anchor: 'mask_phone' })
await deliver({ type: 'callTree', version: 3, anchor: 'mask_phone', name: 'mask_phone', where: 'utils/phone.py:28', trees: TREES })
await act(async () => document.body.querySelectorAll<HTMLButtonElement>('[role="tab"]')[1].click())
expect(saved?.callTree).toEqual({ anchor: 'mask_phone', entry: 1 })
await unload()
await load()
expect(dialog()).toBeNull()
await deliver(documentOf(3, [block('0', 'mask_phone')]))
expect(host.posted).toContainEqual({ type: 'openCallTree', version: 3, anchor: 'mask_phone' })
expect(dialog()!.textContent).toContain('正在查调用关系')
await deliver({ type: 'callTree', version: 3, anchor: 'mask_phone', name: 'mask_phone', where: 'utils/phone.py:28', trees: TREES })
expect(selectedTab()).toBe('夜间对账1处')
await act(async () => dialog()!.querySelector<HTMLButtonElement>('[aria-label="关闭"]')!.click())
expect(saved?.callTree).toBeUndefined()
await unload()
await load()
await deliver(documentOf(3, [block('0', 'mask_phone')]))
expect(dialog()).toBeNull()
expect(host.posted).not.toContainEqual(expect.objectContaining({ type: 'openCallTree' }))
})
it('页面重新加载前切走、文档还没到又切走一次,记着的弹框还在', async () => {
saved = { editing: [], folds: {}, callTree: { anchor: 'mask_phone', entry: 1 } }
await load()
await unload()
await load()
await deliver(documentOf(3, [block('0', 'mask_phone')]))
expect(host.posted).toContainEqual({ type: 'openCallTree', version: 3, anchor: 'mask_phone' })
expect(dialog()).not.toBeNull()
})
it('那块已经不在了:不开弹框、不要树,记录丢掉', async () => {
saved = { editing: [], folds: {}, callTree: { anchor: 'gone', entry: 0 } }
await load()
await deliver(documentOf(4, [block('0', 'mask_phone')]))
expect(dialog()).toBeNull()
expect(host.posted).not.toContainEqual(expect.objectContaining({ type: 'openCallTree' }))
expect(saved?.callTree).toBeUndefined()
})
it('弹框开着时文档换了一版:照旧关掉,记录跟着清掉', async () => {
saved = { editing: [], folds: {}, callTree: { anchor: 'mask_phone', entry: 0 } }
await load()
await deliver(documentOf(5, [block('0', 'mask_phone')]))
expect(dialog()).not.toBeNull()
await deliver(documentOf(6, [block('0', 'mask_phone')]))
expect(dialog()).toBeNull()
expect(saved?.callTree).toBeUndefined()
})
})