9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
173 lines
7.2 KiB
JavaScript
173 lines
7.2 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import './drafts.js';
|
|
import './draft-delete.js';
|
|
import './draft-clear.js';
|
|
import './draft-open.js';
|
|
|
|
function createPageMock(evaluateResults = []) {
|
|
const evaluate = vi.fn();
|
|
for (const result of evaluateResults) {
|
|
evaluate.mockResolvedValueOnce(result);
|
|
}
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
evaluate,
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
}
|
|
|
|
const sampleDraft = {
|
|
content: {
|
|
contextStore: { liveContext: { title: '本地草稿' } },
|
|
editorContent: { text: '草稿正文内容' },
|
|
noteImageConfig: { items: [{}, {}] },
|
|
},
|
|
updatedAt: 1710000000000,
|
|
};
|
|
|
|
describe('xiaohongshu draft commands', () => {
|
|
it('registers draft commands with correct access and side-effect guard columns', () => {
|
|
expect(getRegistry().get('xiaohongshu/drafts')?.access).toBe('read');
|
|
expect(getRegistry().get('xiaohongshu/draft-open')?.access).toBe('read');
|
|
expect(getRegistry().get('xiaohongshu/draft-delete')?.access).toBe('write');
|
|
expect(getRegistry().get('xiaohongshu/draft-clear')?.access).toBe('write');
|
|
expect(getRegistry().get('xiaohongshu/draft-delete')?.columns).toEqual(['status', 'id', 'type', 'message']);
|
|
expect(getRegistry().get('xiaohongshu/draft-clear')?.columns).toEqual(['status', 'type', 'count', 'message']);
|
|
});
|
|
|
|
it('rejects invalid draft types before browser navigation', async () => {
|
|
const command = getRegistry().get('xiaohongshu/drafts');
|
|
const page = createPageMock();
|
|
|
|
await expect(command.func(page, { type: 'bogus' })).rejects.toBeInstanceOf(ArgumentError);
|
|
expect(page.goto).not.toHaveBeenCalled();
|
|
expect(page.evaluate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('lists drafts with stable typed ids so numeric and string keys do not collide', async () => {
|
|
const command = getRegistry().get('xiaohongshu/drafts');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }, { key: '12', row: { title: '字符串键' } }] },
|
|
]);
|
|
|
|
const rows = await command.func(page, { type: 'image' });
|
|
|
|
expect(rows).toMatchObject([
|
|
{ rank: 1, id: 'n:12', title: '本地草稿', images: 2, text_preview: '草稿正文内容' },
|
|
{ rank: 2, id: 's:12', title: '字符串键' },
|
|
]);
|
|
});
|
|
|
|
it('opens a draft by encoded id and returns full content', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-open');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }] },
|
|
]);
|
|
|
|
const rows = await command.func(page, { id: 'n:12', type: 'image' });
|
|
|
|
expect(rows).toEqual([
|
|
{
|
|
id: 'n:12',
|
|
type: 'image',
|
|
title: '本地草稿',
|
|
updated_at: '1710000000000',
|
|
images: 2,
|
|
content: '草稿正文内容',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('fails typed when opening a missing draft id', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-open');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }] },
|
|
]);
|
|
|
|
await expect(command.func(page, { id: 'n:99', type: 'image' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
});
|
|
|
|
it('does not accept raw untyped ids that can collide across IndexedDB key types', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-delete');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }, { key: '12', row: { title: '字符串键' } }] },
|
|
]);
|
|
|
|
await expect(command.func(page, { id: '12', type: 'image', execute: true })).rejects.toBeInstanceOf(EmptyResultError);
|
|
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('dry-runs draft-delete by default after proving the target exists', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-delete');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }] },
|
|
]);
|
|
|
|
const rows = await command.func(page, { id: 'n:12', type: 'image' });
|
|
|
|
expect(rows).toEqual([
|
|
{
|
|
status: 'dry-run',
|
|
id: 'n:12',
|
|
type: 'image',
|
|
message: 'Draft exists. Re-run with --execute to delete.',
|
|
},
|
|
]);
|
|
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('executes draft-delete only with --execute and verifies disappearance', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-delete');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }] },
|
|
{ ok: true, deleted: true },
|
|
]);
|
|
|
|
const rows = await command.func(page, { id: 'n:12', type: 'image', execute: true });
|
|
|
|
expect(rows[0]).toMatchObject({ status: 'deleted', id: 'n:12', type: 'image' });
|
|
expect(page.evaluate).toHaveBeenCalledTimes(2);
|
|
expect(page.evaluate.mock.calls[1][0]).toContain('store.delete(key)');
|
|
});
|
|
|
|
it('fails closed when draft-delete postcondition does not hold', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-delete');
|
|
const page = createPageMock([
|
|
{ ok: true, entries: [{ key: 12, row: sampleDraft }] },
|
|
{ ok: true, deleted: false },
|
|
]);
|
|
|
|
await expect(command.func(page, { id: 'n:12', type: 'image', execute: true })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('dry-runs draft-clear by default and only clears with --execute', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-clear');
|
|
const dryRunPage = createPageMock([
|
|
{ ok: true, before: 3, after: 3, cleared: 0 },
|
|
]);
|
|
const executePage = createPageMock([
|
|
{ ok: true, before: 3, after: 0, cleared: 3 },
|
|
]);
|
|
|
|
await expect(command.func(dryRunPage, { type: 'all' })).resolves.toEqual([
|
|
{ status: 'dry-run', type: 'all', count: 3, message: 'Drafts counted. Re-run with --execute to clear.' },
|
|
]);
|
|
await expect(command.func(executePage, { type: 'all', execute: true })).resolves.toEqual([
|
|
{ status: 'cleared', type: 'all', count: 3, message: 'Drafts cleared.' },
|
|
]);
|
|
expect(dryRunPage.evaluate.mock.calls[0][0]).toContain('return { ok: true, before, after: before, cleared: 0 }');
|
|
expect(executePage.evaluate.mock.calls[0][0]).toContain('clearStore(db, storeName)');
|
|
});
|
|
|
|
it('fails closed when draft-clear leaves drafts behind', async () => {
|
|
const command = getRegistry().get('xiaohongshu/draft-clear');
|
|
const page = createPageMock([
|
|
{ ok: true, before: 3, after: 1, cleared: 2 },
|
|
]);
|
|
|
|
await expect(command.func(page, { type: 'image', execute: true })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
});
|