- static/board.js: full rerender on toggle-grouping; by-project mode = N cols per project with status-badge inside cards, sorted by canonical status order - static/board.css: drop placeholder, add .badge.status - src/gitea.ts: getLatestCommitIso returns null on 404 (path never existed remotely; seen on books repo where some per-task slugs in STATUS.md don't have file in default branch) + new test - .gitignore: dist/ already present; add .playwright-mcp/, board-*.png Smoke-verified: dist/index.html, 78 records from 4 repos, both toggle modes work in browser, drawer lazy-fetches markdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
import { describe, expect, test, vi } from 'vitest';
|
|
import { createGiteaClient } from '../src/gitea.ts';
|
|
|
|
type MockFetch = ReturnType<typeof vi.fn>;
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
function makeClient(mockFetch: MockFetch) {
|
|
return createGiteaClient({
|
|
baseUrl: 'https://git.kzntsv.site',
|
|
token: 'test-token',
|
|
fetch: mockFetch as unknown as typeof fetch,
|
|
});
|
|
}
|
|
|
|
describe('GiteaClient.getFile', () => {
|
|
test('hits /api/v1/repos/<owner>/<repo>/contents/<path> with Authorization header', async () => {
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(
|
|
jsonResponse({
|
|
content: Buffer.from('hello world', 'utf8').toString('base64'),
|
|
encoding: 'base64',
|
|
}),
|
|
);
|
|
const client = makeClient(mockFetch);
|
|
|
|
await client.getFile('OpeItcLoc03', 'board-viewer', '.tasks/STATUS.md');
|
|
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
const [url, init] = mockFetch.mock.calls[0]!;
|
|
expect(url).toBe(
|
|
'https://git.kzntsv.site/api/v1/repos/OpeItcLoc03/board-viewer/contents/.tasks/STATUS.md',
|
|
);
|
|
expect((init as RequestInit).headers).toMatchObject({
|
|
Authorization: 'token test-token',
|
|
});
|
|
});
|
|
|
|
test('decodes base64 content and returns text', async () => {
|
|
const original = 'STATUS.md body\nwith newline.';
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(
|
|
jsonResponse({
|
|
content: Buffer.from(original, 'utf8').toString('base64'),
|
|
encoding: 'base64',
|
|
}),
|
|
);
|
|
const client = makeClient(mockFetch);
|
|
|
|
const result = await client.getFile('owner', 'repo', 'file.md');
|
|
|
|
expect(result).toBe(original);
|
|
});
|
|
|
|
test('returns null on 404 (file absent)', async () => {
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(
|
|
new Response('Not Found', { status: 404 }),
|
|
);
|
|
const client = makeClient(mockFetch);
|
|
|
|
const result = await client.getFile('owner', 'repo', 'missing.md');
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test('throws on non-404 HTTP error (e.g. 500)', async () => {
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(
|
|
new Response('Server error', { status: 500 }),
|
|
);
|
|
const client = makeClient(mockFetch);
|
|
|
|
await expect(client.getFile('owner', 'repo', 'file.md')).rejects.toThrow(/500/);
|
|
});
|
|
});
|
|
|
|
describe('GiteaClient.getLatestCommitIso', () => {
|
|
test('returns commit date ISO string when commit exists', async () => {
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(
|
|
jsonResponse([
|
|
{ commit: { author: { date: '2026-05-22T08:10:00Z' } } },
|
|
]),
|
|
);
|
|
const client = makeClient(mockFetch);
|
|
|
|
const iso = await client.getLatestCommitIso('owner', 'repo', '.tasks/foo.md');
|
|
|
|
expect(iso).toBe('2026-05-22T08:10:00Z');
|
|
const [url] = mockFetch.mock.calls[0]!;
|
|
expect(url).toMatch(
|
|
/\/api\/v1\/repos\/owner\/repo\/commits\?path=\.tasks%2Ffoo\.md&limit=1/,
|
|
);
|
|
});
|
|
|
|
test('returns null when no commits touch the path', async () => {
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(jsonResponse([]));
|
|
const client = makeClient(mockFetch);
|
|
|
|
expect(await client.getLatestCommitIso('o', 'r', 'p.md')).toBeNull();
|
|
});
|
|
|
|
test('returns null when Gitea responds 404 (path never existed)', async () => {
|
|
const mockFetch: MockFetch = vi.fn().mockResolvedValue(
|
|
new Response('Not Found', { status: 404 }),
|
|
);
|
|
const client = makeClient(mockFetch);
|
|
|
|
expect(await client.getLatestCommitIso('o', 'r', 'missing.md')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('GiteaClient.rawUrl', () => {
|
|
test('builds the raw URL for a file on the default branch', () => {
|
|
const client = makeClient(vi.fn());
|
|
|
|
const url = client.rawUrl('OpeItcLoc03', 'board-viewer', '.tasks/STATUS.md');
|
|
|
|
expect(url).toBe(
|
|
'https://git.kzntsv.site/OpeItcLoc03/board-viewer/raw/branch/master/.tasks/STATUS.md',
|
|
);
|
|
});
|
|
});
|