9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
171 lines
7.4 KiB
JavaScript
171 lines
7.4 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
import { extractRedditMedia, parseRedditHomeLimit } from './home.js';
|
|
import './home.js';
|
|
|
|
function makePage(result) {
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
evaluate: vi.fn().mockResolvedValue(result),
|
|
};
|
|
}
|
|
|
|
function makeEntry(id, overrides = {}) {
|
|
return {
|
|
data: {
|
|
id,
|
|
title: `Title for ${id}`,
|
|
subreddit_name_prefixed: 'r/dummy',
|
|
score: 100,
|
|
num_comments: 10,
|
|
author: 'someone',
|
|
permalink: `/r/dummy/comments/${id}/title/`,
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('reddit home command', () => {
|
|
const command = getRegistry().get('reddit/home');
|
|
|
|
it('registers with the expected shape', () => {
|
|
expect(command).toBeDefined();
|
|
expect(command.access).toBe('read');
|
|
expect(command.browser).toBe(true);
|
|
expect(command.columns).toEqual([
|
|
'rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url',
|
|
'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls',
|
|
]);
|
|
});
|
|
|
|
it('parseRedditHomeLimit accepts [1,100] and rejects out-of-range / non-integer without silent clamp', () => {
|
|
expect(parseRedditHomeLimit(undefined)).toBe(25);
|
|
expect(parseRedditHomeLimit(null)).toBe(25);
|
|
expect(parseRedditHomeLimit('')).toBe(25);
|
|
expect(parseRedditHomeLimit(1)).toBe(1);
|
|
expect(parseRedditHomeLimit(25)).toBe(25);
|
|
expect(parseRedditHomeLimit(100)).toBe(100);
|
|
|
|
for (const bad of [0, -1, 101, 1.5, NaN, 'abc']) {
|
|
expect(() => parseRedditHomeLimit(bad)).toThrow(ArgumentError);
|
|
}
|
|
});
|
|
|
|
it('rejects a bad limit BEFORE navigating', async () => {
|
|
const page = makePage({ kind: 'ok', entries: [] });
|
|
await expect(command.func(page, { limit: 101 })).rejects.toBeInstanceOf(ArgumentError);
|
|
expect(page.goto).not.toHaveBeenCalled();
|
|
expect(page.evaluate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('throws AuthRequiredError when logged out (401/403 or missing identity)', async () => {
|
|
await expect(command.func(makePage({ kind: 'auth', detail: 'login required' }), { limit: 25 }))
|
|
.rejects.toBeInstanceOf(AuthRequiredError);
|
|
});
|
|
|
|
it('throws CommandExecutionError on HTTP / exception failure modes', async () => {
|
|
await expect(command.func(makePage({ kind: 'http', httpStatus: 503, where: '/best.json' }), { limit: 25 }))
|
|
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
await expect(command.func(makePage({ kind: 'exception', detail: 'network' }), { limit: 25 }))
|
|
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('throws EmptyResultError when Reddit returns no posts', async () => {
|
|
await expect(command.func(makePage({ kind: 'ok', entries: [] }), { limit: 25 }))
|
|
.rejects.toBeInstanceOf(EmptyResultError);
|
|
});
|
|
|
|
it('maps entries to row shape with 1-based rank, full URLs, and typed numbers', async () => {
|
|
const entries = [makeEntry('a1'), makeEntry('b2', { score: 250, num_comments: 42 })];
|
|
const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 25 });
|
|
|
|
expect(rows).toEqual([
|
|
{
|
|
rank: 1, title: 'Title for a1', subreddit: 'r/dummy', score: 100, comments: 10,
|
|
postId: 'a1', author: 'someone', url: 'https://www.reddit.com/r/dummy/comments/a1/title/',
|
|
post_hint: '', url_overridden_by_dest: '', preview_image_url: '', gallery_urls: [],
|
|
},
|
|
{
|
|
rank: 2, title: 'Title for b2', subreddit: 'r/dummy', score: 250, comments: 42,
|
|
postId: 'b2', author: 'someone', url: 'https://www.reddit.com/r/dummy/comments/b2/title/',
|
|
post_hint: '', url_overridden_by_dest: '', preview_image_url: '', gallery_urls: [],
|
|
},
|
|
]);
|
|
// Row shape must match declared columns exactly.
|
|
for (const row of rows) {
|
|
expect(Object.keys(row).sort()).toEqual(
|
|
[
|
|
'author', 'comments', 'gallery_urls', 'postId', 'post_hint', 'preview_image_url',
|
|
'rank', 'score', 'subreddit', 'title', 'url', 'url_overridden_by_dest',
|
|
],
|
|
);
|
|
}
|
|
});
|
|
|
|
it('surfaces media route fields from the personalized home feed', async () => {
|
|
const entries = [makeEntry('a1', {
|
|
post_hint: 'image',
|
|
url_overridden_by_dest: 'https://i.redd.it/a.jpg',
|
|
preview: {
|
|
images: [{ source: { url: 'https://preview.redd.it/a.jpg?x=1&y=2' } }],
|
|
},
|
|
gallery_data: { items: [{ media_id: 'm2' }, { media_id: 'm1' }] },
|
|
media_metadata: {
|
|
m1: { s: { u: 'https://preview.redd.it/m1.jpg?x=1&y=1' } },
|
|
m2: { s: { u: 'https://preview.redd.it/m2.jpg?x=1&y=2' } },
|
|
},
|
|
})];
|
|
const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 25 });
|
|
|
|
expect(rows[0]).toMatchObject({
|
|
post_hint: 'image',
|
|
url_overridden_by_dest: 'https://i.redd.it/a.jpg',
|
|
preview_image_url: 'https://preview.redd.it/a.jpg?x=1&y=2',
|
|
gallery_urls: [
|
|
'https://preview.redd.it/m2.jpg?x=1&y=2',
|
|
'https://preview.redd.it/m1.jpg?x=1&y=1',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('extractRedditMedia tolerates missing media without throwing', () => {
|
|
expect(extractRedditMedia({ is_self: true })).toEqual({
|
|
post_hint: '',
|
|
url_overridden_by_dest: '',
|
|
preview_image_url: '',
|
|
gallery_urls: [],
|
|
});
|
|
});
|
|
|
|
it('applies the post-fetch limit slice (defence in depth vs Reddit overshoot)', async () => {
|
|
const entries = Array.from({ length: 30 }, (_, i) => makeEntry(`p${i}`));
|
|
const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 5 });
|
|
expect(rows).toHaveLength(5);
|
|
expect(rows[0].postId).toBe('p0');
|
|
expect(rows[4].postId).toBe('p4');
|
|
});
|
|
|
|
it('drops entries with no data.id rather than silently emitting sentinels', async () => {
|
|
const entries = [makeEntry('keep1'), { data: { title: 'no id' } }, makeEntry('keep2')];
|
|
const rows = await command.func(makePage({ kind: 'ok', entries }), { limit: 25 });
|
|
expect(rows.map((r) => r.postId)).toEqual(['keep1', 'keep2']);
|
|
});
|
|
|
|
it('throws CommandExecutionError when all home entries are missing post ids', async () => {
|
|
const entries = [{ data: { title: 'no id' } }, { data: { title: 'also no id' } }];
|
|
await expect(command.func(makePage({ kind: 'ok', entries }), { limit: 25 }))
|
|
.rejects.toMatchObject({
|
|
code: 'COMMAND_EXEC',
|
|
message: expect.stringContaining('required post id anchors'),
|
|
});
|
|
});
|
|
|
|
it('embeds the requested limit literally inside the evaluate script', async () => {
|
|
const page = makePage({ kind: 'ok', entries: [makeEntry('x')] });
|
|
await command.func(page, { limit: 7 });
|
|
const script = page.evaluate.mock.calls[0][0];
|
|
expect(script).toContain('const limit = 7');
|
|
});
|
|
});
|