Merge remote-tracking branch 'origin/master' into mergebot/pr927-r3
# Conflicts: # packages/ui/tui/README.i18n.yaml # packages/ui/tui/src/components/dialogs.ts
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
disposeTuiTestHarness,
|
||||
type TuiHarnessOptions,
|
||||
} from './harness.ts'
|
||||
import { HeadlessTerminal } from './headless-terminal.ts'
|
||||
import { TestSessionQueryService } from './session-query.ts'
|
||||
|
||||
const UNUSED_TOOL_OUTPUT: ToolDefinition['output'] = {
|
||||
@@ -5510,6 +5511,54 @@ describe('tool cards and surface replay', () => {
|
||||
})
|
||||
|
||||
describe('TUI user-interaction dialogs', () => {
|
||||
it('limits the visible option window to maxQuestionOptions', async () => {
|
||||
const result = await setup({
|
||||
config: { maxQuestionOptions: 1, questionDialogWidth: 60, questionDialogMaxHeight: 20 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'cap',
|
||||
question: 'Pick one',
|
||||
options: [{ label: 'Visible first' }, { label: 'Hidden second' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Visible first')
|
||||
expect(result.terminal.output).not.toContain('Hidden second')
|
||||
expect(result.terminal.output).toContain('↓ 1 more')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('renders a pending question between the transcript and editor', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 40, questionDialogMaxHeight: 10 },
|
||||
})
|
||||
result.terminal.send('draft input')
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'placement',
|
||||
question: 'Pick one',
|
||||
options: [{ label: 'First' }, { label: 'Second' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.resize(60, 20)
|
||||
await tick()
|
||||
const render = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
const questionIndex = render.indexOf('Pick one')
|
||||
const editorIndex = render.indexOf('draft input')
|
||||
expect(questionIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(editorIndex).toBeGreaterThan(questionIndex)
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('answers single-select, multi-select, custom, and optionless questions', async () => {
|
||||
const result = await setup({ config: { maxQuestionOptions: 1 } })
|
||||
|
||||
@@ -5629,6 +5678,468 @@ describe('TUI user-interaction dialogs', () => {
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('scrolls tall option lists with ↑/↓ overflow markers when the dialog height is capped', async () => {
|
||||
const result = await setup({
|
||||
config: {
|
||||
questionDialogWidth: 60,
|
||||
questionDialogMaxHeight: 12,
|
||||
maxQuestionOptions: 8,
|
||||
},
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'scroll',
|
||||
question: 'Pick one',
|
||||
options: [
|
||||
{ label: 'Alpha', description: 'first choice with a description that will wrap to multiple lines when the dialog is narrow' },
|
||||
{ label: 'Bravo', description: 'second choice' },
|
||||
{ label: 'Charlie', description: 'third choice' },
|
||||
{ label: 'Delta', description: 'fourth choice' },
|
||||
{ label: 'Echo', description: 'fifth choice' },
|
||||
{ label: 'Foxtrot', description: 'sixth choice' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('↓')
|
||||
expect(result.terminal.output).toContain('more')
|
||||
for (let step = 0; step < 5; step += 1) result.terminal.send('\x1b[B')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('↑')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('keeps controls visible when the selected option block exceeds the row budget', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 40, questionDialogMaxHeight: 10 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'oversize',
|
||||
question: 'Pick one',
|
||||
options: [
|
||||
{ label: 'Huge', description: `start ${'middle '.repeat(40)}visible tail` },
|
||||
{ label: 'Other' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Huge')
|
||||
expect(result.terminal.output).toContain('PgUp/PgDn')
|
||||
expect(result.terminal.output).toContain('↑↓ Tab ↵ Esc')
|
||||
expect(result.terminal.output).not.toContain('visible tail')
|
||||
for (let page = 0; page < 30; page += 1) result.terminal.send('\x1b[6~')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('visible tail')
|
||||
for (let page = 0; page < 30; page += 1) result.terminal.send('\x1b[5~')
|
||||
result.terminal.send('\x1b[6~')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('start middle')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('pages long question detail so every plan-review line remains reachable', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 20, questionDialogMaxHeight: 10 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'long-detail',
|
||||
question: 'Approve this plan?',
|
||||
detail: `visible start ${'review step '.repeat(60)}visible tail`,
|
||||
options: [{ label: 'Approve' }, { label: 'Reject' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.resize(60, 20)
|
||||
await tick()
|
||||
const initialRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(initialRender).toContain('plan?')
|
||||
expect(initialRender).toContain('visible start')
|
||||
expect(initialRender).not.toContain('visible tail')
|
||||
expect(initialRender).toMatch(/PgUp\/PgDn \d+\/\d+/u)
|
||||
for (let page = 0; page < 30; page += 1) result.terminal.send('\x1b[6~')
|
||||
await tick()
|
||||
const finalRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(finalRender).toContain('visible tail')
|
||||
expect(finalRender).toContain('Approve')
|
||||
result.terminal.send('\x1b[B')
|
||||
await tick()
|
||||
const movedRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(movedRender).toContain('visible tail')
|
||||
expect(movedRender).toContain('Reject')
|
||||
result.terminal.send('\x1b[A')
|
||||
result.terminal.send('\t')
|
||||
await tick()
|
||||
result.terminal.send('\x1b[6~')
|
||||
await tick()
|
||||
result.terminal.send('\x1b[5~')
|
||||
result.terminal.resize(61, 20)
|
||||
await tick()
|
||||
const customPagedRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(customPagedRender).not.toContain('visible tail')
|
||||
expect(customPagedRender).toContain('Esc options')
|
||||
result.terminal.send('\x1b')
|
||||
for (let page = 0; page < 30; page += 1) result.terminal.send('\x1b[5~')
|
||||
await tick()
|
||||
const restoredRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(restoredRender).toContain('visible start')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('reclaims enough rows to keep selected content, paging, and option markers visible', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 60, questionDialogMaxHeight: 8 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'one-row',
|
||||
question: 'Pick one',
|
||||
options: [
|
||||
{ label: 'Selected first', description: `start ${'middle '.repeat(30)}visible tail` },
|
||||
{ label: 'Hidden second' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Selected first')
|
||||
expect(result.terminal.output).not.toContain('Hidden second')
|
||||
expect(result.terminal.output).toContain('↓ 1 more')
|
||||
expect(result.terminal.output).toContain('PgUp/PgDn')
|
||||
expect(result.terminal.output).toContain('Esc interrupt')
|
||||
for (let page = 0; page < 30; page += 1) result.terminal.send('\x1b[6~')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('visible tail')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('preserves both option markers and controls at the minimum configured height', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 60, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'minimum-options',
|
||||
question: 'Pick one',
|
||||
multiSelect: true,
|
||||
options: ['One', 'Two', 'Three', 'Four', 'Five'].map(label => ({
|
||||
label,
|
||||
description: `${label} ${'wrapped detail '.repeat(20)}`,
|
||||
})),
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\x1b[B')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('↑ 2 more')
|
||||
expect(result.terminal.output).toContain('Three')
|
||||
expect(result.terminal.output).toContain('PgUp/PgDn')
|
||||
expect(result.terminal.output).toContain('↓ 2 more')
|
||||
expect(result.terminal.output).toContain('Tab custom')
|
||||
expect(result.terminal.output).toContain('Space toggle')
|
||||
expect(result.terminal.output).toContain('Esc interrupt')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Error: Select at least one')
|
||||
result.terminal.resize(61)
|
||||
await tick()
|
||||
const validationRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(validationRender).toContain('Tab custom')
|
||||
expect(validationRender).toContain('Space toggle')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('preserves detail text and every action when one compact header row remains', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 20, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'one-header-row',
|
||||
question: 'Plan?',
|
||||
detail: 'abcdvisible tail',
|
||||
multiSelect: true,
|
||||
options: [
|
||||
{ label: 'Yes', description: 'accept' },
|
||||
{ label: 'No', description: 'reject' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.resize(60, 20)
|
||||
await tick()
|
||||
const initialRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(initialRender).toContain('P↑↓ ↑↓ Tab S↵Esc')
|
||||
result.terminal.send('\x1b[6~')
|
||||
result.terminal.send('\x1b[6~')
|
||||
await tick()
|
||||
const detailRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(detailRender).toContain('visible tail')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
|
||||
const single = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'one-header-row-single',
|
||||
question: 'Plan?',
|
||||
detail: 'abcdvisible tail',
|
||||
options: [
|
||||
{ label: 'Yes', description: 'accept' },
|
||||
{ label: 'No', description: 'reject' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const singleRejected = expect(single).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.resize(61, 20)
|
||||
await tick()
|
||||
const singleRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(singleRender).toContain('P↑↓ ↑↓ Tab↵Esc')
|
||||
result.terminal.send('\x03')
|
||||
await singleRejected
|
||||
|
||||
const compact = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'one-header-row-compact',
|
||||
question: 'Pick?',
|
||||
multiSelect: true,
|
||||
options: [
|
||||
{ label: 'Yes', description: 'accept' },
|
||||
{ label: 'No', description: 'reject' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const compactRejected = expect(compact).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.resize(60, 20)
|
||||
await tick()
|
||||
const compactRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(compactRender).toContain('↑↓ Tab Sp ↵Esc')
|
||||
result.terminal.send('\x03')
|
||||
await compactRejected
|
||||
|
||||
const oneOption = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'one-header-row-one-option',
|
||||
question: 'Pick?',
|
||||
detail: 'Review every line.',
|
||||
options: [{ label: 'Yes', description: 'wrapped detail '.repeat(8) }],
|
||||
}],
|
||||
})
|
||||
const oneOptionRejected = expect(oneOption).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.resize(61, 20)
|
||||
await tick()
|
||||
const oneOptionRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(oneOptionRender).toContain('P↑↓ Tab↵Esc')
|
||||
expect(oneOptionRender).not.toContain('P↑↓ ↑↓')
|
||||
result.terminal.send('\x03')
|
||||
await oneOptionRejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('expands the visible option window forward and backward around the selection', async () => {
|
||||
const result = await setup({
|
||||
config: {
|
||||
questionDialogWidth: 60,
|
||||
questionDialogMaxHeight: 14,
|
||||
maxQuestionOptions: 8,
|
||||
},
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'middle-scroll',
|
||||
question: 'Pick one',
|
||||
options: [
|
||||
{ label: 'One', description: 'a' },
|
||||
{ label: 'Two', description: 'b' },
|
||||
{ label: 'Three', description: 'c' },
|
||||
{ label: 'Four', description: 'd' },
|
||||
{ label: 'Five', description: 'e' },
|
||||
{ label: 'Six', description: 'f' },
|
||||
{ label: 'Seven', description: 'g' },
|
||||
{ label: 'Eight', description: 'h' },
|
||||
{ label: 'Nine', description: 'i' },
|
||||
{ label: 'Ten', description: 'j' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
for (let step = 0; step < 4; step += 1) result.terminal.send('\x1b[B')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('↑')
|
||||
expect(result.terminal.output).toContain('↓')
|
||||
expect(result.terminal.output).toContain('Five')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('wraps a long option label across multiple lines instead of truncating it', async () => {
|
||||
const result = await setup({ config: { questionDialogWidth: 40 } })
|
||||
const longLabel = 'this is a very long option label that will not fit on one line in a narrow dialog'
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'long-label',
|
||||
question: 'Pick one',
|
||||
options: [{ label: longLabel }, { label: 'Short' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('narrow dialog')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('wraps fixed question chrome within the minimum dialog width', async () => {
|
||||
const result = await setup({ config: { questionDialogWidth: 20 } })
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{ id: 'narrow', question: 'Answer?' }],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).not.toContain('Question 1/1 (1 unanswered)')
|
||||
expect(result.terminal.output).toContain('unanswered)')
|
||||
expect(result.terminal.output).not.toContain('Enter submit • Esc cancel')
|
||||
expect(result.terminal.output).toContain('Esc cancel')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('keeps custom controls visible at the minimum dialog height', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 20, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{ id: 'short-viewport', question: 'Answer this deliberately long question?' }],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('long question?')
|
||||
expect(result.terminal.output).toContain('Esc cancel')
|
||||
result.terminal.resize(60, 4)
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Enter an answer')
|
||||
expect(result.terminal.output).toContain('long question?')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('compacts custom controls for a question that also has options', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 20, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'compact-custom-options',
|
||||
question: 'Choose or type a deliberately long answer',
|
||||
options: [{ label: 'Default' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
result.terminal.send('\t')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Esc options')
|
||||
result.terminal.send('\x1b')
|
||||
await tick()
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('reports hidden question rows when the viewport leaves one row', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 60, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
result.terminal.resize(60, 2)
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{ id: 'one-row-dialog', question: 'Answer this deliberately long question?' }],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('lines hidden')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('keeps question text when the viewport leaves two question rows', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 60, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
result.terminal.resize(60, 3)
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{ id: 'two-row-dialog', question: 'Answer this deliberately long question?' }],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('long question?')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('bounds option mode when the viewport leaves three question rows', async () => {
|
||||
const result = await setup({
|
||||
config: { questionDialogWidth: 60, questionDialogMaxHeight: 6 },
|
||||
})
|
||||
result.terminal.resize(60, 4)
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{
|
||||
id: 'three-row-options',
|
||||
question: 'Pick one',
|
||||
options: [{ label: 'First' }, { label: 'Second' }],
|
||||
}],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('lines hidden')
|
||||
result.terminal.send('\x03')
|
||||
await rejected
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('keeps question rows within a sub-five-column viewport', async () => {
|
||||
const terminal = new HeadlessTerminal(4, 12)
|
||||
const result = await createTuiTestHarness(terminal, vi.fn(), {
|
||||
config: { questionDialogWidth: 20 },
|
||||
})
|
||||
const beforeQuestion = terminal.frames
|
||||
const answer = result.ctx.userInteraction.ask({
|
||||
questions: [{ id: 'narrow-viewport', question: 'Pick?', options: [{ label: 'Yes' }] }],
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await terminal.waitForFrame(beforeQuestion)
|
||||
await expect(terminal.snapshot()).resolves.toContain('terminal 4x12')
|
||||
terminal.send('\x03')
|
||||
await rejected
|
||||
await disposeTuiTestHarness(result)
|
||||
})
|
||||
|
||||
it('asks batches in order and rejects cancelled or aborted work', async () => {
|
||||
const result = await setup()
|
||||
const preAborted = new AbortController()
|
||||
|
||||
Reference in New Issue
Block a user