mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
* chore: remove outdated pr-review skill The .qwen/skills/pr-review skill is outdated and no longer needed. Also removes the dangling reference from terminal-capture skill. * chore: add token-stats script to e2e-testing skill * fix(test): remove flaky AskUserQuestionDialog Submit tab test The "shows Submit tab for multiple questions" test fails on macOS CI due to terminal width truncation causing "Submit answers" to render as just "Submit".
273 lines
7.6 KiB
TypeScript
273 lines
7.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { AskUserQuestionDialog } from './AskUserQuestionDialog.js';
|
|
import type { ToolAskUserQuestionConfirmationDetails } from '@qwen-code/qwen-code-core';
|
|
import { ToolConfirmationOutcome } from '@qwen-code/qwen-code-core';
|
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
|
|
|
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
const createSingleQuestion = (
|
|
overrides: Partial<
|
|
ToolAskUserQuestionConfirmationDetails['questions'][0]
|
|
> = {},
|
|
): ToolAskUserQuestionConfirmationDetails['questions'][0] => ({
|
|
question: 'What is your favorite color?',
|
|
header: 'Color',
|
|
options: [
|
|
{ label: 'Red', description: 'A warm color' },
|
|
{ label: 'Blue', description: 'A cool color' },
|
|
{ label: 'Green', description: '' },
|
|
],
|
|
multiSelect: false,
|
|
...overrides,
|
|
});
|
|
|
|
const createConfirmationDetails = (
|
|
overrides: Partial<ToolAskUserQuestionConfirmationDetails> = {},
|
|
): ToolAskUserQuestionConfirmationDetails => ({
|
|
type: 'ask_user_question',
|
|
title: 'Question',
|
|
questions: [createSingleQuestion()],
|
|
onConfirm: vi.fn(),
|
|
...overrides,
|
|
});
|
|
|
|
describe('<AskUserQuestionDialog />', () => {
|
|
describe('rendering', () => {
|
|
it('renders single question with options', () => {
|
|
const details = createConfirmationDetails();
|
|
const onConfirm = vi.fn();
|
|
|
|
const { lastFrame } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
expect(output).toContain('What is your favorite color?');
|
|
expect(output).toContain('Red');
|
|
expect(output).toContain('Blue');
|
|
expect(output).toContain('Green');
|
|
expect(output).toContain('A warm color');
|
|
expect(output).toContain('A cool color');
|
|
});
|
|
|
|
it('renders header for single question', () => {
|
|
const details = createConfirmationDetails();
|
|
const onConfirm = vi.fn();
|
|
|
|
const { lastFrame } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
|
|
expect(lastFrame()).toContain('Color');
|
|
});
|
|
|
|
it('renders "Type something..." custom input option', () => {
|
|
const details = createConfirmationDetails();
|
|
const onConfirm = vi.fn();
|
|
|
|
const { lastFrame } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
|
|
expect(lastFrame()).toContain('Type something...');
|
|
});
|
|
|
|
it('renders help text for single select', () => {
|
|
const details = createConfirmationDetails();
|
|
const onConfirm = vi.fn();
|
|
|
|
const { lastFrame } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
|
|
expect(lastFrame()).toContain('Enter: Select');
|
|
expect(lastFrame()).toContain('Esc: Cancel');
|
|
expect(lastFrame()).not.toContain('Switch tabs');
|
|
});
|
|
|
|
it('renders tabs for multiple questions', () => {
|
|
const details = createConfirmationDetails({
|
|
questions: [
|
|
createSingleQuestion({ header: 'Q1' }),
|
|
createSingleQuestion({
|
|
header: 'Q2',
|
|
question: 'Second question?',
|
|
}),
|
|
],
|
|
});
|
|
const onConfirm = vi.fn();
|
|
|
|
const { lastFrame } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
expect(output).toContain('Q1');
|
|
expect(output).toContain('Q2');
|
|
expect(output).toContain('Submit');
|
|
expect(output).toContain('Switch tabs');
|
|
});
|
|
|
|
it('renders multi-select with checkboxes', () => {
|
|
const details = createConfirmationDetails({
|
|
questions: [createSingleQuestion({ multiSelect: true })],
|
|
});
|
|
const onConfirm = vi.fn();
|
|
|
|
const { lastFrame } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
|
|
const output = lastFrame();
|
|
expect(output).toContain('[ ]');
|
|
expect(output).toContain('Space: Toggle');
|
|
expect(output).toContain('Enter: Confirm');
|
|
});
|
|
});
|
|
|
|
describe('single-select interaction', () => {
|
|
it('selects an option with Enter and submits immediately for single question', async () => {
|
|
const onConfirm = vi.fn();
|
|
const details = createConfirmationDetails();
|
|
|
|
const { stdin, unmount } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
await wait();
|
|
|
|
// Press Enter to select the first option (Red)
|
|
stdin.write('\r');
|
|
await wait();
|
|
|
|
expect(onConfirm).toHaveBeenCalledWith(
|
|
ToolConfirmationOutcome.ProceedOnce,
|
|
{ answers: { 0: 'Red' } },
|
|
);
|
|
unmount();
|
|
});
|
|
it('cancels with Escape', async () => {
|
|
const onConfirm = vi.fn();
|
|
const details = createConfirmationDetails();
|
|
|
|
const { stdin, unmount } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
await wait();
|
|
|
|
stdin.write('\u001B'); // Escape
|
|
await wait();
|
|
|
|
expect(onConfirm).toHaveBeenCalledWith(ToolConfirmationOutcome.Cancel);
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('multi-select interaction', () => {
|
|
it('toggles options with Space', async () => {
|
|
const onConfirm = vi.fn();
|
|
const details = createConfirmationDetails({
|
|
questions: [createSingleQuestion({ multiSelect: true })],
|
|
});
|
|
|
|
const { stdin, lastFrame, unmount } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
await wait();
|
|
|
|
// Space to toggle first option
|
|
stdin.write(' ');
|
|
await wait();
|
|
|
|
// Should show checked state
|
|
expect(lastFrame()).toContain('[✓]');
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('multiple questions', () => {
|
|
it('shows unanswered questions as (not answered) in Submit tab', async () => {
|
|
const onConfirm = vi.fn();
|
|
const details = createConfirmationDetails({
|
|
questions: [
|
|
createSingleQuestion({ header: 'Q1' }),
|
|
createSingleQuestion({ header: 'Q2' }),
|
|
],
|
|
});
|
|
|
|
const { stdin, lastFrame, unmount } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
await wait();
|
|
|
|
// Navigate directly to submit tab without answering anything
|
|
stdin.write('\u001B[C'); // Right
|
|
await wait();
|
|
stdin.write('\u001B[C'); // Right
|
|
await wait();
|
|
|
|
expect(lastFrame()).toContain('(not answered)');
|
|
unmount();
|
|
});
|
|
});
|
|
|
|
describe('focus behavior', () => {
|
|
it('does not respond to keys when isFocused is false', async () => {
|
|
const onConfirm = vi.fn();
|
|
const details = createConfirmationDetails();
|
|
|
|
const { stdin, unmount } = renderWithProviders(
|
|
<AskUserQuestionDialog
|
|
confirmationDetails={details}
|
|
isFocused={false}
|
|
onConfirm={onConfirm}
|
|
/>,
|
|
);
|
|
await wait();
|
|
|
|
stdin.write('\r'); // Enter
|
|
await wait();
|
|
stdin.write('\u001B'); // Escape
|
|
await wait();
|
|
|
|
expect(onConfirm).not.toHaveBeenCalled();
|
|
unmount();
|
|
});
|
|
});
|
|
});
|