import type { PeekTarget, ShownPeek } from './definitionPeek' import type { DefinitionPeek, WebviewToExtension } from './types' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { DefinitionPeeks, linkPeekSource, placePeek } from './definitionPeek' import { parseCommentLink } from './repoLink' const DELAY = 300 const HIDING = 500 const PEEK: DefinitionPeek = { path: 'app/devices.py', line: 41, block: { id: '3', anchor: '3', kind: 'class', startLine: 41, endLine: 60, attributes: { kind: 'class' }, concepts: [], children: [], translation: [] }, names: [], } interface Anchor { name: string } function setup(version: number | undefined = 7): { peeks: DefinitionPeeks sent: WebviewToExtension[] shown: () => ShownPeek | undefined asks: () => { id: number }[] } { const sent: WebviewToExtension[] = [] let current: ShownPeek | undefined const peeks = new DefinitionPeeks(msg => sent.push(msg), (next) => { current = next }) peeks.setDelays(DELAY, HIDING) peeks.setVersion(version) const asks = (): { id: number }[] => sent.flatMap(msg => msg.type === 'peekDefinition' || msg.type === 'peekLink' ? [{ id: msg.id }] : []) return { peeks, sent, shown: () => current, asks } } function name(offset: number): PeekTarget { return { anchor: { name: `n${offset}` }, source: { offset } } } beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('定义卡片什么时候弹', () => { it('停够 editor.hover.delay 才问,问的是这份文档版本里这个下标;回答带卡片才弹', () => { const { peeks, sent, shown } = setup() const target = name(120) peeks.point(target) vi.advanceTimersByTime(DELAY - 1) expect(sent).toEqual([]) vi.advanceTimersByTime(1) expect(sent).toEqual([{ type: 'peekDefinition', id: 1, version: 7, offset: 120 }]) peeks.answer(1, PEEK) expect(shown()).toEqual({ anchor: target.anchor, peek: PEEK, jump: { type: 'goToDefinition', version: 7, offset: 120 } }) }) it('没到时间就移开:不问', () => { const { peeks, sent } = setup() peeks.point(name(120)) vi.advanceTimersByTime(DELAY / 2) peeks.point(null) vi.advanceTimersByTime(DELAY * 2) expect(sent).toEqual([]) }) it('回答没有卡片:不弹', () => { const { peeks, shown } = setup() peeks.point(name(120)) vi.advanceTimersByTime(DELAY) peeks.answer(1, undefined) expect(shown()).toBeUndefined() }) it('问了之后移开、或换了名字:迟到的回答丢掉', () => { const { peeks, shown, asks } = setup() peeks.point(name(120)) vi.advanceTimersByTime(DELAY) peeks.point(name(200)) vi.advanceTimersByTime(DELAY) const [first, second] = asks() peeks.answer(first.id, PEEK) expect(shown()).toBeUndefined() peeks.answer(second.id, PEEK) expect(shown()?.anchor).toEqual({ name: 'n200' }) }) it('注释里指到符号的链接:问 peekLink,点标题发的是点这个链接时发的 openRepoLink', () => { const { peeks, sent, shown } = setup() const anchor = { name: 'link' } peeks.point({ anchor, source: { path: 'app/devices.py', symbol: 'Repo.find' } }) vi.advanceTimersByTime(DELAY) expect(sent).toEqual([{ type: 'peekLink', id: 1, path: 'app/devices.py', symbol: 'Repo.find' }]) peeks.answer(1, PEEK) peeks.jump() expect(sent.at(-1)).toEqual({ type: 'openRepoLink', path: 'app/devices.py', symbol: 'Repo.find' }) expect(shown()).toBeUndefined() }) it('不带路径的引用:问 peekLink 和点标题时都带着它写在哪块', () => { const { peeks, sent } = setup() const anchor = { name: 'reference' } peeks.point({ anchor, source: { symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' } }) vi.advanceTimersByTime(DELAY) expect(sent).toEqual([{ type: 'peekLink', id: 1, symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' }]) peeks.answer(1, PEEK) peeks.jump() expect(sent.at(-1)).toEqual({ type: 'openRepoLink', symbol: 'get_agent_admitted', block: 'class AgentService > function get_default_agent' }) }) it('还没收到 VS Code 的悬停延迟:不弹', () => { const sent: WebviewToExtension[] = [] const peeks = new DefinitionPeeks(msg => sent.push(msg), () => {}) peeks.setVersion(1) peeks.point(name(5)) vi.advanceTimersByTime(10_000) expect(sent).toEqual([]) }) }) describe('定义卡片什么时候收', () => { function showing(): ReturnType & { target: PeekTarget } { const context = setup() const target = name(120) context.peeks.point(target) vi.advanceTimersByTime(DELAY) context.peeks.answer(1, PEEK) return { ...context, target } } it('移开名字:过 editor.hover.hidingDelay 才收,期间移进浮窗就不收', () => { const { peeks, shown } = showing() peeks.point(null) vi.advanceTimersByTime(HIDING - 1) expect(shown()).toBeDefined() peeks.enterPopup() vi.advanceTimersByTime(HIDING * 3) expect(shown()).toBeDefined() }) it('移出浮窗到空白处:过一会儿收;移回名字就不收', () => { const { peeks, shown, target } = showing() peeks.enterPopup() peeks.point(null) vi.advanceTimersByTime(HIDING / 2) peeks.point(target) vi.advanceTimersByTime(HIDING * 3) expect(shown()).toBeDefined() peeks.point(null) vi.advanceTimersByTime(HIDING) expect(shown()).toBeUndefined() }) it('从名字移向浮窗时扫过别的名字:不把收的时间往后推', () => { const { peeks, shown } = showing() peeks.point(name(300)) vi.advanceTimersByTime(HIDING / 2) peeks.point(name(400)) vi.advanceTimersByTime(HIDING / 2) expect(shown()).toBeUndefined() }) it('扫过别的名字后进了浮窗:不收,也不弹那个名字的卡片', () => { const { peeks, shown, asks } = showing() peeks.point(name(300)) vi.advanceTimersByTime(DELAY / 2) peeks.enterPopup() vi.advanceTimersByTime(HIDING * 3) expect(shown()?.anchor).toEqual({ name: 'n120' }) expect(asks()).toHaveLength(1) }) it('指着浮窗里的东西不弹别的卡片', () => { const { peeks, asks } = showing() peeks.enterPopup() vi.advanceTimersByTime(DELAY * 3) expect(asks()).toHaveLength(1) }) it('移到另一个名字、它有卡片:换成它的', () => { const { peeks, shown, asks } = showing() peeks.point(name(300)) vi.advanceTimersByTime(DELAY) peeks.answer(asks()[1].id, PEEK) expect(shown()?.anchor).toEqual({ name: 'n300' }) vi.advanceTimersByTime(HIDING * 3) expect(shown()?.anchor).toEqual({ name: 'n300' }) }) it('移到另一个名字、它没有卡片:原来的照常过一会儿收', () => { const { peeks, shown, asks } = showing() peeks.point(name(300)) vi.advanceTimersByTime(DELAY) peeks.answer(asks()[1].id, undefined) expect(shown()?.anchor).toEqual({ name: 'n120' }) vi.advanceTimersByTime(HIDING - DELAY) expect(shown()).toBeUndefined() }) it('按 Esc、滚动、按下鼠标(dismiss)立刻收;指针不离开这个名字不再弹', () => { const { peeks, shown, asks, target } = showing() peeks.dismiss() expect(shown()).toBeUndefined() peeks.point(target) vi.advanceTimersByTime(DELAY * 3) expect(asks()).toHaveLength(1) }) it('换了文档版本:收起,作罢没问完的', () => { const { peeks, shown, asks } = showing() peeks.point(name(300)) peeks.setVersion(8) expect(shown()).toBeUndefined() vi.advanceTimersByTime(DELAY * 3) expect(asks()).toHaveLength(1) }) }) describe('链接能不能弹定义卡片', () => { it('指到符号的才能:带 #符号 的仓库链接、不带路径的引用(带着写在哪块)', () => { expect(linkPeekSource(parseCommentLink('app/devices.py#Repo.find'))).toEqual({ path: 'app/devices.py', symbol: 'Repo.find' }) expect(linkPeekSource(parseCommentLink('symbol:DesktopOnly'))).toEqual({ symbol: 'DesktopOnly' }) expect(linkPeekSource({ symbol: 'DesktopOnly', block: 'function gate' })).toEqual({ symbol: 'DesktopOnly', block: 'function gate' }) }) it('只指到文件、目录的,网址:不能', () => { expect(linkPeekSource(parseCommentLink('app/devices.py'))).toBeUndefined() expect(linkPeekSource(parseCommentLink('app/'))).toBeUndefined() expect(linkPeekSource(parseCommentLink('https://example.com#x'))).toBeUndefined() }) }) describe('浮窗放在哪', () => { const viewport = { width: 1000, height: 800 } it('下方放得下:贴着名字下边往下长,左边对齐名字', () => { expect(placePeek({ top: 100, bottom: 120, left: 300 }, { width: 400, height: 300 }, viewport)) .toEqual({ top: 124, left: 300, maxWidth: 692, maxHeight: 668 }) }) it('下方放不下、上方空间更大:底边贴着名字上边往上长', () => { expect(placePeek({ top: 600, bottom: 620, left: 300 }, { width: 400, height: 300 }, viewport)) .toEqual({ bottom: 204, left: 300, maxWidth: 692, maxHeight: 588 }) }) it('两边都放不下:放空间大的那边;右边放不下往左挪,钳在视口内', () => { expect(placePeek({ top: 300, bottom: 320, left: 900 }, { width: 400, height: 900 }, viewport)) .toEqual({ top: 324, left: 592, maxWidth: 400, maxHeight: 468 }) }) })