post.test.js
1 import { describe, expect, it, vi } from 'vitest'; 2 import { getRegistry } from '@jackwener/opencli/registry'; 3 import './post.js'; 4 vi.mock('node:fs', async (importOriginal) => { 5 const actual = await importOriginal(); 6 return { 7 ...actual, 8 statSync: vi.fn((p, _opts) => { 9 if (String(p).includes('missing')) 10 return undefined; 11 return { isFile: () => true }; 12 }), 13 }; 14 }); 15 vi.mock('node:path', async (importOriginal) => { 16 const actual = await importOriginal(); 17 return { 18 ...actual, 19 resolve: vi.fn((p) => `/abs/${p}`), 20 extname: vi.fn((p) => { 21 const m = p.match(/\.[^.]+$/); 22 return m ? m[0] : ''; 23 }), 24 }; 25 }); 26 function makePage(overrides = {}) { 27 return { 28 goto: vi.fn().mockResolvedValue(undefined), 29 wait: vi.fn().mockResolvedValue(undefined), 30 evaluate: vi.fn().mockResolvedValue({ ok: true }), 31 setFileInput: vi.fn().mockResolvedValue(undefined), 32 ...overrides, 33 }; 34 } 35 describe('twitter post command', () => { 36 const getCommand = () => getRegistry().get('twitter/post'); 37 it('posts text-only tweet successfully', async () => { 38 const command = getCommand(); 39 const page = makePage({ 40 evaluate: vi.fn() 41 .mockResolvedValueOnce({ ok: true }) 42 .mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }), 43 }); 44 const result = await command.func(page, { text: 'hello world' }); 45 expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'hello world' }]); 46 expect(page.goto).toHaveBeenCalledWith('https://x.com/compose/tweet'); 47 }); 48 it('returns failed when text area not found', async () => { 49 const command = getCommand(); 50 const page = makePage({ 51 evaluate: vi.fn() 52 .mockResolvedValueOnce({ ok: false, message: 'Could not find the tweet composer text area.' }), 53 }); 54 const result = await command.func(page, { text: 'hello' }); 55 expect(result).toEqual([{ status: 'failed', message: 'Could not find the tweet composer text area.', text: 'hello' }]); 56 }); 57 it('throws when more than 4 images', async () => { 58 const command = getCommand(); 59 const page = makePage(); 60 await expect(command.func(page, { text: 'hi', images: 'a.png,b.png,c.png,d.png,e.png' })).rejects.toThrow('Too many images: 5 (max 4)'); 61 }); 62 it('throws when image file does not exist', async () => { 63 const command = getCommand(); 64 const page = makePage(); 65 await expect(command.func(page, { text: 'hi', images: 'missing.png' })).rejects.toThrow('Not a valid file'); 66 }); 67 it('throws on unsupported image format', async () => { 68 const command = getCommand(); 69 const page = makePage(); 70 await expect(command.func(page, { text: 'hi', images: 'photo.bmp' })).rejects.toThrow('Unsupported image format'); 71 }); 72 it('throws when page.setFileInput is not available', async () => { 73 const command = getCommand(); 74 const page = makePage({ 75 evaluate: vi.fn().mockResolvedValueOnce({ ok: true }), 76 setFileInput: undefined, 77 }); 78 await expect(command.func(page, { text: 'hi', images: 'a.png' })).rejects.toThrow('Browser extension does not support file upload'); 79 }); 80 it('posts with images when upload completes', async () => { 81 const command = getCommand(); 82 const page = makePage({ 83 evaluate: vi.fn() 84 .mockResolvedValueOnce({ ok: true }) // type text 85 .mockResolvedValueOnce(true) // upload polling returns true 86 .mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }), // click post 87 }); 88 const result = await command.func(page, { text: 'with images', images: 'a.png,b.png' }); 89 expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'with images' }]); 90 expect(page.setFileInput).toHaveBeenCalled(); 91 const uploadScript = page.evaluate.mock.calls[1][0]; 92 expect(uploadScript).toContain('[data-testid="attachments"]'); 93 expect(uploadScript).toContain('[role="group"]'); 94 }); 95 it('returns failed when image upload times out', async () => { 96 const command = getCommand(); 97 const page = makePage({ 98 evaluate: vi.fn() 99 .mockResolvedValueOnce({ ok: true }) 100 .mockResolvedValueOnce(false), 101 }); 102 const result = await command.func(page, { text: 'timeout', images: 'a.png' }); 103 expect(result).toEqual([{ status: 'failed', message: 'Image upload timed out (30s).', text: 'timeout' }]); 104 }); 105 it('validates images before navigating to compose page', async () => { 106 const command = getCommand(); 107 const page = makePage(); 108 await expect(command.func(page, { text: 'hi', images: 'missing.png' })).rejects.toThrow('Not a valid file'); 109 // Should NOT have navigated since validation happens first 110 expect(page.goto).not.toHaveBeenCalled(); 111 }); 112 it('throws when no browser session', async () => { 113 const command = getCommand(); 114 await expect(command.func(null, { text: 'hi' })).rejects.toThrow('Browser session required for twitter post'); 115 }); 116 });