Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

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);
});
});