WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
at main 193 lines 5.9 kB view raw
1import { beforeEach, describe, expect, it, vi } from 'vitest'; 2import { 3 promptForIdentifier, 4 promptForLogin, 5 promptForPassword, 6 promptForRemoteSelection, 7 promptToSaveRemote, 8} from '../../src/utils/prompts.js'; 9 10// Mock @inquirer/prompts 11vi.mock('@inquirer/prompts', () => ({ 12 confirm: vi.fn(), 13 input: vi.fn(), 14 password: vi.fn(), 15 select: vi.fn(), 16})); 17 18describe('Prompts', () => { 19 let mockInput: ReturnType<typeof vi.fn>; 20 let mockPassword: ReturnType<typeof vi.fn>; 21 let mockSelect: ReturnType<typeof vi.fn>; 22 let mockConfirm: ReturnType<typeof vi.fn>; 23 24 beforeEach(async () => { 25 const inquirer = await import('@inquirer/prompts'); 26 mockInput = vi.mocked(inquirer.input); 27 mockPassword = vi.mocked(inquirer.password); 28 mockSelect = vi.mocked(inquirer.select); 29 mockConfirm = vi.mocked(inquirer.confirm); 30 vi.clearAllMocks(); 31 }); 32 33 describe('promptForIdentifier', () => { 34 it('should prompt for identifier and return valid handle', async () => { 35 mockInput.mockResolvedValue('user.bsky.social'); 36 37 const result = await promptForIdentifier(); 38 39 expect(result).toBe('user.bsky.social'); 40 expect(mockInput).toHaveBeenCalledWith({ 41 message: 'Enter your AT Protocol identifier (handle or DID):', 42 validate: expect.any(Function), 43 }); 44 }); 45 46 it('should validate identifier format', async () => { 47 mockInput.mockResolvedValue('user.bsky.social'); 48 49 await promptForIdentifier(); 50 51 const validateFn = mockInput.mock.calls[0]?.[0]?.validate; 52 expect(validateFn).toBeDefined(); 53 54 if (validateFn) { 55 // Valid handle 56 expect(validateFn('user.bsky.social')).toBe(true); 57 58 // Valid DID 59 expect(validateFn('did:plc:test123')).toBe(true); 60 61 // Empty 62 expect(validateFn('')).toBe('Identifier cannot be empty'); 63 expect(validateFn(' ')).toBe('Identifier cannot be empty'); 64 65 // Invalid format 66 expect(validateFn('invalid')).toContain('Invalid'); 67 } 68 }); 69 }); 70 71 describe('promptForPassword', () => { 72 it('should prompt for password with masking', async () => { 73 mockPassword.mockResolvedValue('test-password'); 74 75 const result = await promptForPassword(); 76 77 expect(result).toBe('test-password'); 78 expect(mockPassword).toHaveBeenCalledWith({ 79 message: 'Enter your app password:', 80 mask: '*', 81 validate: expect.any(Function), 82 }); 83 }); 84 85 it('should validate password is not empty', async () => { 86 mockPassword.mockResolvedValue('test-password'); 87 88 await promptForPassword(); 89 90 const validateFn = mockPassword.mock.calls[0]?.[0]?.validate; 91 expect(validateFn).toBeDefined(); 92 93 if (validateFn) { 94 // Valid password 95 expect(validateFn('password123')).toBe(true); 96 97 // Empty 98 expect(validateFn('')).toBe('Password cannot be empty'); 99 } 100 }); 101 }); 102 103 describe('promptForLogin', () => { 104 it('should prompt for both identifier and password', async () => { 105 mockInput.mockResolvedValue('user.bsky.social'); 106 mockPassword.mockResolvedValue('test-password'); 107 108 const result = await promptForLogin(); 109 110 expect(result).toEqual({ 111 identifier: 'user.bsky.social', 112 password: 'test-password', 113 }); 114 expect(mockInput).toHaveBeenCalledOnce(); 115 expect(mockPassword).toHaveBeenCalledOnce(); 116 }); 117 }); 118 119 describe('promptForRemoteSelection', () => { 120 it('should prompt user to select from multiple remotes', async () => { 121 const remotes = [ 122 { name: 'origin', url: 'git@tangled.org:did:plc:abc123/repo.git' }, 123 { name: 'upstream', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, 124 ]; 125 126 mockSelect.mockResolvedValue('upstream'); 127 128 const result = await promptForRemoteSelection(remotes); 129 130 expect(result).toBe('upstream'); 131 expect(mockSelect).toHaveBeenCalledWith({ 132 message: 'Multiple tangled.org remotes found. Which one would you like to use?', 133 choices: [ 134 { name: 'origin (git@tangled.org:did:plc:abc123/repo.git)', value: 'origin' }, 135 { name: 'upstream (git@tangled.org:did:plc:xyz789/repo.git)', value: 'upstream' }, 136 ], 137 default: 'origin', 138 }); 139 }); 140 141 it('should default to "origin" if present', async () => { 142 const remotes = [ 143 { name: 'upstream', url: 'git@tangled.org:did:plc:abc123/repo.git' }, 144 { name: 'origin', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, 145 ]; 146 147 mockSelect.mockResolvedValue('origin'); 148 149 await promptForRemoteSelection(remotes); 150 151 const call = mockSelect.mock.calls[0]?.[0]; 152 expect(call?.default).toBe('origin'); 153 }); 154 155 it('should not have default if "origin" not present', async () => { 156 const remotes = [ 157 { name: 'upstream', url: 'git@tangled.org:did:plc:abc123/repo.git' }, 158 { name: 'fork', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, 159 ]; 160 161 mockSelect.mockResolvedValue('upstream'); 162 163 await promptForRemoteSelection(remotes); 164 165 const call = mockSelect.mock.calls[0]?.[0]; 166 expect(call?.default).toBeUndefined(); 167 }); 168 }); 169 170 describe('promptToSaveRemote', () => { 171 it('should prompt user to save remote selection', async () => { 172 mockConfirm.mockResolvedValue(true); 173 174 const result = await promptToSaveRemote(); 175 176 expect(result).toBe(true); 177 expect(mockConfirm).toHaveBeenCalledWith({ 178 message: 'Save this remote selection for this repository? (saves to .tangledrc)', 179 default: false, 180 }); 181 }); 182 183 it('should default to false', async () => { 184 mockConfirm.mockResolvedValue(false); 185 186 const result = await promptToSaveRemote(); 187 188 expect(result).toBe(false); 189 const call = mockConfirm.mock.calls[0]?.[0]; 190 expect(call?.default).toBe(false); 191 }); 192 }); 193});