✨ feat(vscode): FastAPI 接口卡片的调用树带上前端调用它的地方
This commit is contained in:
1 parent
d4459e93b3
commit
29cedcb751
30 files changed
+1316
-130
No files matched your search
@@ -36,11 +36,12 @@ async function render(element: ReactElement): Promise<void> {
|
||||
await act(async () => root.render(element))
|
||||
}
|
||||
|
||||
function block(kind: string, id = kind): BlockView {
|
||||
function block(kind: string, id = kind, callTree?: BlockView['callTree']): BlockView {
|
||||
return {
|
||||
id,
|
||||
anchor: id,
|
||||
kind,
|
||||
...(callTree && { callTree }),
|
||||
startLine: 0,
|
||||
endLine: 1,
|
||||
code: '',
|
||||
@@ -79,7 +80,7 @@ function tree(callTree?: BlockTreeState['callTree'], edit = true): BlockTreeStat
|
||||
}
|
||||
|
||||
function node(name: string, extra: Partial<CallTree['root']> = {}): CallTree['root'] {
|
||||
return { name, kind: 'function', where: `${name}.py:1`, current: false, children: [], ...extra }
|
||||
return { name, kind: 'function', where: `${name}.py:1`, jump: { uri: `file:///${name}.py`, line: 0, character: 0 }, current: false, children: [], ...extra }
|
||||
}
|
||||
|
||||
const ONE: CallTree = {
|
||||
@@ -104,21 +105,29 @@ const MANY: CallTree[] = [
|
||||
]
|
||||
|
||||
describe('调用树弹框', () => {
|
||||
it('函数和类都有按钮,棵数为 0 只有图标、大于 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: () => {} })} />)
|
||||
it('宿主标了能看调用树的块才有按钮(接口卡片也是),棵数为 0 只有图标、大于 0 带数字;只读的树一排按钮都没有', async () => {
|
||||
const counts = new Map([['function', 0], ['class', 2], ['statement', 4], ['route', 1], ['bare', 5]])
|
||||
const endpoint = { method: 'GET', path: '/agents' }
|
||||
await render(
|
||||
<BlockLevel
|
||||
blocks={[block('function', 'function', {}), block('class', 'class', {}), block('statement'), block('fastapi.route', 'route', { endpoint }), block('function', 'bare')]}
|
||||
tree={tree({ counts, onOpen: () => {} })}
|
||||
/>,
|
||||
)
|
||||
expect(container.querySelector('[data-call-tree="function"]')!.textContent).toBe('')
|
||||
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')
|
||||
expect(container.querySelector('[data-call-tree="route"]')!.textContent).toBe('1')
|
||||
expect(container.querySelector('[data-call-tree="bare"]')).toBeNull()
|
||||
|
||||
await render(<BlockLevel blocks={[block('function')]} tree={tree({ counts: new Map([['function', 3]]), onOpen: () => {} }, false)} />)
|
||||
await render(<BlockLevel blocks={[block('function', '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')
|
||||
const card = block('function', 'mask', {})
|
||||
function Page(): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
@@ -135,6 +144,7 @@ describe('调用树弹框', () => {
|
||||
status="ready"
|
||||
trees={[ONE]}
|
||||
onClose={() => setOpen(false)}
|
||||
onJump={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -165,7 +175,7 @@ describe('调用树弹框', () => {
|
||||
? (
|
||||
<>
|
||||
<div data-id="mask">卡片</div>
|
||||
<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={[ONE]} onClose={() => setOpen(false)} />
|
||||
<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={[ONE]} onClose={() => setOpen(false)} onJump={() => {}} />
|
||||
</>
|
||||
)
|
||||
: <div data-id="mask">卡片</div>
|
||||
@@ -184,7 +194,7 @@ describe('调用树弹框', () => {
|
||||
})
|
||||
|
||||
it('多于一棵树才有入口切换,点一项换成那棵;只有一棵没有这排', async () => {
|
||||
await render(<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={MANY} onClose={() => {}} />)
|
||||
await render(<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={MANY} onClose={() => {}} onJump={() => {}} />)
|
||||
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 ?? ''
|
||||
@@ -196,9 +206,9 @@ describe('调用树弹框', () => {
|
||||
})
|
||||
|
||||
it('正在查、没有调用者,以及类的「新建」节点', async () => {
|
||||
await render(<CallTreeDialog kind="class" name="" where="" status="loading" trees={[]} onClose={() => {}} />)
|
||||
await render(<CallTreeDialog kind="class" name="" where="" status="loading" trees={[]} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(document.body.textContent).toContain('正在查调用关系')
|
||||
await render(<CallTreeDialog kind="class" name="OrderService" where="billing/order.py:14" status="empty" trees={[]} onClose={() => {}} />)
|
||||
await render(<CallTreeDialog kind="class" name="OrderService" where="billing/order.py:14" status="empty" trees={[]} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(document.body.textContent).toContain('没有调用它的地方')
|
||||
const made = node('新建 OrderService', {
|
||||
kind: 'class',
|
||||
@@ -214,6 +224,7 @@ describe('调用树弹框', () => {
|
||||
status="ready"
|
||||
trees={[{ id: 'post', name: 'POST /orders', hits: 1, root: node('POST /orders', { detail: '入口', kind: 'function', children: [made] }) }]}
|
||||
onClose={() => {}}
|
||||
onJump={() => {}}
|
||||
/>,
|
||||
)
|
||||
expect(document.body.textContent).toContain('新建 OrderService')
|
||||
@@ -222,4 +233,48 @@ describe('调用树弹框', () => {
|
||||
expect(document.body.textContent).not.toContain('方法')
|
||||
expect(document.body.querySelector('[role="tablist"]')).toBeNull()
|
||||
})
|
||||
it('接口在契约文件里没对上:写明重新生成、列出找到的契约文件;一份都没有时写没找到;都不说「没有调用它的地方」', async () => {
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="empty" trees={[]} contractMiss={{ files: ['web/src/lib/api/schema.d.ts', 'admin/src/lib/api/schema.d.ts'] }} onClose={() => {}} onJump={() => {}} />)
|
||||
const body = () => document.body.querySelector('.cr-call-body')!.textContent ?? ''
|
||||
expect(body()).toContain('接口契约文件里没有这个接口,重新生成后再看')
|
||||
expect(body()).toContain('web/src/lib/api/schema.d.ts')
|
||||
expect(body()).toContain('admin/src/lib/api/schema.d.ts')
|
||||
expect(body()).not.toContain('没有调用它的地方')
|
||||
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="empty" trees={[]} contractMiss={{ files: [] }} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(body()).toContain('工作区里没找到接口契约文件(openapi-typescript 生成的 .d.ts 或 openapi.json)')
|
||||
expect(body()).not.toContain('没有调用它的地方')
|
||||
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="ready" trees={[ONE]} contractMiss={{ files: [] }} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(body()).toContain('工作区里没找到接口契约文件')
|
||||
expect(body()).toContain('夜间对账')
|
||||
})
|
||||
|
||||
it('契约里几条都对得上、分不出是哪一条时列出那几条,不叫人重新生成', async () => {
|
||||
const miss = { files: ['web/src/lib/api/schema.d.ts'], ambiguous: ['/api/v1/admin/agents', '/api/v1/agents'] }
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="empty" trees={[]} contractMiss={miss} onClose={() => {}} onJump={() => {}} />)
|
||||
const body = document.body.querySelector('.cr-call-body')!.textContent ?? ''
|
||||
expect(body).toContain('接口契约文件里有几条都对得上这个接口,按 operationId 也分不出是哪一条')
|
||||
expect(body).toContain('/api/v1/admin/agents')
|
||||
expect(body).toContain('/api/v1/agents')
|
||||
expect(body).not.toContain('重新生成')
|
||||
expect(body).not.toContain('没有调用它的地方')
|
||||
})
|
||||
|
||||
it('点一行跳到它在的文件和行,「请求」节点跳到发请求的那一行', async () => {
|
||||
const jumps: unknown[] = []
|
||||
const request = node('请求 GET /api/v1/agents/default', {
|
||||
kind: 'request',
|
||||
where: 'web/src/lib/agents.ts:11',
|
||||
jump: { uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 },
|
||||
children: [node('get_default_agent', { current: true })],
|
||||
})
|
||||
const page: CallTree = { id: 'page', name: 'MyAgentsPage', hits: 1, root: node('MyAgentsPage', { detail: '页面 /my-agents', children: [request] }) }
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="get_default_agent" where="api/routes.py:79" status="ready" trees={[page]} onClose={() => {}} onJump={jump => jumps.push(jump)} />)
|
||||
expect(document.body.textContent).toContain('页面 /my-agents')
|
||||
const rows = [...document.body.querySelectorAll<HTMLButtonElement>('button.cr-call-card')]
|
||||
expect(rows.map(row => row.querySelector('.cr-call-name')!.textContent)).toEqual(['MyAgentsPage', '请求 GET /api/v1/agents/default', 'get_default_agent'])
|
||||
await act(async () => rows[1].click())
|
||||
expect(jumps).toEqual([{ uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 }])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user