mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-03 06:00:49 +00:00
feat: add auth command
This commit is contained in:
parent
d4608afc2d
commit
9a3041335f
7 changed files with 1616 additions and 22 deletions
421
packages/cli/src/commands/auth/interactiveSelector.test.ts
Normal file
421
packages/cli/src/commands/auth/interactiveSelector.test.ts
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { InteractiveSelector } from './interactiveSelector.js';
|
||||
import { stdin, stdout } from 'node:process';
|
||||
|
||||
describe('InteractiveSelector', () => {
|
||||
const mockOptions = [
|
||||
{ value: 'option1', label: 'Option 1', description: 'First option' },
|
||||
{ value: 'option2', label: 'Option 2', description: 'Second option' },
|
||||
{ value: 'option3', label: 'Option 3', description: 'Third option' },
|
||||
];
|
||||
|
||||
const mockPrompt = 'Select an option:';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create an instance with default prompt', () => {
|
||||
const selector = new InteractiveSelector(mockOptions);
|
||||
expect(selector).toBeInstanceOf(InteractiveSelector);
|
||||
});
|
||||
|
||||
it('should create an instance with custom prompt', () => {
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
expect(selector).toBeInstanceOf(InteractiveSelector);
|
||||
});
|
||||
});
|
||||
|
||||
describe('select', () => {
|
||||
it('should reject if raw mode is not available', async () => {
|
||||
// Mock stdin without setRawMode
|
||||
const originalSetRawMode = stdin.setRawMode;
|
||||
(stdin as any).setRawMode = undefined;
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
|
||||
await expect(selector.select()).rejects.toThrow(
|
||||
'Raw mode not available. Please run in an interactive terminal.',
|
||||
);
|
||||
|
||||
// Restore
|
||||
(stdin as any).setRawMode = originalSetRawMode;
|
||||
});
|
||||
|
||||
it('should select first option with Enter key', async () => {
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockSetEncoding = vi.fn();
|
||||
const mockRemoveListener = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
// Simulate Enter key press
|
||||
setTimeout(() => callback('\r'), 0);
|
||||
return stdin;
|
||||
});
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).setEncoding = mockSetEncoding;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
(stdin as any).on = mockOn;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const result = await selector.select();
|
||||
|
||||
expect(result).toBe('option1');
|
||||
expect(mockSetRawMode).toHaveBeenCalledWith(true);
|
||||
expect(mockResume).toHaveBeenCalled();
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should select second option after arrow down then Enter', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Simulate arrow down
|
||||
dataCallback('\x1B[B');
|
||||
|
||||
// Simulate Enter
|
||||
setTimeout(() => dataCallback('\r'), 0);
|
||||
|
||||
const result = await selectPromise;
|
||||
|
||||
expect(result).toBe('option2');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle arrow up navigation', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Move down twice
|
||||
dataCallback('\x1B[B');
|
||||
dataCallback('\x1B[B');
|
||||
|
||||
// Move up once
|
||||
dataCallback('\x1B[A');
|
||||
|
||||
// Simulate Enter
|
||||
setTimeout(() => dataCallback('\r'), 0);
|
||||
|
||||
const result = await selectPromise;
|
||||
|
||||
expect(result).toBe('option2');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject with Ctrl+C', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Simulate Ctrl+C
|
||||
setTimeout(() => dataCallback('\x03'), 0);
|
||||
|
||||
await expect(selectPromise).rejects.toThrow('Interrupted');
|
||||
});
|
||||
|
||||
it('should wrap around when navigating past last option', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Move down past last option (should wrap to first)
|
||||
dataCallback('\x1B[B');
|
||||
dataCallback('\x1B[B');
|
||||
dataCallback('\x1B[B'); // Now at option1 again (wrapped)
|
||||
|
||||
// Simulate Enter
|
||||
setTimeout(() => dataCallback('\r'), 0);
|
||||
|
||||
const result = await selectPromise;
|
||||
|
||||
expect(result).toBe('option1');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should wrap around when navigating before first option', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Move up from first option (should wrap to last)
|
||||
dataCallback('\x1B[A');
|
||||
|
||||
// Simulate Enter
|
||||
setTimeout(() => dataCallback('\r'), 0);
|
||||
|
||||
const result = await selectPromise;
|
||||
|
||||
expect(result).toBe('option3');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should ignore arrow left/right keys', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Press arrow right (should be ignored)
|
||||
dataCallback('\x1B[C');
|
||||
|
||||
// Press arrow left (should be ignored)
|
||||
dataCallback('\x1B[D');
|
||||
|
||||
// Press Enter - should still select first option
|
||||
setTimeout(() => dataCallback('\r'), 0);
|
||||
|
||||
const result = await selectPromise;
|
||||
|
||||
expect(result).toBe('option1');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle newline character as Enter', async () => {
|
||||
let dataCallback!: (chunk: string) => void;
|
||||
|
||||
const mockSetRawMode = vi.fn();
|
||||
const mockResume = vi.fn();
|
||||
const mockOn = vi.fn((event: any, callback: any) => {
|
||||
dataCallback = callback;
|
||||
return stdin;
|
||||
});
|
||||
const mockRemoveListener = vi.fn();
|
||||
|
||||
(stdin as any).isRaw = false;
|
||||
(stdin as any).setRawMode = mockSetRawMode;
|
||||
(stdin as any).resume = mockResume;
|
||||
(stdin as any).on = mockOn;
|
||||
(stdin as any).removeListener = mockRemoveListener;
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
const selectPromise = selector.select();
|
||||
|
||||
// Simulate newline
|
||||
setTimeout(() => dataCallback('\n'), 0);
|
||||
|
||||
const result = await selectPromise;
|
||||
|
||||
expect(result).toBe('option1');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderMenu', () => {
|
||||
it('should render menu with correct formatting', () => {
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
|
||||
// Access private method for testing
|
||||
(selector as any).renderMenu();
|
||||
|
||||
expect(stdoutWriteSpy).toHaveBeenCalled();
|
||||
const output = stdoutWriteSpy.mock.calls.map((call) => call[0]).join('');
|
||||
|
||||
expect(output).toContain('Select an option:');
|
||||
expect(output).toContain('Option 1');
|
||||
expect(output).toContain('Option 2');
|
||||
expect(output).toContain('Option 3');
|
||||
expect(output).toContain('First option');
|
||||
expect(output).toContain('Second option');
|
||||
expect(output).toContain('Third option');
|
||||
expect(output).toContain('↑ ↓');
|
||||
expect(output).toContain('Enter');
|
||||
expect(output).toContain('Ctrl+C');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should highlight selected option', () => {
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
(selector as any).selectedIndex = 1;
|
||||
(selector as any).renderMenu();
|
||||
|
||||
const output = stdoutWriteSpy.mock.calls.map((call) => call[0]).join('');
|
||||
|
||||
// Selected option should have cyan color code
|
||||
expect(output).toContain('\x1B[36m');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should calculate correct total lines', () => {
|
||||
const selector = new InteractiveSelector(mockOptions, mockPrompt);
|
||||
|
||||
// Access private method for testing
|
||||
(selector as any).calculateTotalLines();
|
||||
|
||||
// Expected: 4 (prompt + empty + empty + instructions) + 3 (options) = 7
|
||||
expect((selector as any).calculateTotalLines()).toBe(7);
|
||||
});
|
||||
|
||||
it('should handle options without descriptions', () => {
|
||||
const simpleOptions = [
|
||||
{ value: 'a', label: 'A' },
|
||||
{ value: 'b', label: 'B' },
|
||||
];
|
||||
|
||||
const stdoutWriteSpy = vi
|
||||
.spyOn(stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
|
||||
const selector = new InteractiveSelector(simpleOptions, mockPrompt);
|
||||
(selector as any).renderMenu();
|
||||
|
||||
const output = stdoutWriteSpy.mock.calls.map((call) => call[0]).join('');
|
||||
|
||||
expect(output).toContain('A');
|
||||
expect(output).toContain('B');
|
||||
|
||||
stdoutWriteSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue