mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
import { createServer } from 'node:net'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { deepLinks } from '../../next.config.mjs'
|
|
|
|
/**
|
|
* A DEEP LINK RELOADS. `_fallback.json` declares which path prefixes are an
|
|
* app's routes and which page answers them; the site edge (hanzo-inc/cloud
|
|
* apps/sites), `scripts/serve.mjs` and the dev rewrites read the same file, so
|
|
* /chat/<id> reloaded renders the chat page everywhere instead of a 404.
|
|
*
|
|
* serve.mjs is driven over a real socket against a throwaway build: a declared
|
|
* deep path answers its page 200; a sibling that only shares letters, a path
|
|
* with an extension, and every path of a build with no table stay 404; a file
|
|
* that exists still wins.
|
|
*/
|
|
|
|
const free = () =>
|
|
new Promise<number>((ok) => {
|
|
const s = createServer().listen(0, () => {
|
|
const { port } = s.address() as { port: number }
|
|
s.close(() => ok(port))
|
|
})
|
|
})
|
|
|
|
async function serve(files: Record<string, string>) {
|
|
const root = mkdtempSync(join(tmpdir(), 'fallback-'))
|
|
for (const [name, body] of Object.entries(files)) writeFileSync(join(root, name), body)
|
|
const port = await free()
|
|
const child: ChildProcess = spawn(process.execPath, ['scripts/serve.mjs', root, String(port)], { stdio: 'pipe' })
|
|
await new Promise<void>((ok, fail) => {
|
|
child.stdout!.on('data', () => ok())
|
|
child.on('exit', (code) => fail(new Error(`serve.mjs exited ${code}`)))
|
|
})
|
|
const get = async (path: string) => {
|
|
const r = await fetch(`http://127.0.0.1:${port}${path}`)
|
|
return [r.status, await r.text()] as const
|
|
}
|
|
return {
|
|
get,
|
|
stop: () => {
|
|
child.kill()
|
|
rmSync(root, { recursive: true, force: true })
|
|
},
|
|
}
|
|
}
|
|
|
|
const build = {
|
|
'index.html': 'home',
|
|
'chat.html': 'chat page',
|
|
'dev.html': 'dev page',
|
|
'chat.js': 'chunk',
|
|
}
|
|
|
|
test('a declared deep path answers its page, and nothing else does', async () => {
|
|
const s = await serve({ ...build, '_fallback.json': JSON.stringify({ '/chat': 'chat.html', '/dev/': '/dev.html' }) })
|
|
try {
|
|
expect(await s.get('/chat/abc')).toEqual([200, 'chat page'])
|
|
expect(await s.get('/chat/abc/def')).toEqual([200, 'chat page'])
|
|
expect(await s.get('/dev/runs/r1')).toEqual([200, 'dev page'])
|
|
expect(await s.get('/chat')).toEqual([200, 'chat page'])
|
|
expect(await s.get('/chat.js')).toEqual([200, 'chunk'])
|
|
expect((await s.get('/chatter'))[0]).toBe(404)
|
|
expect((await s.get('/chat/missing.js'))[0]).toBe(404)
|
|
expect((await s.get('/elsewhere/x'))[0]).toBe(404)
|
|
} finally {
|
|
s.stop()
|
|
}
|
|
})
|
|
|
|
test('a build without a table, or with one that is not a table, declares nothing', async () => {
|
|
for (const table of [undefined, '[]', '{"/chat": "chat.js"}', '{"/chat": 1}', 'not json']) {
|
|
const s = await serve(table === undefined ? build : { ...build, '_fallback.json': table })
|
|
try {
|
|
expect((await s.get('/chat/abc'))[0], String(table)).toBe(404)
|
|
} finally {
|
|
s.stop()
|
|
}
|
|
}
|
|
})
|
|
|
|
test('a declared page cannot name a file outside the build', async () => {
|
|
const s = await serve({ ...build, '_fallback.json': JSON.stringify({ '/x': '../../../etc/passwd.html' }) })
|
|
try {
|
|
expect((await s.get('/x/y'))[0]).toBe(404)
|
|
} finally {
|
|
s.stop()
|
|
}
|
|
})
|
|
|
|
test('dev rewrites every declared prefix to its page, after files', () => {
|
|
expect(deepLinks({ '/chat': 'chat.html', '/dev/': '/dev.html', '/': 'index.html' })).toEqual([
|
|
{ source: '/chat/:rest*', destination: '/chat' },
|
|
{ source: '/dev/:rest*', destination: '/dev' },
|
|
{ source: '/:rest*', destination: '/index' },
|
|
])
|
|
expect(deepLinks().map((r) => r.source)).toContain('/chat/:rest*')
|
|
})
|