// The reader a page gets answers every read the in-page reader does, and // sends each to the server's method where the server serves the reads. import { beforeEach, describe, expect, it, vi } from 'vitest'; const READS = [ 'listTree', 'listFiles', 'readFile', 'readImage', 'listCommits', 'readCommit', 'lastCommit', ]; /** The methods a server that serves every read advertises. */ const READS_SERVED = [ 'getTree', 'listFiles', 'getFile', 'getRaw', 'listCommits', 'getCommit', 'getLastCommit', ]; async function load() { vi.resetModules(); const git = await import('../src/lib/git.js'); git.account.did = 'did:plc:owner'; git.account.pds = 'https://pds.example.com'; git.account.handle = 'owner.example.com'; return git; } /** @type {string[]} */ let asked = []; beforeEach(() => { asked = []; vi.unstubAllGlobals(); // The git service decision probes this page's own origin first; a probe // this stub refuses leaves the account's PDS as the service. vi.stubGlobal('location', new URL('https://site.example/')); vi.stubGlobal('fetch', async (/** @type {string|URL|Request} */ input) => { const url = String( typeof input === 'object' && 'url' in input ? input.url : input, ); asked.push(url); if (url.includes('dev.pdsjs.git.listRepos')) { return new Response('no', { status: 404 }); } if (url.includes('community.lexicon.service.describe')) { return Response.json({ methods: READS_SERVED.map((name) => ({ value: `dev.pdsjs.git.${name}`, })), }); } if (url.includes('dev.pdsjs.git.getRaw')) { return new Response('png', { headers: { 'Content-Type': 'image/png' } }); } return Response.json({ ref: 'refs/heads/main', sha: 'a'.repeat(40), path: '', entries: [], files: [], commits: [], commit: null, parents: [], truncated: false, }); }); }); describe('readerFor', () => { it('answers every read the pages make', async () => { const { readerFor } = await load(); const reader = readerFor('repo'); for (const method of READS) { expect(typeof reader[method], method).toBe('function'); } }); it('sends each read to its method', async () => { const { readerFor } = await load(); const reader = readerFor('repo'); await reader.listTree('did:plc:owner', 'repo', 'main', 'src'); await reader.listFiles('did:plc:owner', 'repo', 'main'); await reader.readFile('did:plc:owner', 'repo', 'main', 'a.txt'); const image = await reader.readImage( 'did:plc:owner', 'repo', 'main', 'a.png', ); await reader.listCommits('did:plc:owner', 'repo', 'main', 20); await reader.readCommit('did:plc:owner', 'repo', 'a'.repeat(40)); await reader.lastCommit('did:plc:owner', 'repo', 'main', 'a.txt'); const methods = asked .filter((url) => url.includes('/xrpc/dev.pdsjs.git.')) // The service decision probes listRepos before the first read. .filter((url) => !url.includes('listRepos')) .map((url) => new URL(url).pathname.split('.').pop()); expect(methods).toEqual([ 'getTree', 'listFiles', 'getFile', 'getRaw', 'listCommits', 'getCommit', 'getLastCommit', ]); expect(image.mediaType).toBe('image/png'); expect(new TextDecoder().decode(image.bytes)).toBe('png'); expect(asked.find((url) => url.includes('getTree'))).toContain( 'repo=repo&ref=main&path=src', ); }); it('does not take a single-page fallback for the git service', async () => { // A host serving the app answers every path with the app's own HTML and // a 200, listRepos included. The probe has to read the answer's type, // or every later read is sent to a page that cannot answer it. const { readerFor } = await load(); vi.stubGlobal('fetch', async (/** @type {string|URL|Request} */ input) => { const url = String( typeof input === 'object' && 'url' in input ? input.url : input, ); asked.push(url); if (url.startsWith('https://site.example/')) { return new Response('app', { headers: { 'Content-Type': 'text/html' }, }); } if (url.includes('dev.pdsjs.git.listRepos')) { return Response.json({ repos: [] }); } if (url.includes('community.lexicon.service.describe')) { return Response.json({ methods: READS_SERVED.map((name) => ({ value: `dev.pdsjs.git.${name}`, })), }); } return Response.json({ path: '', entries: [] }); }); await readerFor('repo').listTree('did:plc:owner', 'repo', 'main', ''); const tree = asked.find((url) => url.includes('getTree')); expect(tree).toContain('https://pds.example.com/'); }); it('says where a read went, for the wait a page shows', async () => { const { readerFor, readSource, onRepoProgress } = await load(); expect(readSource()).toBeNull(); let told = 0; onRepoProgress(() => told++); await readerFor('repo').listTree('did:plc:owner', 'repo', 'main', ''); expect(readSource()).toBe('server'); expect(told).toBe(1); }); });