diff --git a/.gitignore b/.gitignore index e6e95ca9..dafdcdea 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ pnpm-global TODOs.md *.timestamp-*.mjs .claude + +# base fixture builds +__tests__/base/fixture/.vitepress/dist-* diff --git a/__tests__/base/cdn.test.ts b/__tests__/base/cdn.test.ts new file mode 100644 index 00000000..60657877 --- /dev/null +++ b/__tests__/base/cdn.test.ts @@ -0,0 +1,63 @@ +import { newPage, realErrors, waitForHydration, type TestPage } from './helpers' + +const origin = () => `http://localhost:${process.env['PAGES_PORT']}` +const cdnPort = () => process.env['VP_CDN_PORT'] + +let t: TestPage + +beforeAll(async () => { + t = await newPage() +}) + +afterAll(async () => { + await t.page.close() + await t.browser.close() +}) + +describe('assetsBase with a separate cdn origin', () => { + test('pages hydrate from cross-origin assets', async () => { + await t.page.goto(`${origin()}/`) + await waitForHydration(t.page) + const cdnResources = await t.page.evaluate( + (port) => + performance + .getEntriesByType('resource') + .filter((r) => r.name.includes(`:${port}/`)).length, + cdnPort() + ) + expect(cdnResources).toBeGreaterThan(5) + }) + + test('client-side navigation loads page chunks from the cdn', async () => { + await t.page.evaluate(() => ((window as any).__spa_marker = 1)) + await t.page.click('.vp-doc a[href="/sub/page.html"]') + await t.page.waitForFunction( + () => document.querySelector('h1')?.textContent?.includes('Sub page') + ) + expect( + await t.page.evaluate(() => (window as any).__spa_marker === 1) + ).toBe(true) + const chunkFromCdn = await t.page.evaluate( + (port) => + performance + .getEntriesByType('resource') + .some((r) => r.name.includes(`:${port}/`) && r.name.includes('.md.')), + cdnPort() + ) + expect(chunkFromCdn).toBe(true) + }) + + test('search works with the index chunk on the cdn', async () => { + await t.page.click('.VPNavBarSearchButton') + const input = await t.page.waitForSelector('input#localsearch-input') + await input.type('xylophone') + await t.page.waitForSelector('#localsearch-list li[role=option] a') + expect( + await t.page.getAttribute('#localsearch-list li[role=option] a', 'href') + ).toBe('/sub/deep/page2.html#deep-heading') + }) + + test('no console or page errors across the whole flow', () => { + expect(realErrors(t.errors)).toEqual([]) + }) +}) diff --git a/__tests__/base/constants.ts b/__tests__/base/constants.ts new file mode 100644 index 00000000..a2d5bcbe --- /dev/null +++ b/__tests__/base/constants.ts @@ -0,0 +1,2 @@ +export const SUB_PREFIX = '/ipfs/QmRelocatableTest123/' +export const ALT_PREFIX = '/some/other/place/' diff --git a/__tests__/base/emit.test.ts b/__tests__/base/emit.test.ts new file mode 100644 index 00000000..e217fce5 --- /dev/null +++ b/__tests__/base/emit.test.ts @@ -0,0 +1,171 @@ +import { readFileSync, readdirSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const dir = resolve(fileURLToPath(import.meta.url), '..') +const dist = (mode: string, ...p: string[]) => + join(dir, `fixture/.vitepress/dist-${mode}`, ...p) +const read = (mode: string, file: string) => + readFileSync(dist(mode, file), 'utf-8') + +const walk = (root: string): string[] => + readdirSync(root, { recursive: true, withFileTypes: true }) + .filter((e) => e.isFile()) + .map((e) => join(e.parentPath, e.name)) + +describe('relative base emit', () => { + test('root page references everything at ./', () => { + const html = read('relative', 'index.html') + expect(html).toContain( + 'window.__VP_SITE_ROOT__=new URL("./",location).href' + ) + expect(html).toMatch(/href="\.\/assets\/style\.[\w-]+\.css"/) + expect(html).toMatch(/src="\.\/assets\/app\.[\w-]+\.js"/) + expect(html).toMatch(/src="\.\/assets\/chunks\/metadata\.[\w-]+\.js"/) + expect(html).toContain('href="./vp-icons.css"') + }) + + test('markdown links compile page-relative with explicit index.html', () => { + const html = read('relative', 'index.html') + expect(html).toContain('href="./sub/page.html"') + expect(html).toContain('href="./sub/index.html"') + expect(html).toContain('href="./moved/target.html"') + }) + + test('non-page links get the prefix but no .html', () => { + const html = read('relative', 'index.html') + expect(html).toContain('href="./file.zip"') + expect(html).not.toContain('file.zip.html') + }) + + test('public and hashed assets in content are page-relative', () => { + const html = read('relative', 'index.html') + expect(html).toContain('src="./logo.png"') + expect(html).toMatch(/src="\.\/assets\/photo\.[\w-]+\.png"/) + }) + + test('depth 1 pages use ../', () => { + const html = read('relative', 'sub/page.html') + expect(html).toContain( + 'window.__VP_SITE_ROOT__=new URL("../",location).href' + ) + expect(html).toMatch(/href="\.\.\/assets\/style\.[\w-]+\.css"/) + expect(html).toContain('href="../vp-icons.css"') + expect(html).toContain('src="../logo.png"') + expect(html).toContain('href="../index.html"') + expect(html).toContain('href="../sub/deep/page2.html"') + }) + + test('hash and external links stay untouched', () => { + const html = read('relative', 'sub/page.html') + expect(html).toContain('href="#local-anchor"') + expect(html).toContain('href="https://example.com/x"') + }) + + test('depth 2 pages use ../../', () => { + const html = read('relative', 'sub/deep/page2.html') + expect(html).toContain( + 'window.__VP_SITE_ROOT__=new URL("../../",location).href' + ) + expect(html).toMatch(/href="\.\.\/\.\.\/assets\/style\.[\w-]+\.css"/) + }) + + test('rewritten page lands at its rewrite depth', () => { + const html = read('relative', 'moved/target.html') + expect(html).toContain( + 'window.__VP_SITE_ROOT__=new URL("../",location).href' + ) + expect(html).toMatch(/href="\.\.\/assets\/style\.[\w-]+\.css"/) + }) + + test('404 renders at root depth', () => { + const html = read('relative', '404.html') + expect(html).toContain( + 'window.__VP_SITE_ROOT__=new URL("./",location).href' + ) + expect(html).toMatch(/href="\.\/assets\/style\.[\w-]+\.css"/) + }) + + test('no sentinel leaks into emitted html or css', () => { + for (const file of walk(dist('relative'))) { + if (!/\.(html|css)$/.test(file)) continue + expect(readFileSync(file, 'utf-8'), file).not.toContain('__VP_BASE__') + } + }) +}) + +describe('assetsBase emit', () => { + const cdn = () => `http://localhost:${process.env['VP_CDN_PORT']}/` + + test('scripts, styles and preloads move to the cdn with crossorigin', () => { + const html = read('cdn', 'index.html') + expect(html).toMatch( + new RegExp( + `