|
@@ -122,12 +122,15 @@ async function bench(opts: BenchOptions = {}) {
|
|
|
})
|
|
})
|
|
|
/** Notices the fake conversation face collected (runDetached routing). */
|
|
/** Notices the fake conversation face collected (runDetached routing). */
|
|
|
const notices: Array<{ scope: SessionId | undefined; level: 'info' | 'error'; text: string }> = []
|
|
const notices: Array<{ scope: SessionId | undefined; level: 'info' | 'error'; text: string }> = []
|
|
|
|
|
+ /** Composer focuses the fake conversation face collected (popup dismissal). */
|
|
|
|
|
+ const focuses: Array<SessionId | undefined> = []
|
|
|
const removeConversation = ctx.provide('conversation', {
|
|
const removeConversation = ctx.provide('conversation', {
|
|
|
input: {
|
|
input: {
|
|
|
for: (actx: Context) => ({
|
|
for: (actx: Context) => ({
|
|
|
notify: (level: 'info' | 'error', text: string) => {
|
|
notify: (level: 'info' | 'error', text: string) => {
|
|
|
notices.push({ scope: scopeOf(actx), level, text })
|
|
notices.push({ scope: scopeOf(actx), level, text })
|
|
|
},
|
|
},
|
|
|
|
|
+ focus: () => { focuses.push(scopeOf(actx)) },
|
|
|
}),
|
|
}),
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -146,7 +149,7 @@ async function bench(opts: BenchOptions = {}) {
|
|
|
await source.candidates(session, { query: '', position: 'leading', drilled: false, signal: new AbortController().signal })
|
|
await source.candidates(session, { query: '', position: 'leading', drilled: false, signal: new AbortController().signal })
|
|
|
}
|
|
}
|
|
|
return {
|
|
return {
|
|
|
- ctx, fiber, command, source, mint, warm, listCalls, executeCalls, executions, registered, notices, remote,
|
|
|
|
|
|
|
+ ctx, fiber, command, source, mint, warm, listCalls, executeCalls, executions, registered, notices, focuses, remote,
|
|
|
removeSessions, removeConversation,
|
|
removeSessions, removeConversation,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -939,7 +942,7 @@ describe('popupFor', () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('dismisses matching popups and confirmations while preserving other commands and drafts', async () => {
|
|
it('dismisses matching popups and confirmations while preserving other commands and drafts', async () => {
|
|
|
- const { command, source, mint } = await bench()
|
|
|
|
|
|
|
+ const { command, source, mint, focuses } = await bench()
|
|
|
const pending = Promise.withResolvers<readonly SelectOption[]>()
|
|
const pending = Promise.withResolvers<readonly SelectOption[]>()
|
|
|
let pendingSignal: AbortSignal | undefined
|
|
let pendingSignal: AbortSignal | undefined
|
|
|
const onSelect = vi.fn()
|
|
const onSelect = vi.fn()
|
|
@@ -967,8 +970,6 @@ describe('popupFor', () => {
|
|
|
const other = command.popupFor(mint('other').ctx)
|
|
const other = command.popupFor(mint('other').ctx)
|
|
|
const consume = vi.fn(() => true as const)
|
|
const consume = vi.fn(() => true as const)
|
|
|
scope.ctx.on('slash/input-consume-token', consume)
|
|
scope.ctx.on('slash/input-consume-token', consume)
|
|
|
- const focus = vi.fn()
|
|
|
|
|
- command.bindComposerFocus(sid('s1'), focus)
|
|
|
|
|
menuPick(source, 'theme', proj('s1'))
|
|
menuPick(source, 'theme', proj('s1'))
|
|
|
menuPick(source, 'theme', proj('s2'))
|
|
menuPick(source, 'theme', proj('s2'))
|
|
|
menuPick(source, 'other', proj('other'))
|
|
menuPick(source, 'other', proj('other'))
|
|
@@ -990,7 +991,7 @@ describe('popupFor', () => {
|
|
|
expect(second.state.getSnapshot()).toMatchObject({ open: false, options: [] })
|
|
expect(second.state.getSnapshot()).toMatchObject({ open: false, options: [] })
|
|
|
expect(onSelect).not.toHaveBeenCalled()
|
|
expect(onSelect).not.toHaveBeenCalled()
|
|
|
expect(consume).not.toHaveBeenCalled()
|
|
expect(consume).not.toHaveBeenCalled()
|
|
|
- expect(focus).not.toHaveBeenCalled()
|
|
|
|
|
|
|
+ expect(focuses).toEqual([])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('resolves lazily per session; a foreign session gets its own controller; unscoped ctx throws', async () => {
|
|
it('resolves lazily per session; a foreign session gets its own controller; unscoped ctx throws', async () => {
|
|
@@ -1002,8 +1003,8 @@ describe('popupFor', () => {
|
|
|
expect(() => command.popupFor(ctx)).toThrow('requires a session scope')
|
|
expect(() => command.popupFor(ctx)).toThrow('requires a session scope')
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('a successful select dispatches the scoped consume-token and fires the bound composer focus', async () => {
|
|
|
|
|
- const { command, source, mint } = await bench()
|
|
|
|
|
|
|
+ it('a successful select dispatches the scoped consume-token and focuses the composer', async () => {
|
|
|
|
|
+ const { command, source, mint, focuses } = await bench()
|
|
|
const onSelect = vi.fn()
|
|
const onSelect = vi.fn()
|
|
|
command.register(themeContribution({ ui: themeUi({ onSelect }) }))
|
|
command.register(themeContribution({ ui: themeUi({ onSelect }) }))
|
|
|
const scope = mint('s1')
|
|
const scope = mint('s1')
|
|
@@ -1012,38 +1013,13 @@ describe('popupFor', () => {
|
|
|
consumes.push(r)
|
|
consumes.push(r)
|
|
|
return true
|
|
return true
|
|
|
})
|
|
})
|
|
|
- const focus = vi.fn()
|
|
|
|
|
- command.bindComposerFocus(sid('s1'), focus)
|
|
|
|
|
-
|
|
|
|
|
expect(menuPick(source, 'theme', proj('s1'), 6)).toBe('handled')
|
|
expect(menuPick(source, 'theme', proj('s1'), 6)).toBe('handled')
|
|
|
const popup = command.popupFor(scope.ctx)
|
|
const popup = command.popupFor(scope.ctx)
|
|
|
await Promise.resolve() // options land
|
|
await Promise.resolve() // options land
|
|
|
await popup.select(0)
|
|
await popup.select(0)
|
|
|
expect(onSelect).toHaveBeenCalledExactlyOnceWith({ id: 'dark', label: 'Dark' } satisfies SelectOption, proj('s1'))
|
|
expect(onSelect).toHaveBeenCalledExactlyOnceWith({ id: 'dark', label: 'Dark' } satisfies SelectOption, proj('s1'))
|
|
|
expect(consumes).toEqual([{ guard: { kind: 'span', span: { start: 0, end: 6, draftRev: 3 } } }])
|
|
expect(consumes).toEqual([{ guard: { kind: 'span', span: { start: 0, end: 6, draftRev: 3 } } }])
|
|
|
- expect(focus).toHaveBeenCalledTimes(1)
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('unbinding an old composer preserves its replacement and unbinding the current one removes focus', async () => {
|
|
|
|
|
- const { command, source, mint } = await bench()
|
|
|
|
|
- command.register(themeContribution())
|
|
|
|
|
- const scope = mint('s1')
|
|
|
|
|
- scope.ctx.on('slash/input-consume-token', () => true)
|
|
|
|
|
- const oldFocus = vi.fn()
|
|
|
|
|
- const newFocus = vi.fn()
|
|
|
|
|
- const unbindOld = command.bindComposerFocus(sid('s1'), oldFocus)
|
|
|
|
|
- const unbindNew = command.bindComposerFocus(sid('s1'), newFocus)
|
|
|
|
|
- unbindOld()
|
|
|
|
|
- menuPick(source, 'theme', proj('s1'))
|
|
|
|
|
- await Promise.resolve()
|
|
|
|
|
- await command.popupFor(scope.ctx).select(0)
|
|
|
|
|
- expect(oldFocus).not.toHaveBeenCalled()
|
|
|
|
|
- expect(newFocus).toHaveBeenCalledOnce()
|
|
|
|
|
- unbindNew()
|
|
|
|
|
- menuPick(source, 'theme', proj('s1'))
|
|
|
|
|
- await Promise.resolve()
|
|
|
|
|
- await command.popupFor(scope.ctx).select(0)
|
|
|
|
|
- expect(newFocus).toHaveBeenCalledOnce()
|
|
|
|
|
|
|
+ expect(focuses).toEqual([sid('s1')])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('the enter path opens with the bare-token guard', async () => {
|
|
it('the enter path opens with the bare-token guard', async () => {
|