Files
jackwener__opencli/clis/twitter/post.test.ts
jackwener 314bc6f231 refactor: simplify core modules — remove root shims, consolidate error classification, streamline cascade/interceptor, clean up synthesize
1. Remove root-level shim files (errors.ts, logger.ts, registry.ts, types.ts, utils.ts, launcher.ts) — update all ~840 adapter imports to reference src/ directly
2. Consolidate interceptor: reuse shared DISGUISE_FN in tap interceptor instead of reimplementing
3. Unify error classification: single ClassifiedError type with icon/exitCode/hint lookup table, eliminating duplicated pattern matching between resolveExitCode and renderError
4. Simplify cascade probe: replace repetitive switch cases with PROBE_OPTIONS lookup map
12. Clean up synthesize.ts: remove deprecated snake_case field aliases (recommended_args, recommended_columns, recommendedColumnsLegacy) and unnecessary constant aliases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 03:06:47 +08:00

158 lines
5.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { getRegistry } from '../../src/registry.js';
import './post.js';
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
...actual,
statSync: vi.fn((p: string, _opts?: any) => {
if (String(p).includes('missing')) return undefined;
return { isFile: () => true };
}),
};
});
vi.mock('node:path', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:path')>();
return {
...actual,
resolve: vi.fn((p: string) => `/abs/${p}`),
extname: vi.fn((p: string) => {
const m = p.match(/\.[^.]+$/);
return m ? m[0] : '';
}),
};
});
function makePage(overrides: Record<string, any> = {}) {
return {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue({ ok: true }),
setFileInput: vi.fn().mockResolvedValue(undefined),
...overrides,
};
}
describe('twitter post command', () => {
const getCommand = () => getRegistry().get('twitter/post');
it('posts text-only tweet successfully', async () => {
const command = getCommand();
const page = makePage({
evaluate: vi.fn()
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }),
});
const result = await command!.func!(page as any, { text: 'hello world' });
expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'hello world' }]);
expect(page.goto).toHaveBeenCalledWith('https://x.com/compose/tweet');
});
it('returns failed when text area not found', async () => {
const command = getCommand();
const page = makePage({
evaluate: vi.fn()
.mockResolvedValueOnce({ ok: false, message: 'Could not find the tweet composer text area.' }),
});
const result = await command!.func!(page as any, { text: 'hello' });
expect(result).toEqual([{ status: 'failed', message: 'Could not find the tweet composer text area.', text: 'hello' }]);
});
it('throws when more than 4 images', async () => {
const command = getCommand();
const page = makePage();
await expect(
command!.func!(page as any, { text: 'hi', images: 'a.png,b.png,c.png,d.png,e.png' }),
).rejects.toThrow('Too many images: 5 (max 4)');
});
it('throws when image file does not exist', async () => {
const command = getCommand();
const page = makePage();
await expect(
command!.func!(page as any, { text: 'hi', images: 'missing.png' }),
).rejects.toThrow('Not a valid file');
});
it('throws on unsupported image format', async () => {
const command = getCommand();
const page = makePage();
await expect(
command!.func!(page as any, { text: 'hi', images: 'photo.bmp' }),
).rejects.toThrow('Unsupported image format');
});
it('throws when page.setFileInput is not available', async () => {
const command = getCommand();
const page = makePage({
evaluate: vi.fn().mockResolvedValueOnce({ ok: true }),
setFileInput: undefined,
});
await expect(
command!.func!(page as any, { text: 'hi', images: 'a.png' }),
).rejects.toThrow('Browser extension does not support file upload');
});
it('posts with images when upload completes', async () => {
const command = getCommand();
const page = makePage({
evaluate: vi.fn()
.mockResolvedValueOnce({ ok: true }) // type text
.mockResolvedValueOnce(true) // upload polling returns true
.mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }), // click post
});
const result = await command!.func!(page as any, { text: 'with images', images: 'a.png,b.png' });
expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'with images' }]);
expect(page.setFileInput).toHaveBeenCalled();
const uploadScript = page.evaluate.mock.calls[1][0] as string;
expect(uploadScript).toContain('[data-testid="attachments"]');
expect(uploadScript).toContain('[role="group"]');
});
it('returns failed when image upload times out', async () => {
const command = getCommand();
const page = makePage({
evaluate: vi.fn()
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce(false),
});
const result = await command!.func!(page as any, { text: 'timeout', images: 'a.png' });
expect(result).toEqual([{ status: 'failed', message: 'Image upload timed out (30s).', text: 'timeout' }]);
});
it('validates images before navigating to compose page', async () => {
const command = getCommand();
const page = makePage();
await expect(
command!.func!(page as any, { text: 'hi', images: 'missing.png' }),
).rejects.toThrow('Not a valid file');
// Should NOT have navigated since validation happens first
expect(page.goto).not.toHaveBeenCalled();
});
it('throws when no browser session', async () => {
const command = getCommand();
await expect(
command!.func!(null as any, { text: 'hi' }),
).rejects.toThrow('Browser session required for twitter post');
});
});