✨ feat(vscode): 代码阅读的函数和类可以看调用树
This commit is contained in:
1 parent
8dc572bf94
commit
645542eb5c
17 files changed
+1679
-5
No files matched your search
@@ -0,0 +1,225 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
import type { Root } from 'react-dom/client'
|
||||
import type { BlockTreeState } from './BlockTree'
|
||||
import type { BlockView, CallTree } from './types'
|
||||
import { act, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockLevel } from './BlockTree'
|
||||
import { CallTreeDialog } from './CallTreeDialog'
|
||||
|
||||
vi.mock('./useOccurrences', async () => {
|
||||
const { createContext } = await import('react')
|
||||
return { HighlightContext: createContext(undefined) }
|
||||
})
|
||||
vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } }))
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
||||
container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
container.remove()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
async function render(element: ReactElement): Promise<void> {
|
||||
await act(async () => root.render(element))
|
||||
}
|
||||
|
||||
function block(kind: string, id = kind): BlockView {
|
||||
return {
|
||||
id,
|
||||
anchor: id,
|
||||
kind,
|
||||
startLine: 0,
|
||||
endLine: 1,
|
||||
code: '',
|
||||
codeTokens: [],
|
||||
attributes: { kind },
|
||||
concepts: [{ id: kind, label: kind, icon: kind === 'class' ? 'box' : 'braces' }],
|
||||
children: [],
|
||||
translation: [{ text: id, role: 'plain' }],
|
||||
}
|
||||
}
|
||||
|
||||
function tree(callTree?: BlockTreeState['callTree'], edit = true): BlockTreeState {
|
||||
return {
|
||||
...(edit && {
|
||||
edit: {
|
||||
version: 1,
|
||||
editing: new Set<string>(),
|
||||
focusId: null,
|
||||
onFocused: () => {},
|
||||
onToggleEdit: () => {},
|
||||
rewrites: new Map(),
|
||||
onRewriteComment: () => {},
|
||||
onStopRewrite: () => {},
|
||||
},
|
||||
}),
|
||||
collapsed: new Set(),
|
||||
hidden: new Set(),
|
||||
temporary: new Set(),
|
||||
covered: new Map(),
|
||||
kindName: kind => kind,
|
||||
onToggleFold: () => {},
|
||||
onSetFolds: () => {},
|
||||
onOpenRun: () => {},
|
||||
...(callTree && { callTree }),
|
||||
}
|
||||
}
|
||||
|
||||
function node(name: string, extra: Partial<CallTree['root']> = {}): CallTree['root'] {
|
||||
return { name, kind: 'function', where: `${name}.py:1`, current: false, children: [], ...extra }
|
||||
}
|
||||
|
||||
const ONE: CallTree = {
|
||||
id: 'one',
|
||||
name: '夜间对账',
|
||||
hits: 1,
|
||||
root: node('夜间对账', { detail: '入口', children: [node('mask_phone', { current: true, where: 'utils/phone.py:28' })] }),
|
||||
}
|
||||
|
||||
const MANY: CallTree[] = [
|
||||
{
|
||||
id: 'read',
|
||||
name: '读取用户资料',
|
||||
hits: 2,
|
||||
root: node('读取用户资料', {
|
||||
detail: '入口',
|
||||
otherCalls: 2,
|
||||
children: [node('mask_phone', { current: true }), node('mask_phone', { current: true })],
|
||||
}),
|
||||
},
|
||||
ONE,
|
||||
]
|
||||
|
||||
describe('调用树弹框', () => {
|
||||
it('棵数为 0 的函数没有按钮,类有,语句没有;只读的树一排按钮都没有', async () => {
|
||||
const counts = new Map([['function', 0], ['class', 2], ['statement', 4]])
|
||||
await render(<BlockLevel blocks={[block('function'), block('class'), block('statement')]} tree={tree({ counts, onOpen: () => {} })} />)
|
||||
expect(container.querySelector('[data-call-tree="function"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="statement"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="class"]')).not.toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="class"]')!.textContent).toBe('2')
|
||||
|
||||
await render(<BlockLevel blocks={[block('function')]} tree={tree({ counts: new Map([['function', 3]]), onOpen: () => {} }, false)} />)
|
||||
expect(container.querySelector('[aria-label="块操作"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="function"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('弹框打开时原来的卡片还在,关掉之后弹框没了,焦点回到那个按钮', async () => {
|
||||
const card = block('function', 'mask')
|
||||
function Page(): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<BlockLevel
|
||||
blocks={[card]}
|
||||
tree={tree({ counts: new Map([['mask', 3]]), openAnchor: open ? 'mask' : undefined, onOpen: () => setOpen(true) })}
|
||||
/>
|
||||
{open && (
|
||||
<CallTreeDialog
|
||||
kind="function"
|
||||
name="mask_phone"
|
||||
where="utils/phone.py:28"
|
||||
status="ready"
|
||||
trees={[ONE]}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
await render(<Page />)
|
||||
const button = container.querySelector<HTMLButtonElement>('[data-call-tree="mask"]')!
|
||||
await act(async () => button.click())
|
||||
expect(container.querySelector('[data-id="mask"]')).not.toBeNull()
|
||||
const dialog = document.body.querySelector('[role="dialog"]')
|
||||
expect(dialog).not.toBeNull()
|
||||
expect(dialog!.textContent).toContain('调用树')
|
||||
expect(dialog!.textContent).toContain('mask_phone')
|
||||
expect(dialog!.textContent).toContain('utils/phone.py:28')
|
||||
expect(dialog!.textContent).not.toContain('读取用户资料')
|
||||
expect(dialog!.querySelector('[role="tablist"]')).toBeNull()
|
||||
|
||||
await act(async () => dialog!.querySelector<HTMLButtonElement>('[aria-label="关闭"]')!.click())
|
||||
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
|
||||
expect(container.querySelector('[data-id="mask"]')).not.toBeNull()
|
||||
expect(document.activeElement).toBe(container.querySelector('[data-call-tree="mask"]'))
|
||||
})
|
||||
|
||||
it('点遮罩或按 Esc 关掉弹框', async () => {
|
||||
function Page(): ReactElement {
|
||||
const [open, setOpen] = useState(true)
|
||||
return open
|
||||
? (
|
||||
<>
|
||||
<div data-id="mask">卡片</div>
|
||||
<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={[ONE]} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
)
|
||||
: <div data-id="mask">卡片</div>
|
||||
}
|
||||
await render(<Page key="overlay" />)
|
||||
const overlay = document.body.querySelector<HTMLElement>('.cr-call-overlay')!
|
||||
await act(async () => overlay.dispatchEvent(new MouseEvent('click', { bubbles: true })))
|
||||
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
|
||||
|
||||
await render(<Page key="esc" />)
|
||||
await act(async () => document.body.querySelector('.cr-call-dialog')!.dispatchEvent(new MouseEvent('click', { bubbles: true })))
|
||||
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull()
|
||||
await act(async () => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })))
|
||||
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
|
||||
expect(document.body.textContent).toContain('卡片')
|
||||
})
|
||||
|
||||
it('多于一棵树才有入口切换,点一项换成那棵;只有一棵没有这排', async () => {
|
||||
await render(<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={MANY} onClose={() => {}} />)
|
||||
const tabs = [...document.body.querySelectorAll('[role="tab"]')]
|
||||
expect(tabs.map(tab => tab.textContent?.replace(/\s+/g, ''))).toEqual(['读取用户资料2处', '夜间对账1处'])
|
||||
const body = () => document.body.querySelector('.cr-call-body')!.textContent ?? ''
|
||||
expect(body()).toContain('另外 2 处调用')
|
||||
expect(body()).not.toContain('夜间对账')
|
||||
await act(async () => (tabs[1] as HTMLButtonElement).click())
|
||||
expect(body()).toContain('夜间对账')
|
||||
expect(body()).not.toContain('另外 2 处调用')
|
||||
})
|
||||
|
||||
it('正在查、没有调用者,以及类的「新建」节点', async () => {
|
||||
await render(<CallTreeDialog kind="class" name="" where="" status="loading" trees={[]} onClose={() => {}} />)
|
||||
expect(document.body.textContent).toContain('正在查调用关系')
|
||||
await render(<CallTreeDialog kind="class" name="OrderService" where="billing/order.py:14" status="empty" trees={[]} onClose={() => {}} />)
|
||||
expect(document.body.textContent).toContain('没有调用它的地方')
|
||||
const made = node('新建 OrderService', {
|
||||
kind: 'class',
|
||||
detail: '进入这个类',
|
||||
where: 'billing/order.py:40',
|
||||
children: [node('OrderService', { kind: 'class', current: true, where: 'billing/order.py:14' })],
|
||||
})
|
||||
await render(
|
||||
<CallTreeDialog
|
||||
kind="class"
|
||||
name="OrderService"
|
||||
where="billing/order.py:14"
|
||||
status="ready"
|
||||
trees={[{ id: 'post', name: 'POST /orders', hits: 1, root: node('POST /orders', { detail: '入口', kind: 'function', children: [made] }) }]}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
)
|
||||
expect(document.body.textContent).toContain('新建 OrderService')
|
||||
expect(document.body.textContent).toContain('进入这个类')
|
||||
expect(document.body.textContent).toContain('当前')
|
||||
expect(document.body.textContent).not.toContain('方法')
|
||||
expect(document.body.querySelector('[role="tablist"]')).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user