Files
jackwener--opencli/clis/github-trending/repos.test.js
T
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

150 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getRegistry } from '@jackwener/opencli/registry';
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { afterEach, describe, expect, it, vi } from 'vitest';
import './repos.js';
function loadCommand() {
const cmd = getRegistry().get('github-trending/repos');
if (!cmd?.func) throw new Error('github-trending/repos not found or has no func');
return cmd;
}
function article({ repo, desc, lang, stars, forks, since }) {
const langSpan = lang ? `<span itemprop="programmingLanguage">${lang}</span>` : '';
const descP = desc == null ? '' : `<p class="col-9 color-fg-muted my-1 tmp-pr-4">\n ${desc}\n </p>`;
return `<article class="Box-row">
<h2 class="h3 lh-condensed">
<a href="/${repo}" data-view-component="true" class="Link">
<span class="text-normal">${repo.split('/')[0]} /</span>
${repo.split('/')[1]}</a>
</h2>
${descP}
<div class="f6 color-fg-muted mt-2">
${langSpan}
<a href="/${repo}/stargazers" class="Link Link--muted d-inline-block"><svg></svg>\n ${stars}</a>
<a href="/${repo}/forks" class="Link Link--muted d-inline-block"><svg></svg>\n ${forks}</a>
<span class="d-inline-block float-sm-right"><svg></svg> ${since} stars today</span>
</div>
</article>`;
}
function pageHtml(articles) {
return `<!DOCTYPE html><html><body><div class="Box">${articles.join('\n')}</div></body></html>`;
}
function mockHtmlOnce(html, { ok = true, status = 200 } = {}) {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok,
status,
statusText: 'OK',
text: vi.fn().mockResolvedValue(html),
}));
}
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('github-trending/repos', () => {
it('parses repos, stars, forks, language, description and url', async () => {
const html = pageHtml([
article({ repo: 'owner-a/repo-a', desc: 'Tools &amp; toys for the web', lang: 'Rust', stars: '1,234', forks: '56', since: '78' }),
article({ repo: 'owner-b/repo-b', desc: null, lang: null, stars: '9', forks: '0', since: '3' }),
]);
mockHtmlOnce(html);
const rows = await loadCommand().func({ since: 'daily', language: '', limit: 25 });
expect(rows).toEqual([
{ rank: 1, repo: 'owner-a/repo-a', description: 'Tools & toys for the web', language: 'Rust', stars: 1234, forks: 56, starsSince: 78, url: 'https://github.com/owner-a/repo-a' },
{ rank: 2, repo: 'owner-b/repo-b', description: '', language: null, stars: 9, forks: 0, starsSince: 3, url: 'https://github.com/owner-b/repo-b' },
]);
});
it('honors --limit by truncating the result set', async () => {
const html = pageHtml([
article({ repo: 'a/a', desc: 'a', lang: 'Go', stars: '1', forks: '1', since: '1' }),
article({ repo: 'b/b', desc: 'b', lang: 'Go', stars: '2', forks: '2', since: '2' }),
article({ repo: 'c/c', desc: 'c', lang: 'Go', stars: '3', forks: '3', since: '3' }),
]);
mockHtmlOnce(html);
const rows = await loadCommand().func({ since: 'daily', language: '', limit: 2 });
expect(rows.map((r) => r.repo)).toEqual(['a/a', 'b/b']);
});
it('builds the language + since URL', async () => {
const fetchFn = vi.fn().mockResolvedValue({
ok: true,
status: 200,
text: vi.fn().mockResolvedValue(pageHtml([
article({ repo: 'x/y', desc: 'z', lang: 'Rust', stars: '1', forks: '1', since: '1' }),
])),
});
vi.stubGlobal('fetch', fetchFn);
await loadCommand().func({ since: 'weekly', language: 'rust', limit: 25 });
const calledUrl = String(fetchFn.mock.calls[0][0]);
expect(calledUrl).toBe('https://github.com/trending/rust?since=weekly');
});
it('rejects an unknown --since', async () => {
await expect(loadCommand().func({ since: 'yearly', language: '', limit: 25 }))
.rejects.toThrow(/since/i);
});
it('rejects a non-positive --limit', async () => {
await expect(loadCommand().func({ since: 'daily', language: '', limit: 0 }))
.rejects.toThrow(/limit/i);
});
it('rejects --limit above 25', async () => {
await expect(loadCommand().func({ since: 'daily', language: '', limit: 30 }))
.rejects.toThrow(/limit/i);
});
it('throws EmptyResultError when no repositories are present', async () => {
mockHtmlOnce('<main><h2>It looks like we dont have any trending repositories for your choices.</h2></main>');
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(EmptyResultError);
});
it('fails closed when GitHub row selectors drift without an explicit empty marker', async () => {
mockHtmlOnce('<main><div class="Box">Trending repositories</div></main>');
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(CommandExecutionError);
});
it('fails closed when a repository row has no repo identity', async () => {
mockHtmlOnce(pageHtml([article({ repo: 'owner-a/repo-a', desc: 'x', lang: 'Rust', stars: '1', forks: '2', since: '3' })
.replace('href="/owner-a/repo-a"', 'href="https://evil.example/owner-a/repo-a"')]));
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(CommandExecutionError);
});
it('fails closed when stars, forks, or period stars stop parsing', async () => {
const valid = article({ repo: 'owner-a/repo-a', desc: 'x', lang: 'Rust', stars: '1', forks: '2', since: '3' });
mockHtmlOnce(pageHtml([valid.replace('1</a>', 'many</a>')]));
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(CommandExecutionError);
mockHtmlOnce(pageHtml([valid.replace('2</a>', 'some</a>')]));
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(CommandExecutionError);
mockHtmlOnce(pageHtml([valid.replace('3 stars today', 'several stars today')]));
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(CommandExecutionError);
});
it('throws on a non-ok HTTP response', async () => {
mockHtmlOnce('', { ok: false, status: 503 });
await expect(loadCommand().func({ since: 'daily', language: '', limit: 25 }))
.rejects.toThrow(/HTTP 503/);
});
});