[web] focus viewer tests on meaningful regressions #192

Merged
eleith merged 1 commits from refactor/asset-viewer-tests into main 2026-11-04 19:41:57 +00:00
19 changed files with 635 additions and 3899 deletions

No files matched your search

@@ -1,58 +0,0 @@
import { describe, expect, test } from 'vitest'
import { render } from 'svelte/server'
import { createPieceHelpers, type PublicWebPiece } from '@luzzle/web.pieces'
import { getIcon } from '@luzzle/web.templates/icons'
const IconDefault = getIcon('default')
const piece: PublicWebPiece = {
id: 'default-icon',
key: 'default-icon',
type: 'arbitrary-kind',
slug: 'default-icon',
title: '<Default>&',
date_added: 1,
metadata: { cover: 'cover' },
keywords: '[]',
assets: [
{
asset_key: 'cover',
transformation: 'image.original',
asset_path: 'cover.jpg',
mime_type: 'image/jpeg'
}
]
}
const helpers = createPieceHelpers(
piece.assets,
(path) => `/assets/${path}`,
() => '/piece'
)
describe('shipped static default icon', () => {
test('renders the poster UI with mapped fields and piece helpers and the default aspect ratio', () => {
const { body } = render(IconDefault, {
props: { piece, fields: { image: 'cover' }, size: { width: 80 }, active: false, helpers }
})
expect(body).toContain('class="poster')
expect(body).toContain('&lt;Default>&amp;')
expect(body).toContain('--piece-icon-height: 120px;')
expect(body).toContain('src="/assets/cover.jpg" loading="eager"')
})
test('honors explicit height, lazy loading and active styling', () => {
const { body } = render(IconDefault, {
props: {
piece,
fields: { image: 'cover' },
size: { width: 80, height: 55 },
lazy: true,
active: true,
helpers
}
})
expect(body).toContain('--piece-icon-height: 55px;')
expect(body).toMatch(/class="poster [^"]*\bactive\b/)
expect(body).toContain('loading="lazy" fetchpriority="auto"')
})
})
@@ -228,14 +228,13 @@ describe('static default icon consumer SSR', () => {
expect(body).not.toContain('<img')
})
test('uses each new piece and mapped field value rather than a fallback snapshot', async () => {
const current = piece('new-kind')
current.title = 'Next <Piece>'
test('a missing mapped image retains the title without emitting a broken image request', async () => {
const current = piece()
current.templates.icon.fields = { image: 'missing' }
const body = await html({ piece: current, size: { width: 200 }, active: true })
expect(body).toContain('Next &lt;Piece>')
expect(body).toContain('--piece-icon-width: 200px;')
expect(body).toMatch(/class="poster [^"]*\bactive\b/)
const body = await html({ piece: current })
expect(body).toContain('&lt;Piece>&amp;')
expect(body).not.toContain('<img')
})
})
@@ -1,29 +0,0 @@
import { readFileSync } from 'node:fs'
import { expect, test } from 'vitest'
import { parse } from 'svelte/compiler'
test.each([
{ role: 'icon', component: 'Icon', getter: 'getIcon', catalog: 'icons' },
{ role: 'page', component: 'Page', getter: 'getPage', catalog: 'pages' },
{ role: 'opengraph', component: 'Opengraph', getter: 'getOpengraph', catalog: 'opengraphs' }
])(
'$role uses reactive props and the shared static catalog, not prepared HTML or async dispatch',
({ role, component, getter, catalog }) => {
const source = readFileSync(new URL(`./${role}.svelte`, import.meta.url), 'utf8')
expect(source).toContain(`from '@luzzle/web.templates/${catalog}'`)
expect(source).toContain(`const ${component} = $derived(${getter}(`)
expect(source).toContain('const fields = $derived(')
expect(source).toContain('const helpers = $derived(getPieceHelpers(piece))')
expect(source).toContain(`<${component} {piece} {fields}`)
expect(source).not.toMatch(
/import\.meta\.glob|\bimport\s*\(|\{@html|\{#(?:await|key|if)\b|presentation|prepared/i
)
const content = parse(source, { modern: true }).fragment.nodes.filter(
(node) => node.type !== 'Text' && node.type !== 'SvelteHead'
)
expect(content.length).toBeGreaterThan(0)
for (const node of content) {
expect(['Component', 'RegularElement']).toContain(node.type)
}
}
)
+18 -77
View File
@@ -1,90 +1,31 @@
import { existsSync } from 'node:fs'
import { expect, test, vi } from 'vitest'
import {
editorHref,
encodeArchivePath,
previewHref,
publishedHref,
publishHref
} from './navigation'
import { expect, test } from 'vitest'
import { editorHref, previewHref, publishedHref, publishHref } from './navigation'
const { resolve } = vi.hoisted(() => ({
resolve: vi.fn(() => {
throw new Error('Could not get the request store. This is an internal error.')
})
}))
vi.mock('$app/paths', () => ({ resolve }))
test('archive filenames and preview jobs remain path segments, not URL queries or fragments', () => {
const file = 'books/雪 #?%.books.md'
const root = '/admin/piece/books/%E9%9B%AA%20%23%3F%25.books.md'
test('Published replaces Live without keeping a legacy handler or redirect alias', () => {
const routes = new URL('../../routes/(auth)/admin/piece/[...path]/', import.meta.url)
expect(existsSync(new URL('live/', routes))).toBe(false)
for (const file of [
'published/+layout.server.ts',
'published/+page.svelte',
'published/page/+page.svelte',
'published/icon/+page.svelte',
'published/share/+page.svelte'
])
expect(existsSync(new URL(file, routes)), file).toBe(true)
expect(editorHref(file)).toBe(`${root}/source`)
expect(publishedHref(file)).toBe(`${root}/published`)
expect(previewHref(file)).toBe(`${root}/preview`)
expect(previewHref(file, 'job #?%')).toBe(`${root}/preview/job%20%23%3F%25`)
})
test.each([
'books/源 #?%.books.md',
'folder with spaces/a book.books.md',
'nested/literal%2F.books.md',
'books/📚.books.md'
])(
'editor navigation encodes raw file %s and job segments once without a request store',
(file) => {
const root = `/admin/piece/${file.split('/').map(encodeURIComponent).join('/')}`
const job = 'job #?%'
const links = [editorHref(file), publishedHref(file), previewHref(file), previewHref(file, job)]
expect(links).toEqual([
`${root}/source`,
`${root}/published`,
`${root}/preview`,
`${root}/preview/job%20%23%3F%25`
])
for (const href of links) {
const url = new URL(href, 'https://app.test')
expect(url.search).toBe('')
expect(url.hash).toBe('')
expect(decodeURIComponent(url.pathname)).toContain(file)
}
expect(resolve).not.toHaveBeenCalled()
}
)
test('a percent-looking filename is encoded literally rather than becoming another path segment', () => {
expect(editorHref('books/literal%2F.books.md')).toBe(
'/admin/piece/books/literal%252F.books.md/source'
)
})
test('publish preselection retains the complete filename as one query value', () => {
const file = 'nested/name?check=0&file=other#fragment + 雪.books.md'
test.each([
'books/源 #?%&"+=.books.md',
'folder with spaces/a book.books.md',
'nested/literal%2F.books.md',
'books/📚.books.md',
'nested/name?check=0&file=other#fragment.books.md'
])('publish navigation preselects the complete raw filename %s', (file) => {
const url = new URL(publishHref(file), 'https://app.test')
expect(url.pathname).toBe('/admin/publish')
expect(url.hash).toBe('')
expect([...url.searchParams.entries()]).toEqual([
['check', '1'],
['file', file]
])
expect(resolve).not.toHaveBeenCalled()
})
test('publish navigation encodes a query value rather than a pathname', () => {
expect(publishHref('folder/a b+c.books.md')).toBe(
'/admin/publish?check=1&file=folder%2Fa+b%2Bc.books.md'
)
})
test.each([
['books/源 #?%.books.md', 'books/%E6%BA%90%20%23%3F%25.books.md'],
['folder with spaces/a book.books.md', 'folder%20with%20spaces/a%20book.books.md'],
['nested/literal%2F.books.md', 'nested/literal%252F.books.md'],
['books/📚.books.md', 'books/%F0%9F%93%9A.books.md'],
['published/page/icon/share/source/file.md', 'published/page/icon/share/source/file.md']
])('encodes raw piece path %s segment-wise without a request store', (file, encoded) => {
expect(encodeArchivePath(file)).toBe(encoded)
expect(resolve).not.toHaveBeenCalled()
})
File diff suppressed because it is too large. Load diff
@@ -26,34 +26,18 @@ vi.mock('$app/paths', () => ({
resolve: (route: string, params: Record<string, string>) =>
route.replace(/\[(?:\.\.\.)?(\w+)\]/g, (_, name: string) => params[name])
}))
// The synchronous renderer stub checks the viewport's SSR contract and real asset helpers.
// Node SSR cannot verify zoom layout, natural template heights, 3D paint/shadows,
// resize/orientation updates, or ResizeObserver mounting and cleanup in a browser.
// SSR establishes the initial composition, not client resize behavior.
vi.mock('#lib/pieces/components/icon.svelte', async () => ({
default: (await import('./fixtures/IconRenderer.svelte')).default
}))
test.each([
{ preview: false, src: '/cdn/pieces/assets/published/cover.jpg' },
{ preview: true, src: '/admin/preview/preview-job/asset/published/cover.jpg' }
])('SSR keeps eager 500px rendering and resource URLs (preview=$preview)', (state) => {
page.data.preview = state.preview
test('starts with an eager 500px composition without imposing a height on the selected icon', () => {
const html = render(IconViewport, { props: { piece } }).body
expect(html.match(/<main\b/g)).toHaveLength(1)
expect(html).toContain('id="main-content"')
expect(html).toContain('aria-label="Piece icon"')
expect(html.match(/<img\b/g)).toHaveLength(1)
expect(html).toContain('data-renderer="icon"')
expect(html).toContain('data-width="500"')
expect(html).toContain('loading="eager"')
expect(html).toContain(`src="${state.src}"`)
// SSR starts at full size; only the client can measure the available width.
expect(html).toContain('max-width: 500px')
const frame = html.match(/<div\b[^>]*class="icon-frame\b[^>]*>/)?.[0]
expect(frame).toBeDefined()
expect(frame).toContain('width: 500px; zoom: 1')
// Height belongs to the selected template, not a shared 750px canvas.
expect(frame).not.toContain('height:')
})
@@ -1,97 +0,0 @@
import { readFileSync } from 'node:fs'
import { expect, test } from 'vitest'
import { render } from 'svelte/server'
import { createRawSnippet } from 'svelte'
import { assetPathToUrl } from '#lib/pieces/assets.js'
import { asset } from './fixtures/published'
import PieceOverview from './PieceOverview.svelte'
test('shared overview owns title, facts slot and actual output inspector, not a main landmark or publication policy', () => {
const children = createRawSnippet(() => ({
render: () => '<div class="overview-facts"><p>Caller facts fixture</p></div>'
}))
const html = render(PieceOverview, {
props: {
title: 'Piece <title & "text">',
assets: [asset('image.original', 'cover.jpg')],
fileUrl: (path) => assetPathToUrl(path, '/cdn'),
children
}
}).body.replace(/<!--[^]*?-->/g, '')
expect(html).toContain('Piece &lt;title &amp; "text">')
expect(html).toContain('Caller facts fixture')
expect(html).toContain('Image</caption>')
expect(html).toContain('href="/cdn/pieces/assets/cover.jpg"')
expect(html).toContain('Outputs</h2>')
expect(html).not.toContain('<main')
expect(html).not.toContain('Last publish')
expect(html.indexOf('Caller facts fixture')).toBeLessThan(html.indexOf('Image</caption>'))
})
test('shared overview omits not-yet-produced assets and puts caller logs after its facts', () => {
const children = createRawSnippet(() => ({
render: () => '<p>Pending facts fixture</p><section><h2>Preview logs</h2></section>'
}))
const html = render(PieceOverview, {
props: {
title: 'Preview',
fileUrl: () => {
throw new Error('No asset callback expected')
},
children
}
}).body
expect(html).toContain('Pending facts fixture')
expect(html).toContain('Preview logs')
expect(html.indexOf('Pending facts fixture')).toBeLessThan(html.indexOf('Preview logs'))
expect(html).not.toContain('None recorded')
expect(html).not.toContain('Outputs</h2>')
expect(html).not.toContain('<main')
})
test('shared overview places caller facts and logs before actual output tables', () => {
const children = createRawSnippet(() => ({
render: () => '<p>Facts fixture</p><section><h2>Preview logs</h2></section>'
}))
const html = render(PieceOverview, {
props: {
title: 'Preview',
assets: [asset('image.original', 'cover.jpg')],
fileUrl: (path) => assetPathToUrl(path, '/cdn'),
children
}
}).body
expect(html).toContain('Image</caption>')
expect(html.indexOf('Facts fixture')).toBeLessThan(html.indexOf('Preview logs'))
expect(html.indexOf('Preview logs')).toBeLessThan(html.indexOf('Image</caption>'))
})
test('Preview and Published share the same badge styles and success/pending color rules', () => {
const source = readFileSync(new URL('./PieceOverview.svelte', import.meta.url), 'utf8')
const success = source.match(
/\[data-status='matching'\]\),\s*\.overview :global\(\.overview-facts \.status\[data-status='completed'\]\)\s*\{([^}]+)\}/
)?.[1]
expect(success).toContain('background: var(--color-primary-container)')
expect(success).toContain('color: var(--color-on-primary-container)')
const pending = source.match(
/\[data-status='changed'\]\),\s*\.overview :global\(\.overview-facts \.status\[data-status='running'\]\)\s*\{([^}]+)\}/
)?.[1]
expect(pending).toContain('background: var(--color-secondary-container)')
expect(pending).toContain('color: var(--color-on-secondary-container)')
})
test('shared overview can show empty preview outputs without guessing URLs or requiring publication facts', () => {
const html = render(PieceOverview, {
props: {
title: 'Preview title',
assets: [],
fileUrl: () => undefined,
assetsHeading: 'Preview assets'
}
}).body
expect(html).toContain('Preview title')
expect(html).toContain('Preview assets</h2>')
expect(html).toContain('None recorded')
expect(html).not.toContain('<a ')
expect(html).not.toContain('Publication')
})
@@ -1,65 +0,0 @@
import { expect, test, vi } from 'vitest'
import { render } from 'svelte/server'
import { OpengraphImageWidth, OpengraphImageHeight } from '@luzzle/web.pieces'
import ShareViewport from './ShareViewport.svelte'
import { publishedPiece as rawPublishedPiece, asset } from './fixtures/published'
const publishedPiece = {
...rawPublishedPiece,
templates: {
icon: { name: 'default', fields: { image: 'cover' } },
page: { name: 'default', fields: { image: 'cover' } },
opengraph: { name: 'default', fields: { image: 'cover' } }
}
}
vi.mock('$app/state', () => ({
page: { data: { config: { url: { app: '/garden', luzzle_assets: '/cdn' } } } }
}))
vi.mock('$app/paths', () => ({
resolve: (route: string, params: Record<string, string>) =>
route.replace(/\[(?:\.\.\.)?(\w+)\]/g, (_, name: string) => params[name])
}))
// This synchronous stub establishes viewport markup and helper URLs,
// not the actual static renderer or client prop updates.
vi.mock('#lib/pieces/components/opengraph.svelte', async () => ({
default: (await import('./fixtures/Composition.svelte')).default
}))
test('shows only the composition with published assets, without explanatory text', () => {
const html = render(ShareViewport, { props: { piece: publishedPiece } }).body
expect(html).toContain('aria-label="Share"')
expect(html).not.toContain('Rendered share composition')
expect(html).not.toContain('Reconstructed with')
expect(html).not.toContain('not the generated PNG')
expect(html).not.toContain('<h1')
expect(html).not.toContain('<p')
expect(html).toContain('data-renderer="composition"')
expect(html).toContain('src="/cdn/pieces/assets/published/cover.jpg"')
expect(html).not.toContain('/admin/asset')
expect(html).not.toContain('/admin/preview')
expect(html).not.toContain('Open generated share image')
expect(html).toContain(`max-width: ${OpengraphImageWidth}px; height: ${OpengraphImageHeight}px`)
expect(html).toContain(`width: ${OpengraphImageWidth}px; height: ${OpengraphImageHeight}px`)
expect(html.match(/<main /g)).toHaveLength(1)
})
test('recorded generated PNG remains separate from the composition', () => {
const piece = {
...publishedPiece,
assets: [...publishedPiece.assets, asset('opengraph', 'actual-share.png', 'piece', 'image/png')]
}
const html = render(ShareViewport, { props: { piece } }).body
expect(html).toContain('data-renderer="composition"')
expect(html).not.toContain('actual-share.png')
expect(html).not.toContain('Open generated share image')
})
test('missing assets still permit reconstruction without claiming a generated file', () => {
const html = render(ShareViewport, { props: { piece: { ...publishedPiece, assets: [] } } }).body
expect(html).toContain('aria-label="Share"')
expect(html).toContain('data-renderer="composition"')
expect(html).not.toContain('Rendered share composition')
expect(html).not.toContain('Open generated share image')
expect(html).not.toContain('/pieces/assets/')
})
@@ -1,89 +1,36 @@
import { readFileSync } from 'node:fs'
import { expect, test } from 'vitest'
import { render } from 'svelte/server'
import SourceContent from './SourceContent.svelte'
function source(content: string, mime = 'text/plain') {
function renderSource(content: string, mime: string) {
return render(SourceContent, {
props: {
value: {
key: 'private-key',
output: 'custom.output',
key: 'source',
output: 'markdown',
field: 'Content',
value: content,
mime
},
label: 'Custom.output: Asset 1, Content'
label: 'Markdown: Asset 1, Content'
}
}).body.replace(/<!--[^]*?-->/g, '')
}).body
}
function escaped(text: string) {
return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;')
}
test('recorded HTML is shown as source text, not executed or rendered as a document', () => {
const content = '<script>alert("x")</script> & <iframe src="https://bad.test"></iframe>'
function code(html: string) {
return html.match(/<code[^>]*>([\s\S]*?)<\/code>/)?.[1].replace(/<\/?span\b[^>]*>/g, '')
}
const html = renderSource(content, 'text/html')
test.each(['text/html', 'image/svg+xml', 'text/css', 'text/javascript', 'unknown'])(
'%s content stays inert and complete instead of executing as markup or CSS',
(mime) => {
const text =
' <script>alert("x")</script>\r\n<iframe src="https://bad.test"></iframe> & <style>body{display:none}</style>\n '
const html = source(text, mime)
expect(code(html)).toBe(escaped(text))
for (const tag of ['<script', '<iframe', '<style', '<svg', '<img'])
expect(html).not.toContain(tag)
expect(html).not.toContain('private-key')
expect(html).toContain('role="region"')
expect(html).toContain('tabindex="0"')
expect(html).toContain('aria-label="Custom.output: Asset 1, Content source"')
}
)
test('JSON source is pretty-printed without changing numeric lexemes, duplicate keys or string escapes', () => {
const html = source(
'{"id":9007199254740993,"id":1e400,"html":"<script>bad()</script>","literal":"\\u0041"}',
'application/json'
)
expect(code(html)).toBe(
escaped(
'{\n "id": 9007199254740993,\n "id": 1e400,\n "html": "<script>bad()</script>",\n "literal": "\\u0041"\n}'
)
)
expect(html).not.toContain('<script>')
expect(html).toContain('&lt;script>alert("x")&lt;/script> &amp; &lt;iframe')
expect(html).not.toMatch(/<(?:script|iframe)\b/)
expect(html).toContain('role="region"')
expect(html).toContain('tabindex="0"')
expect(html).toContain('aria-label="Markdown: Asset 1, Content source"')
})
test('malformed JSON remains the full original text', () => {
const content = ' { broken <script>alert(1)</script>\r\n'
expect(code(source(content, 'application/json'))).toBe(escaped(content))
})
test('JSON source is passed through the lossless formatter before display', () => {
const html = renderSource('{"count":2}', 'application/json')
test('large source remains available in full rather than truncating the record', () => {
const content = ' snow 雪 <tag> & \r\n'.repeat(12000)
expect(code(source(content))).toBe(escaped(content))
})
test('source view uses the editor token palette with a bounded keyboard-scrollable viewport', () => {
const file = readFileSync(new URL('./SourceContent.svelte', import.meta.url), 'utf8')
expect(file).toContain("import '#lib/components/editor/themes/token-colors.css'")
expect(file).toContain('class="syntax-highlight"')
expect(file).toContain('max-height: 24rem')
expect(file).toContain('overflow: auto')
expect(file).toContain('var(--editor-foreground, var(--color-on-surface))')
expect(file).toContain('var(--editor-background, var(--color-surface))')
expect(file).not.toContain('{@html')
expect(file).not.toContain('innerHTML')
})
test('highlighting is loaded after expansion/mount with a current-source guard and safe plain fallback', () => {
const file = readFileSync(new URL('./SourceContent.svelte', import.meta.url), 'utf8')
expect(file).toContain("import('./source-highlight.js')")
expect(file).toContain('highlighted?.source === source && highlighted.mime === value.mime')
expect(file).toContain('if (active) highlighted =')
expect(file).toContain('active = false')
expect(file).toContain('{token.text}')
expect(file).not.toContain('fetch(')
expect(file).not.toContain('clipboard')
expect(html).toContain('{\n "count": 2\n}')
})
@@ -1,229 +1,48 @@
import { readFileSync } from 'node:fs'
import { beforeEach, expect, test, vi } from 'vitest'
import { expect, test, vi } from 'vitest'
import { render } from 'svelte/server'
import ViewerHeader from './ViewerHeader.svelte'
const { page, mount, resolve } = vi.hoisted(() => {
const mount = { prefix: '' }
return {
mount,
page: {
url: new URL('https://app.test/'),
route: { id: '/(auth)/admin/piece/[...path]/published' }
},
resolve: vi.fn(
(route: string, params: { path: string }) =>
`${mount.prefix}${route.replace('/(auth)', '').replace('[...path]', params.path)}`
)
}
})
const { page } = vi.hoisted(() => ({
page: { route: { id: '/(auth)/admin/piece/[...path]/published' } }
}))
vi.mock('$app/state', () => ({ page }))
vi.mock('$app/paths', () => ({ resolve }))
vi.mock('$app/paths', () => ({
resolve: (route: string, params: { path: string }) =>
route.replace('/(auth)', '').replace('[...path]', () => params.path)
}))
// This resolver double tests delegation and encoded params, not native Kit/browser routing.
const file = 'books/源 #?%.books.md'
const encodedPath = 'books/%E6%BA%90%20%23%3F%25.books.md'
const file = 'notes/icon/雪 #?%.books.md'
const root = '/admin/piece/notes/icon/%E9%9B%AA%20%23%3F%25.books.md'
const publishedRoute = '/(auth)/admin/piece/[...path]/published'
const tabSuffixes = { overview: '', page: '/page', icon: '/icon', share: '/share' }
function links(path = encodedPath, prefix = '') {
const root = `${prefix}/admin/piece/${path}`
return {
source: `${root}/source`,
overview: `${root}/published`,
page: `${root}/published/page`,
icon: `${root}/published/icon`,
share: `${root}/published/share`
}
}
function expectNavigation(path = encodedPath) {
const routes = [
'/(auth)/admin/piece/[...path]/source',
...Object.values(tabSuffixes).map((suffix) => publishedRoute + suffix)
test('all views and the editor link refer to the current archive file', () => {
const destinations = [
'source',
'published',
'published/page',
'published/icon',
'published/share'
]
expect(resolve).toHaveBeenCalledTimes(routes.length)
for (const route of routes) expect(resolve).toHaveBeenCalledWith(route, { path })
}
beforeEach(() => {
page.url = new URL('https://app.test/')
page.route.id = publishedRoute
mount.prefix = ''
vi.clearAllMocks()
})
const html = render(ViewerHeader, { props: { file } }).body
test.each(['overview', 'page', 'icon', 'share'] as const)(
'%s is active by route ID and delegates navigation to resolve',
(tab) => {
const navigation = links()
page.url = new URL(navigation[tab], 'https://app.test')
page.route.id = publishedRoute + tabSuffixes[tab]
const html = render(ViewerHeader, { props: { file } }).body
expect(html).toContain(`href="${navigation.source}"`)
expect(html).toContain('aria-label="Back to editor"')
expect(html).toContain('title="Back to editor"')
expect(html).toContain('aria-label="Published views"')
expect(html).toContain(`href="${navigation[tab]}" aria-current="page"`)
expect(html.match(/aria-current="page"/g)).toHaveLength(1)
for (const label of ['Overview', 'Page', 'Icon', 'Share']) expect(html).toContain(label)
expect(html).not.toContain('Logs')
expect(html).not.toContain('<main')
expect(html).not.toContain('target="_blank"')
expectNavigation()
for (const destination of destinations) {
expect(html).toContain(`href="${root}/${destination}"`)
}
)
expect(html).toContain('aria-label="Back to editor"')
})
test.each([
[file, encodedPath],
['folder with spaces/a book.books.md', 'folder%20with%20spaces/a%20book.books.md'],
['nested/literal%2F.books.md', 'nested/literal%252F.books.md'],
['books/📚.books.md', 'books/%F0%9F%93%9A.books.md'],
['published/page/icon/share/source/file.md', 'published/page/icon/share/source/file.md']
])('raw file %s becomes an encoded-once path param for all five routes', (raw, encoded) => {
// A different URL identity must not be used to derive the file or its mount.
page.url = new URL('https://app.test/garden/admin/piece/other.md/published/share?x=1#old')
page.route.id = publishedRoute + '/share'
const html = render(ViewerHeader, { props: { file: raw } }).body
for (const href of Object.values(links(encoded))) expect(html).toContain(`href="${href}"`)
expectNavigation(encoded)
expect(html).not.toContain('/garden/')
expect(html).not.toContain('other.md')
})
{ view: 'overview', suffix: '' },
{ view: 'page', suffix: '/page' },
{ view: 'icon', suffix: '/icon' },
{ view: 'share', suffix: '/share' }
])('marks only the current $view tab as active', ({ suffix }) => {
page.route.id = publishedRoute + suffix
test.each(['overview', 'page', 'icon', 'share'] as const)(
'controlled resolver prefix and trailing slash leave %s active by route ID',
(tab) => {
mount.prefix = '/garden'
const navigation = links(encodedPath, mount.prefix)
page.url = new URL(`${navigation[tab]}/?x=1#fragment`, 'https://app.test')
page.route.id = publishedRoute + tabSuffixes[tab]
const html = render(ViewerHeader, { props: { file } }).body
expect(html).toContain(`href="${navigation[tab]}" aria-current="page"`)
expect(html.match(/aria-current="page"/g)).toHaveLength(1)
expect(html).toContain(`href="${navigation.source}"`)
// Prefix handling belongs to resolve, not the raw file or its encoded param.
expectNavigation()
}
)
test('active tab follows route ID rather than a coincidentally matching URL pathname', () => {
page.url = new URL(links().icon, 'https://app.test')
page.route.id = publishedRoute + '/page'
const html = render(ViewerHeader, { props: { file } }).body
expect(html).toContain(`href="${links().page}" aria-current="page"`)
expect(html).not.toContain(`href="${links().icon}" aria-current="page"`)
expect(html.match(/aria-current="page"/g)).toHaveLength(1)
})
test('replacement file renders do not retain a previous source identity', () => {
render(ViewerHeader, { props: { file: 'first.md' } })
vi.clearAllMocks()
page.url = new URL(links('second.md').share, 'https://app.test')
page.route.id = publishedRoute + '/share'
const html = render(ViewerHeader, { props: { file: 'second.md' } }).body
expect(html).toContain('/second.md/published/share')
expect(html).toContain('aria-current="page"')
expect(html).not.toContain('first')
expectNavigation('second.md')
})
test('back link uses the same decorative arrow icon as Preview, not a visible text label', () => {
const html = render(ViewerHeader, { props: { file } }).body
const anchors = html.match(/<a\b(?:[^>"']|"[^"]*"|'[^']*')*>[\s\S]*?<\/a>/g) ?? []
const back = anchors.find((anchor) => anchor.includes('aria-label="Back to editor"'))!
expect(back).toContain(`href="${links().source}"`)
expect(back).toContain('title="Back to editor"')
expect(back).toMatch(/<svg\b[^>]*aria-hidden="true"/)
expect(back).toMatch(/<svg\b[^>]*focusable="false"/)
expect(
back
.replace(/<!--[^]*?-->/g, '')
.replace(/<[^>]*>/g, '')
.trim()
).toBe('')
expect(back).not.toContain('target=')
expectNavigation()
})
test('Published header consumes the narrowly scoped shared bar stylesheet', () => {
const published = readFileSync(new URL('./ViewerHeader.svelte', import.meta.url), 'utf8')
expect(published).toContain("ViewerBar.css'")
expect(published).toContain("'virtual:icons/ph/arrow-left'")
for (const name of [
'viewer-bar',
'viewer-bar-back',
'viewer-bar-links',
'viewer-bar-link',
'viewer-bar-label'
])
expect(published).toContain(`class="${name}`)
const css = readFileSync(new URL('./ViewerBar.css', import.meta.url), 'utf8')
for (const declaration of [
'position: sticky',
'top: 0',
'z-index: 100',
'display: grid',
"grid-template-areas: 'back links label'",
'align-items: center',
'gap: var(--space-3)',
'padding: var(--space-2) var(--space-4)',
'font-size: var(--font-size-small)',
'text-transform: uppercase',
'font-size: 0.9em',
'opacity: 0.85',
'letter-spacing: 0.05em'
])
expect(css).toContain(declaration)
expect(published).not.toContain('<style>')
})
test('shared bar stacks mode/back above full-width wrapping tabs on mobile, with comfortable targets', () => {
const css = readFileSync(new URL('./ViewerBar.css', import.meta.url), 'utf8')
const mobile = css.slice(css.indexOf('@media (max-width: 600px)'))
for (const declaration of [
"grid-template-areas: 'back label' 'links links'",
'justify-self: stretch',
'flex-wrap: wrap',
'flex: 1 0 auto',
'padding-inline: var(--space-1)',
'overflow-x: visible'
])
expect(mobile).toContain(declaration)
expect(css).toContain('min-width: 44px')
expect(css).toContain('min-height: 44px')
expect(mobile).not.toContain('display: none')
expect(mobile).not.toContain('position: absolute')
const html = render(ViewerHeader, { props: { file } }).body
expect(html).toContain('<nav class="viewer-bar-links" aria-label="Published views"')
expect(html).toContain('class="viewer-bar-label">Published</span>')
for (const label of ['Overview', 'Page', 'Icon', 'Share']) expect(html).toContain(`>${label}</a>`)
})
test('shared active/hover/focus states are token-based, visibly distinguish selection and retain Preview disabled styling', () => {
const css = readFileSync(new URL('./ViewerBar.css', import.meta.url), 'utf8')
for (const declaration of [
'color-mix(in srgb, var(--color-on-tertiary-container) 10%, transparent)',
'color-mix(in srgb, var(--color-on-tertiary-container) 20%, transparent)',
"[aria-current='page']",
'.viewer-bar-link.active',
'font-weight: var(--font-weight-bold)',
'text-decoration: underline',
'a:focus-visible',
'outline: 2px solid currentColor',
'outline-offset: -2px',
'.viewer-bar-link.disabled',
'opacity: 0.35',
'pointer-events: none'
])
expect(css).toContain(declaration)
expect(css).not.toContain('var(--color-surface-container)')
expect(css).not.toContain('rgba(')
})
test('mocked SSR after an await still delegates encoded route params', async () => {
await Promise.resolve()
const html = render(ViewerHeader, { props: { file } }).body
expect(html).toContain('Back to editor')
expectNavigation()
const activeLinks = html.match(/<a\b[^>]*aria-current="page"[^>]*>/g) ?? []
expect(activeLinks).toHaveLength(1)
expect(activeLinks[0]).toContain(`href="${root}/published${suffix}"`)
})
+160 -552
View File
@@ -1,575 +1,183 @@
import { expect, test, vi } from 'vitest'
import { groupAssetOutputs, groupOutputRows } from './assets'
import { assetPathToUrl } from '#lib/pieces/assets.js'
import { asset } from './fixtures/published'
import type { PublicWebPieceAsset } from '@luzzle/web.pieces'
import { groupAssetOutputs, groupOutputRows, type OutputFile, type OutputValue } from './assets'
function groupAssets(assets: Parameters<typeof groupAssetOutputs>[0], base: string) {
return groupAssetOutputs(assets, (path) => assetPathToUrl(path, base))
}
const fileUrl = (path: string) => `/files/${path}`
function transform(records: Parameters<typeof groupAssetOutputs>[0], name: string, base = '') {
const section = groupAssets(records, base).transforms.find((section) => section.name === name)
expect(section).toBeDefined()
return section!
}
test('groups projected rows in asset order without replacing row identities or reordering input', () => {
const section = transform(
[
asset('image.original', 'second.jpg', 'second'),
asset('image.original', 'first.jpg', 'first'),
{ ...asset('image.palette', null, 'first', 'text/plain'), content: '#123456' },
{ ...asset('image.palette', null, 'second', 'text/plain'), content: '#abcdef' }
],
'image'
)
const rows = [...section.files, ...section.values]
const before = [...rows]
const ordinals = new Map([
['first', 1],
['second', 2]
])
const groups = groupOutputRows(rows, ordinals)
expect(groups.map((group) => group.key)).toEqual(['first', 'second'])
expect(groups[0].items[0]).toBe(section.files[1])
expect(groups[0].items[1]).toBe(section.values[0])
expect(groups[1].items[0]).toBe(section.files[0])
expect(groups[1].items[1]).toBe(section.values[1])
expect(rows).toEqual(before)
expect(groupOutputRows([], ordinals)).toEqual([])
})
test('groups every producer uniformly while retaining keys, originals, variants and attachments as recorded', () => {
const original = asset('image.original', 'first/cover.jpg', 'first')
const records = [
asset('attachment', 'first/cover.jpg', 'first'),
original,
original,
asset('image.s.webp', 'first/s.webp', 'first', 'image/webp'),
asset('image.s.webp', 'first/s.webp', 'first', 'image/webp'),
asset('image.original', 'second/cover.jpg', 'second')
]
const before = structuredClone(records)
const data = groupAssets(records, 'https://cdn.example/prefix/')
expect(records).toEqual(before)
expect(data.transforms.map((section) => section.name)).toEqual(['attachment', 'image'])
expect(transform(records, 'image').files).toMatchObject([
{ key: 'first', path: 'first/cover.jpg', label: 'image.original', mime: 'image/jpeg' },
{ key: 'first', path: 'first/s.webp', label: 'image.s.webp', mime: 'image/webp' },
{ key: 'second', path: 'second/cover.jpg', label: 'image.original', mime: 'image/jpeg' }
])
expect(transform(records, 'image', 'https://cdn.example/prefix/').files[2].url).toBe(
'https://cdn.example/prefix/pieces/assets/second/cover.jpg'
)
expect(transform(records, 'image').files).toHaveLength(3)
expect(transform(records, 'attachment').files).toMatchObject([
{ key: 'first', path: 'first/cover.jpg' }
])
expect(data).not.toHaveProperty('images')
})
test('renamed jobs retain actual file and value outputs in generic producer sections', () => {
const records = [
asset('pictures.original', 'published/cover.jpg', 'cover'),
asset('pictures.s.webp', 'published/s.webp', 'cover', 'image/webp'),
{ ...asset('rendered-note', null, 'piece', 'text/html'), content: '<h1>Serving HTML</h1>' },
{
...asset('colors.v2', null, 'piece', 'application/json'),
content: '{"background":"#123456"}'
},
{ ...asset('new-job', 'published/body.html', 'piece', 'text/html'), content: '<b>both</b>' }
]
const before = structuredClone(records)
const data = groupAssets(records, '/cdn')
expect(records).toEqual(before)
expect(data).not.toHaveProperty('images')
expect(data.transforms.map((section) => section.name)).toEqual([
'pictures',
'rendered-note',
'colors',
'new-job'
])
expect(data.transforms[0].files[0]).toMatchObject({
path: 'published/cover.jpg',
label: 'pictures.original',
url: '/cdn/pieces/assets/published/cover.jpg',
mime: 'image/jpeg'
})
expect(data.transforms[0].files[1]).toMatchObject({
label: 'pictures.s.webp',
mime: 'image/webp',
url: '/cdn/pieces/assets/published/s.webp'
})
expect(data.transforms[1].values[0].value).toBe('<h1>Serving HTML</h1>')
expect(data.transforms[2].values).toEqual([
{
key: 'piece',
output: 'colors.v2',
field: 'Content',
value: records[3].content,
mime: 'application/json'
}
])
expect(data.transforms[3].files[0].url).toBe('/cdn/pieces/assets/published/body.html')
expect(data.transforms[3].values[0]).toMatchObject({ output: 'new-job', value: '<b>both</b>' })
})
test('deduplicates exact projected records, retaining different keys, transforms, MIME and inline values', () => {
const records = [
asset('image.s.webp', 'shared.webp', 'first'),
asset('image.s.webp', 'shared.webp', 'first'),
asset('image.m.webp', 'shared.webp', 'first'),
asset('image.s.webp', 'another.webp', 'first'),
asset('image.s.webp', 'shared.webp', 'second'),
asset('custom', 'export.pdf'),
asset('custom', 'export.pdf'),
asset('custom', 'export.pdf', 'cover', 'application/pdf'),
asset('custom.other', 'export.pdf'),
{ ...asset('text', null, 'piece', 'text/plain'), content: 'one' },
{ ...asset('text', null, 'piece', 'text/plain'), content: 'one' },
{ ...asset('text', null, 'piece', 'text/plain'), content: 'two' }
]
const data = groupAssets(records, '')
expect(
transform(records, 'image').files.map((file) => [file.key, file.label, file.path])
).toEqual([
['first', 'image.s.webp', 'shared.webp'],
['first', 'image.m.webp', 'shared.webp'],
['first', 'image.s.webp', 'another.webp'],
['second', 'image.s.webp', 'shared.webp']
])
expect(
data.transforms.map((section) => [section.name, section.files.length, section.values.length])
).toEqual([
['image', 4, 0],
['custom', 3, 0],
['text', 0, 2]
])
})
test.each(['image.original', 'attachment'])(
'%s retains every distinct missing/usable path without replacing a record',
(name) => {
const records = [
asset(name, null),
asset(name, 'valid.jpg'),
asset(name, 'also-valid.jpg'),
asset(name, 'valid.jpg')
]
const data = groupAssets(records, '')
const files = data.transforms[0].files
expect(files.map((file) => file.path)).toEqual([null, 'valid.jpg', 'also-valid.jpg'])
expect(files[0].url).toBeUndefined()
test('groups recorded outputs by producer namespace without hiding unfamiliar producers', () => {
const original: PublicWebPieceAsset = {
asset_key: 'cover',
transformation: 'image.original',
asset_path: 'cover.jpg',
mime_type: 'image/jpeg'
}
)
test('absent originals remain absent while all size/format variants remain associated with their key', () => {
const variants = ['s', 'm', 'l', 'xl'].flatMap((size) =>
['avif', 'webp'].map((format) => asset(`image.${size}.${format}`, `cover/${size}.${format}`))
)
const data = groupAssets(variants, '/garden')
expect(data.transforms.map((section) => section.name)).toEqual(['image'])
expect(data.transforms[0].files).toHaveLength(8)
expect(data.transforms[0].files.map((file) => file.label)).toEqual(
variants.map((record) => record.transformation)
)
expect(data.transforms[0].files.some((file) => file.label === 'image.original')).toBe(false)
expect(data.transforms[0].files[7].url).toBe('/garden/pieces/assets/cover/xl.webp')
expect(data.transforms[0].files.every((file) => file.key === 'cover')).toBe(true)
})
test('groups all actual downstream transforms including content, unknown outputs and file-plus-inline content', () => {
const records = [
asset('opengraph', 'piece/share.png', 'piece', 'image/png'),
{
...asset('palette', null, 'piece', 'application/json'),
content: '{"background":"#123456","accent":"#abcdef"}'
},
{ ...asset('markdown', null, 'piece', 'text/html'), content: '<h1>inline</h1>' },
{ ...asset('markdown.description', null, 'piece', 'text/html'), content: '' },
{ ...asset('highlight', null), is_embedded: 1 as const },
asset('document.custom', 'piece/export.pdf', 'piece', 'application/pdf'),
asset('image.custom', 'piece/custom.jpg', 'piece'),
{ ...asset('markdown', 'piece/body.html', 'piece', 'text/html'), content: 'inline' }
]
const data = groupAssets(records, '/cdn')
expect(data).not.toHaveProperty('images')
expect(data.transforms.map((section) => section.name)).toEqual([
'opengraph',
'palette',
'markdown',
'highlight',
'document',
'image'
])
expect(transform(records, 'palette').values).toEqual([
{
key: 'piece',
output: 'palette',
field: 'Content',
value: records[1].content,
mime: 'application/json'
}
])
expect(transform(records, 'markdown').values.map((row) => row.value)).toEqual([
'<h1>inline</h1>',
'',
'inline'
])
expect(transform(records, 'markdown', '/cdn').files[0].url).toBe(
'/cdn/pieces/assets/piece/body.html'
)
expect(transform(records, 'highlight').values[0].value).toBeNull()
expect(transform(records, 'markdown').values[1]).toMatchObject({
output: 'markdown.description',
value: ''
})
})
test('retains every actual Opengraph output, not just the first usable PNG', () => {
const section = transform(
[
asset('opengraph', '../bad.png', 'piece', 'image/png'),
asset('opengraph', 'share.jpg', 'piece', 'image/jpeg'),
asset('opengraph', 'valid.png', 'piece', 'image/png'),
asset('opengraph', 'valid.png', 'piece', 'image/png'),
asset('opengraph', 'second.png', 'piece', 'image/png')
],
'opengraph'
)
expect(section.files.map((file) => file.path)).toEqual([
'../bad.png',
'share.jpg',
'valid.png',
'second.png'
])
expect(section.files[0].url).toBeUndefined()
expect(section.files[1].mime).toBe('image/jpeg')
})
test.each(['image/jpeg', 'application/octet-stream', 'text/html'])(
'Opengraph MIME %s is inspectable as recorded, never reclassified by extension',
(mime) => {
const file = transform([asset('opengraph', 'share.png', 'piece', mime)], 'opengraph').files[0]
expect(file).toMatchObject({ mime, path: 'share.png', url: '/pieces/assets/share.png' })
const thumbnail = {
...original,
transformation: 'image.s.webp',
asset_path: 'small.webp',
mime_type: 'image/webp'
}
)
test.each(['image.original', 'attachment', 'image.s.webp', 'document.custom'])(
'%s with neither content nor path remains a missing file record',
(name) => {
const data = groupAssets([asset(name, null)], '')
const files = data.transforms.flatMap((section) => section.files)
expect(files).toHaveLength(1)
expect(files[0]).toMatchObject({ path: null, url: undefined })
const exported: PublicWebPieceAsset = {
asset_key: 'document',
transformation: 'document.export',
asset_path: 'document.pdf',
mime_type: 'application/pdf'
}
)
test('family grouping preserves full output identities, fields, MIME and every file-plus-inline capability', () => {
const records = [
{
...asset('custom.a', 'same/file.json', 'one', 'application/json'),
content: '{"first":"value","second":42}'
},
{ ...asset('custom.b', 'same/file.json', 'one', 'text/plain'), content: 'raw' },
{ ...asset('custom', null, 'two', 'text/plain'), is_embedded: 1 as const },
{ ...asset('other.a', 'same/file.json', 'one', 'text/plain'), content: 'raw' }
]
const before = structuredClone(records)
const grouped = groupAssets([...records, ...records], '/cdn')
expect(records).toEqual(before)
expect(grouped.transforms.map((group) => group.name)).toEqual(['custom', 'other'])
expect(grouped.transforms[0].files.map((file) => [file.label, file.mime, file.path])).toEqual([
['custom.a', 'application/json', 'same/file.json'],
['custom.b', 'text/plain', 'same/file.json']
const grouped = groupAssetOutputs([original, exported, thumbnail], fileUrl)
expect(grouped.transforms.map((family) => family.name)).toEqual(['image', 'document'])
expect(grouped.transforms[0].files).toMatchObject([
{ key: 'cover', label: 'image.original', path: 'cover.jpg' },
{ key: 'cover', label: 'image.s.webp', path: 'small.webp' }
])
expect(
grouped.transforms[0].values.map((value) => [value.output, value.field, value.value, value.key])
).toEqual([
['custom.a', 'Content', records[0].content, 'one'],
['custom.b', 'Content', 'raw', 'one'],
['custom', 'Content', null, 'two']
expect(grouped.transforms[1].files).toMatchObject([
{ key: 'document', label: 'document.export', path: 'document.pdf' }
])
expect(grouped.transforms[1].files[0].label).toBe('other.a')
expect(grouped.transforms[1].values[0].output).toBe('other.a')
})
test('family grouping uses exact prefixes, first-seen order and Map-safe names rather than a known-producer registry', () => {
const names = [
'custom.more',
'imagewriter.s.webp',
'custom',
'image.original',
'Image.original',
'__proto__.one',
'__proto__.two',
'.leading'
]
const result = groupAssets(
names.map((name) => asset(name, 'file.bin')),
''
)
expect(result.transforms.map((group) => group.name)).toEqual([
'custom',
'imagewriter',
'image',
'Image',
'__proto__',
'.leading'
])
expect(result.transforms[0].files.map((file) => file.label)).toEqual(['custom.more', 'custom'])
expect(result.transforms[4].files.map((file) => file.label)).toEqual([
'__proto__.one',
'__proto__.two'
])
expect(result.transforms.flatMap((group) => group.files)).toHaveLength(names.length)
})
test('empty input invents no downstream transform sections', () => {
expect(groupAssets([], '')).toEqual({ transforms: [] })
})
// Projection is lossless, not a JSON parser or formatter. Exercise distinct raw-data
// risks once, with MIME variations, rather than repeating syntax classes per producer.
test.each([
['application/json', '{}'],
['text/json', '[]'],
['application/example+json', 'null'],
[' APPLICATION/JSON ; charset=utf-8', '{broken'],
['text/plain', '{"html":"<script>alert(1)</script>"}'],
['text/html', ' <h1>raw</h1>\r\n'],
['image/svg+xml', '{"color":"#abc"}'],
['application/json', ''],
[
'application/json',
' \r\n { "__proto__":{"polluted":true}, "constructor":"own", "same":1, "same":2, "escaped":"\\u96ea\\n" } \r\n'
],
['application/json', '"\\u0041\\t\\n"'],
['application/json', '{"id":9007199254740993,"nested":{"value":1e400}}'],
['application/json', '['.repeat(12000) + '0' + ']'.repeat(12000)]
])(
'%s retains exactly one whole Content value with original bytes and metadata',
(mime, content) => {
const record = { ...asset('custom.raw', null, 'piece', mime), content }
const before = structuredClone(record)
const section = transform([record, record], 'custom')
expect(record).toEqual(before)
expect(section.files).toEqual([])
expect(section.values).toEqual([
{ key: 'piece', output: 'custom.raw', field: 'Content', value: content, mime }
])
test('removes exact replayed records without merging distinct assets, outputs or source values', () => {
const first: PublicWebPieceAsset = {
asset_key: 'first-snippet',
transformation: 'highlight',
asset_path: 'source.js',
mime_type: 'text/plain',
content: 'first source'
}
)
const secondAsset = { ...first, asset_key: 'second-snippet' }
const secondOutput = { ...first, transformation: 'highlight.metadata' }
const secondValue = { ...first, content: 'second source' }
const secondMime = { ...first, mime_type: 'text/html' }
test('inline images are inspectable values and path-plus-content images retain both views', () => {
const data = groupAssets(
[
{ ...asset('image.original', null, 'embedded'), is_embedded: 1, content: 'BASE64' },
{ ...asset('image.s.webp', 's.webp', 'cover'), content: 'DATA' }
],
''
)
expect(data).not.toHaveProperty('images')
expect(data.transforms.map((section) => [section.name, section.files.length])).toEqual([
['image', 1]
])
expect(data.transforms[0].values.map((value) => [value.output, value.value])).toEqual([
['image.original', 'BASE64'],
['image.s.webp', 'DATA']
])
})
test.each(['https://cdn.test/prefix/', 'https://app.test/site/', '/prefix/', ''])(
'base %s preserves prefixes and encodes raw paths once',
(base) => {
expect(
transform([asset('attachment', 'files/源 #?%.pdf')], 'attachment', base).files[0].url
).toBe(`${base.replace(/\/+$/, '')}/pieces/assets/files/%E6%BA%90%20%23%3F%25.pdf`)
}
)
test('percent-looking filenames are literal paths, not decoded traversal or pre-encoded URLs', () => {
expect(
transform([asset('attachment', '%2e%2e/file%20name.pdf')], 'attachment', '/cdn').files[0].url
).toBe('/cdn/pieces/assets/%252e%252e/file%2520name.pdf')
})
test('absolute public base preserves prefix but excludes query and fragment', () => {
expect(
transform([asset('attachment', 'file.pdf')], 'attachment', 'https://app.test/site/?q=1#hash')
.files[0].url
).toBe('https://app.test/site/pieces/assets/file.pdf')
})
test.each([
null,
undefined,
'',
' ',
'../private.png',
'piece/../private.png',
'/absolute.png',
'//cdn/file.png',
'piece//share.png',
'piece/./share.png',
'https://evil.test/share.png',
'javascript:alert(1)',
'x\\share.png'
])(
'invalid path %j stays inspectable for every producer without invoking the URL callback',
(path) => {
for (const [output, family] of [
['opengraph', 'opengraph'],
['image.original', 'image'],
['attachment', 'attachment'],
['custom.file', 'custom']
]) {
const record = { ...asset(output, null, 'private-key', 'text/plain'), asset_path: path }
const fileUrl = vi.fn(() => '/must-not-be-used')
const data = groupAssetOutputs([record, record], fileUrl)
expect(fileUrl).not.toHaveBeenCalled()
expect(data).toEqual({
transforms: [
{
name: family,
files: [
{
key: 'private-key',
path: path || null,
label: output,
mime: 'text/plain',
url: undefined
}
],
values: []
}
]
})
}
}
)
test('the URL callback receives each deduplicated raw record path once, never an encoded path', () => {
const path = 'files/源 #?%/%2e%2e/file%20name.json'
const record = {
...asset('unknown.full.output', path, 'private-key', 'application/json'),
content: '{"__proto__":"own","number":42}'
}
const fileUrl = vi.fn((rawPath: string) =>
rawPath === path ? '/admin/piece/book/preview/job/assets/output.json' : undefined
)
const data = groupAssetOutputs([record, record], fileUrl)
expect(fileUrl).toHaveBeenCalledExactlyOnceWith(path)
expect(data).toEqual({
transforms: [
{
name: 'unknown',
files: [
{
key: 'private-key',
path,
label: 'unknown.full.output',
mime: 'application/json',
url: '/admin/piece/book/preview/job/assets/output.json'
}
],
values: [
{
key: 'private-key',
output: 'unknown.full.output',
field: 'Content',
value: record.content,
mime: 'application/json'
}
]
}
]
})
})
test('inline-only and embedded-only outputs never ask for file URLs or invent file rows', () => {
const records = [
{ ...asset('custom.note', null, 'first', 'text/plain'), content: 'inline' },
{ ...asset('custom.empty', '', 'first', 'text/plain'), content: '' },
{ ...asset('custom.embedded', null, 'second', 'text/plain'), is_embedded: 1 as const },
{
...asset('custom.json', null, 'second', 'application/json'),
content: '{"own":"value"}',
is_embedded: 1 as const
}
]
const fileUrl = vi.fn(() => '/bogus-file')
const data = groupAssetOutputs(records, fileUrl)
expect(fileUrl).not.toHaveBeenCalled()
expect(data.transforms).toHaveLength(1)
expect(data.transforms[0].files).toEqual([])
expect(
data.transforms[0].values.map(({ output, field, value }) => [output, field, value])
).toEqual([
['custom.note', 'Content', 'inline'],
['custom.empty', 'Content', ''],
['custom.embedded', 'Content', null],
['custom.json', 'Content', records[3].content]
])
})
test('a valid path with no callback URL retains both its file and inline inspection data', () => {
const path = 'files/recorded.txt'
const fileUrl = vi.fn((_path: string) => undefined)
const data = groupAssetOutputs(
[{ ...asset('custom.full', path, 'private-key', 'text/plain'), content: 'inline' }],
const grouped = groupAssetOutputs(
[first, first, secondAsset, secondOutput, secondValue, secondMime],
fileUrl
)
expect(fileUrl).toHaveBeenCalledExactlyOnceWith(path)
expect(data.transforms[0].files).toEqual([
{ key: 'private-key', path, label: 'custom.full', mime: 'text/plain', url: undefined }
])
expect(data.transforms[0].values).toEqual([
expect(grouped.transforms[0].files).toHaveLength(5)
expect(grouped.transforms[0].values).toMatchObject([
{ key: 'first-snippet', output: 'highlight', value: 'first source', mime: 'text/plain' },
{ key: 'second-snippet', output: 'highlight', value: 'first source', mime: 'text/plain' },
{
key: 'private-key',
output: 'custom.full',
field: 'Content',
value: 'inline',
key: 'first-snippet',
output: 'highlight.metadata',
value: 'first source',
mime: 'text/plain'
},
{ key: 'first-snippet', output: 'highlight', value: 'second source', mime: 'text/plain' },
{ key: 'first-snippet', output: 'highlight', value: 'first source', mime: 'text/html' }
])
})
test('retains both the recorded file and whole inline source without parsing the source', () => {
const output: PublicWebPieceAsset = {
asset_key: 'metadata',
transformation: 'markdown.metadata',
asset_path: 'metadata.json',
mime_type: 'application/json',
content: '{broken JSON <script>literal source</script>'
}
const grouped = groupAssetOutputs([output], fileUrl)
expect(grouped.transforms[0].files).toMatchObject([
{ path: 'metadata.json', url: '/files/metadata.json' }
])
expect(grouped.transforms[0].values).toEqual([
{
key: 'metadata',
output: 'markdown.metadata',
field: 'Content',
mime: 'application/json',
value: output.content
}
])
})
test('dedup identity preserves delimiter-looking records and missing, empty and literal null content', () => {
const records = [
{ ...asset('custom.a:b', null, 'key', 'application/json'), is_embedded: 1 as const },
{
...asset('custom.a:b', null, 'key', 'application/json'),
is_embedded: 1 as const,
content: ''
},
{
...asset('custom.a:b', null, 'key', 'application/json'),
is_embedded: 1 as const,
content: 'null'
},
{ ...asset('custom.a:b', null, 'key', 'application/json'), content: 'null' },
{ ...asset('custom.a', null, 'key', 'b:application/json'), content: 'null' },
{ ...asset('custom.a:b', null, 'key:', 'application/json'), content: 'null' },
{ ...asset('custom.a:b', null, 'key', 'application/json'), content: 'null ' }
]
const fileUrl = vi.fn(() => '/not-a-file')
const before = structuredClone(records)
const data = groupAssetOutputs([...records, ...records], fileUrl)
expect(records).toEqual(before)
expect(fileUrl).not.toHaveBeenCalled()
expect(data.transforms).toEqual([
{
name: 'custom',
files: [],
values: records.map((record) => ({
key: record.asset_key,
output: record.transformation,
field: 'Content',
value: record.content ?? null,
mime: record.mime_type
}))
}
test('distinguishes missing files, missing embedded values, empty source and literal JSON null', () => {
const missingFile: PublicWebPieceAsset = {
asset_key: 'missing-file',
transformation: 'palette',
asset_path: null,
mime_type: 'application/json'
}
const missingValue = { ...missingFile, asset_key: 'missing-value', is_embedded: 1 as const }
const empty = { ...missingValue, asset_key: 'empty', content: '' }
const jsonNull = { ...missingValue, asset_key: 'json-null', content: 'null' }
const whitespace = { ...missingValue, asset_key: 'whitespace', content: ' \r\n ' }
const grouped = groupAssetOutputs(
[missingFile, missingValue, empty, jsonNull, whitespace],
fileUrl
)
expect(grouped.transforms[0].files).toMatchObject([
{ key: 'missing-file', path: null, url: undefined }
])
expect(grouped.transforms[0].values.map(({ key, value }) => ({ key, value }))).toEqual([
{ key: 'missing-value', value: null },
{ key: 'empty', value: '' },
{ key: 'json-null', value: 'null' },
{ key: 'whitespace', value: ' \r\n ' }
])
})
test('only usable raw paths reach the URL callback, without discarding unavailable records', () => {
const available: PublicWebPieceAsset = {
asset_key: 'available',
transformation: 'attachment',
asset_path: 'files/雪 #?%.pdf',
mime_type: 'application/pdf'
}
const unavailable = { ...available, asset_key: 'unavailable', asset_path: 'unavailable.pdf' }
const unsafe = { ...available, asset_key: 'unsafe', asset_path: '../private.pdf' }
const previewUrl = '/admin/preview/current-job/asset/recorded.pdf'
const resolveFile = vi.fn((path: string) =>
path === available.asset_path ? previewUrl : undefined
)
const grouped = groupAssetOutputs([available, unavailable, unsafe], resolveFile)
expect(resolveFile).toHaveBeenCalledWith(available.asset_path)
expect(resolveFile).toHaveBeenCalledWith(unavailable.asset_path)
expect(resolveFile).not.toHaveBeenCalledWith(unsafe.asset_path)
expect(grouped.transforms[0].files).toMatchObject([
{ key: 'available', path: available.asset_path, url: previewUrl },
{ key: 'unavailable', path: 'unavailable.pdf', url: undefined },
{ key: 'unsafe', path: '../private.pdf', url: undefined }
])
})
test('file and value rows follow the original asset order rather than file-first projection order', () => {
const firstFile: OutputFile = {
key: 'first-cover',
label: 'image.original',
path: 'first.jpg',
mime: 'image/jpeg',
url: '/files/first.jpg'
}
const secondFile = {
...firstFile,
key: 'second-cover',
path: 'second.jpg',
url: '/files/second.jpg'
}
const firstValue: OutputValue = {
key: 'first-cover',
output: 'image.note',
field: 'Content',
mime: 'text/plain',
value: 'First cover note'
}
const assetOrder = new Map([
['first-cover', 1],
['second-cover', 2]
])
const groups = groupOutputRows([secondFile, firstFile, firstValue], assetOrder)
expect(groups).toEqual([
{ key: 'first-cover', items: [firstFile, firstValue] },
{ key: 'second-cover', items: [secondFile] }
])
})
@@ -2,62 +2,38 @@ import { expect, test } from 'vitest'
import type { OutputValue } from './assets'
import { toggleDisclosure } from './disclosure'
function value(): OutputValue {
function sourceRow(key: string): OutputValue {
return {
key: 'private',
output: 'custom.note',
key,
output: 'highlight',
field: 'Content',
mime: 'text/plain',
value: 'raw'
value: 'identical source text'
}
}
test('opens and closes one row without mutating its input set', () => {
const row = value()
test('opening and closing a row returns new state without changing the previous state', () => {
const row = sourceRow('first-snippet')
const initial: ReadonlySet<OutputValue> = new Set()
const opened = toggleDisclosure(initial, row)
const closed = toggleDisclosure(opened, row)
expect(opened).not.toBe(initial)
expect([...opened]).toEqual([row])
expect(initial.size).toBe(0)
const closed = toggleDisclosure(opened, row)
expect(closed).not.toBe(opened)
expect(closed.size).toBe(0)
expect(opened.has(row)).toBe(true)
})
test('multiple rows stay independently open and close in any order', () => {
const first = value()
const second = value()
const third = value()
const both = toggleDisclosure(toggleDisclosure(new Set(), first), second)
expect(both.size).toBe(2)
const justSecond = toggleDisclosure(both, first)
expect(justSecond.has(second)).toBe(true)
expect(justSecond.has(first)).toBe(false)
expect(both.has(first)).toBe(true)
const secondAndThird = toggleDisclosure(justSecond, third)
expect([...secondAndThird]).toEqual([second, third])
expect([...toggleDisclosure(secondAndThird, second)]).toEqual([third])
})
test('closing one asset does not close another asset with identical source text', () => {
const first = sourceRow('first-snippet')
const second = sourceRow('second-snippet')
const firstOpened = toggleDisclosure(new Set(), first)
test('identical fields and raw keys do not merge different projected row identities', () => {
const first = value()
const sameFields = { ...first }
const opened = toggleDisclosure(new Set(), first)
expect(opened.has(sameFields)).toBe(false)
const both = toggleDisclosure(opened, sameFields)
expect(both.size).toBe(2)
expect([...toggleDisclosure(both, first)]).toEqual([sameFields])
})
const bothOpened = toggleDisclosure(firstOpened, second)
const firstClosed = toggleDisclosure(bothOpened, first)
test('a fresh model starts from an empty set, including replaced rows with identical fields', () => {
// Parent reset wiring is a source contract in AssetOutputs.test.ts, not a mounted effect test.
const previous = value()
const previousState = toggleDisclosure(new Set(), previous)
const replacement = { ...previous }
const reset: ReadonlySet<OutputValue> = new Set()
expect(reset.has(previous)).toBe(false)
expect(reset.has(replacement)).toBe(false)
expect([...toggleDisclosure(reset, replacement)]).toEqual([replacement])
expect([...previousState]).toEqual([previous])
expect([...bothOpened]).toEqual([first, second])
expect([...firstClosed]).toEqual([second])
})
@@ -1,71 +0,0 @@
import { expect, test } from 'vitest'
import { fileType, transformCaption, transformFamily } from './presentation'
test.each([
['image', 'image'],
['image.original', 'image'],
['image.s.webp', 'image'],
['markdown.description.nested', 'markdown'],
['pictures.original', 'pictures'],
['colors.v2', 'colors'],
['custom.output', 'custom'],
['imagewriter.s.webp', 'imagewriter'],
['Image.original', 'Image'],
['__proto__.output', '__proto__'],
['constructor.output', 'constructor'],
['image..webp', 'image'],
['image.', 'image'],
['.leading', '.leading'],
['.', '.'],
['', ''],
['no-dot', 'no-dot']
])('output %s uses only its namespace prefix to group into family %s', (name, family) => {
expect(transformFamily(name)).toBe(family)
})
test.each([
['attachment', 'Attachment'],
['opengraph', 'Opengraph'],
['palette', 'Palette'],
['rendered-markdown.note', 'Rendered-markdown.note'],
['pictures.original', 'Pictures.original'],
['image..webp', 'Image..webp'],
['Image.original', 'Image.original'],
['colors.v2', 'Colors.v2'],
['toString', 'ToString'],
['__proto__', '__proto__'],
['', '']
])('caption %s preserves the actual name with generic capitalization', (name, expected) => {
expect(transformCaption(name)).toBe(expected)
})
test.each([
['image/jpeg', 'JPEG'],
['image/png', 'PNG'],
['image/webp', 'WebP'],
['image/avif', 'AVIF'],
['image/svg+xml', 'SVG'],
['application/pdf', 'PDF'],
['application/zip', 'ZIP'],
['text/html', 'HTML'],
['application/json', 'JSON'],
['text/plain', 'Text'],
['text/markdown', 'Markdown'],
['application/octet-stream', 'Binary'],
[' TEXT/HTML; charset=utf-8 ', 'HTML'],
['application/future+json', 'future+json'],
['image/unknown', 'unknown'],
['<svg>', 'File'],
['text/bad subtype', 'File'],
['application/' + 'x'.repeat(25), 'File'],
['__proto__', 'File'],
['constructor', 'File'],
['toString', 'File'],
['', 'File']
])('recorded MIME %s supplies a label or conservative File fallback', (mime, expected) => {
expect(fileType(mime)).toBe(expected)
})
test('absent MIME has the same File fallback', () => {
expect(fileType()).toBe('File')
})
@@ -1,147 +1,64 @@
import { afterEach, expect, test, vi } from 'vitest'
import { languages } from '@codemirror/language-data'
import { highlightSource } from './source-highlight'
import { formatSource, SOURCE_SIZE_LIMIT, type SourceToken } from './source'
import { SOURCE_SIZE_LIMIT } from './source'
afterEach(() => vi.restoreAllMocks())
function expectReconstruction(tokens: SourceToken[], content: string) {
test('highlighting adds token classes without changing source text or whitespace', async () => {
const content = ' \r\n{"id":9007199254740993,"id":1e400,"text":"<script>literal</script>"}\t '
const tokens = await highlightSource(content, 'application/json')
expect(tokens.map((token) => token.text).join('')).toBe(content)
for (const token of tokens) {
expect(token.classes).toMatch(/^(?:tok-[\w-]+(?: tok-[\w-]+)*)?$/)
expect(Object.keys(token).sort()).toEqual(['classes', 'text'])
}
}
function expectClass(tokens: SourceToken[], text: string, name: string) {
expect(
tokens.some((token) => token.text.includes(text) && token.classes.split(' ').includes(name)),
`${text}: ${name}`
).toBe(true)
}
test('highlights already formatted JSON with existing editor classes and exact reconstruction', async () => {
const content = formatSource(
'{"name":"value","count":9007199254740993,"ok":true,"none":null}',
'application/json'
)
const tokens = await highlightSource(content, ' Application/LD+JSON; charset=UTF-8 ')
expectReconstruction(tokens, content)
expectClass(tokens, '"name"', 'tok-name')
expectClass(tokens, '"value"', 'tok-string')
expectClass(tokens, '9007199254740993', 'tok-number')
expectClass(tokens, 'true', 'tok-bool')
expectClass(tokens, 'null', 'tok-keyword')
expectClass(tokens, '{', 'tok-punctuation')
})
test('highlights HTML tags, attributes, values and comments without rendering markup', async () => {
const content = '<!-- comment -->\r\n<div class="hello">literal &lt;text&gt;</div>\r\n'
const tokens = await highlightSource(content, 'text/html')
expectReconstruction(tokens, content)
expectClass(tokens, 'div', 'tok-name')
expectClass(tokens, 'class', 'tok-name')
expectClass(tokens, '"hello"', 'tok-string')
expectClass(tokens, '<!-- comment -->', 'tok-comment')
})
test.each([
['text/css', 'a { color: red; }\r\n'],
['text/javascript', 'const count = 42;\r\n// comment\r\n'],
['text/xml', '<?xml version="1.0"?>\r\n<root count="1" />\r\n'],
['image/svg+xml', '<svg><path d="M0 0" /></svg>\r\n'],
['text/markdown', '# Heading\r\n\r\n> quote\r\n\r\n`code` and **strong**\r\n'],
['application/yaml', 'title: "value"\r\n# comment\r\n']
])('uses installed %s parser while preserving CRLF and all whitespace', async (mime, content) => {
const tokens = await highlightSource(content, mime)
expectReconstruction(tokens, content)
expect(tokens.some((token) => token.classes)).toBe(true)
})
test('shares the editor extra Markdown classes without importing editor chrome', async () => {
const content = '> quote\n\n`code`\n\n---\n'
const tokens = await highlightSource(content, 'text/markdown')
expectReconstruction(tokens, content)
expectClass(tokens, 'quote', 'tok-quote')
expectClass(tokens, 'code', 'tok-monospace')
expectClass(tokens, '---', 'tok-contentSeparator')
test('unknown MIME remains complete plaintext rather than guessing from its contents', async () => {
const content = ' \r\n<div>{"value":1}</div>\t '
expect(await highlightSource(content, 'application/octet-stream')).toEqual([
{ text: content, classes: '' }
])
})
test.each([
[
'text/html',
'<img src=x onerror="alert(1)" class="tok-attacker user-css"><script>alert(2)</script>'
],
[
'application/json',
String.raw`{"</script>":"<img src=x onerror=alert(1)>","class":"tok-attacker user-css","escape":"\u003c"}`
]
])(
'keeps XSS-shaped %s payloads literal and never derives classes from content',
async (mime, content) => {
const tokens = await highlightSource(content, mime)
expectReconstruction(tokens, content)
expect(tokens.map((token) => token.classes).join(' ')).not.toMatch(
/attacker|user-css|alert|onerror/
)
}
)
test('oversized source bypasses the parser and remains complete', async () => {
const json = languages.find((language) => language.name === 'JSON')!
const load = vi.spyOn(json, 'load')
const content = '{"value":1}'.padEnd(SOURCE_SIZE_LIMIT + 1, ' ')
test.each(['', 'text/plain', 'application/octet-stream', 'unknown.html', 'constructor'])(
'returns one complete plaintext token for unknown MIME %j',
async (mime) => {
const content = ' \r\n<div>{"key":1}</div>\t\r\n'
expect(await highlightSource(content, mime)).toEqual([{ text: content, classes: '' }])
}
)
test.each(['', ' \r\n\t', '{"incomplete":', '{"raw":1,"raw":2}'])(
'reconstructs empty, whitespace, malformed and unformatted JSON exactly: %j',
async (content) => {
expectReconstruction(await highlightSource(content, 'application/json'), content)
}
)
test('returns all original plaintext above the highlighting budget without loading a parser', async () => {
const description = languages.find((language) => language.name === 'JSON')!
const load = vi.spyOn(description, 'load')
const content = '{"key":1}'.padEnd(SOURCE_SIZE_LIMIT + 1, ' ') + '\r\n'
expect(await highlightSource(content, 'application/json')).toEqual([
{ text: content, classes: '' }
])
expect(load).not.toHaveBeenCalled()
})
test('can highlight exactly at the input size budget', async () => {
const content = '{"key":1}'.padEnd(SOURCE_SIZE_LIMIT, ' ')
const tokens = await highlightSource(content, 'application/json')
expectReconstruction(tokens, content)
expectClass(tokens, '"key"', 'tok-name')
})
test('falls back to complete plaintext when a known language is unavailable', async () => {
test('a missing language leaves the original source readable', async () => {
vi.spyOn(languages, 'find').mockReturnValueOnce(undefined)
const content = '{"key":1}\r\n'
const content = '{"value":1}\r\n'
expect(await highlightSource(content, 'application/json')).toEqual([
{ text: content, classes: '' }
])
})
test('falls back to complete plaintext when language loading rejects', async () => {
const description = languages.find((language) => language.name === 'JSON')!
vi.spyOn(description, 'load').mockRejectedValueOnce(new Error('language unavailable'))
const content = '{"key":1}\r\n'
test('language-loading failure leaves the original source readable', async () => {
const json = languages.find((language) => language.name === 'JSON')!
vi.spyOn(json, 'load').mockRejectedValueOnce(new Error('Language unavailable'))
const content = '{"value":1}\r\n'
expect(await highlightSource(content, 'application/json')).toEqual([
{ text: content, classes: '' }
])
})
test('falls back to complete plaintext when parsing fails', async () => {
test('parser failure leaves the original source readable', async () => {
const support = await languages.find((language) => language.name === 'JSON')!.load()
vi.spyOn(support.language.parser, 'parse').mockImplementationOnce(() => {
throw new Error('parse failed')
throw new Error('Parser failed')
})
const content = '{"key":1}\r\n'
const content = '{"value":1}\r\n'
expect(await highlightSource(content, 'application/json')).toEqual([
{ text: content, classes: '' }
])
+40 -116
View File
@@ -1,70 +1,21 @@
import { expect, test } from 'vitest'
import { formatSource, SOURCE_SIZE_LIMIT, sourceLanguage } from './source'
import { formatSource, SOURCE_SIZE_LIMIT } from './source'
test.each([
['application/json', 'JSON'],
[' TEXT/JSON ; charset=utf-8 ', 'JSON'],
['application/ld+json', 'JSON'],
['model/gltf+json', 'JSON'],
['text/vendor+json', 'JSON'],
['text/html', 'HTML'],
[' Application/XHTML+XML; charset=UTF-8', 'HTML'],
['text/css', 'CSS'],
['text/javascript', 'JavaScript'],
['application/javascript', 'JavaScript'],
['text/ecmascript', 'JavaScript'],
['application/ecmascript', 'JavaScript'],
['text/xml', 'XML'],
['application/xml', 'XML'],
['application/atom+xml', 'XML'],
['image/svg+xml', 'XML'],
['text/custom+xml', 'XML'],
['text/markdown', 'Markdown'],
['application/yaml', 'YAML'],
['application/x-yaml', 'YAML'],
['text/yaml', 'YAML'],
['text/x-yaml', 'YAML']
])('maps only normalized MIME %s to installed language %s', (mime, language) => {
expect(sourceLanguage(mime)).toBe(language)
})
test('formats structured JSON MIME types without changing nested values', () => {
const content = ' {"items":[null,{"active":true}],"empty":{}} '
test.each([
'',
'text/plain',
'application/octet-stream',
'application/json5',
'application/not-json',
'image/json',
'json',
'index.html',
'javascript',
'__proto__',
'constructor',
'text/html/other',
'application/bad type+json'
])('does not guess a language from %s', (mime) => {
expect(sourceLanguage(mime)).toBeUndefined()
expect(formatSource(' {"key":1}\r\n', mime)).toBe(' {"key":1}\r\n')
})
test('pretty prints only JSON, including nested nulls, arrays and empty containers', () => {
expect(
formatSource(' \r\n{"a":[null,{"b":true,"c":false},[],{}],"empty":{ }}\t', 'application/json')
).toBe(`{
"a": [
expect(formatSource(content, ' Application/Problem+JSON; charset=utf-8 ')).toBe(`{
"items": [
null,
{
"b": true,
"c": false
},
[],
{}
"active": true
}
],
"empty": {}
}`)
})
test('preserves unsafe integer, decimal, exponent and negative-zero lexemes and duplicate keys', () => {
test('formatting preserves numeric lexemes and duplicate keys rather than serializing parsed data', () => {
const values = [
'9007199254740993',
'-9007199254740993',
@@ -74,80 +25,53 @@ test('preserves unsafe integer, decimal, exponent and negative-zero lexemes and
'-0',
'1.00'
]
const input = `{${values.map((value) => `"duplicate":${value}`).join(',')}}`
expect(formatSource(input, 'application/problem+json')).toBe(
const content = `{${values.map((value) => `"duplicate":${value}`).join(',')}}`
expect(formatSource(content, 'application/json')).toBe(
`{\n${values.map((value) => ` "duplicate": ${value}`).join(',\n')}\n}`
)
})
test('retains escaped strings, whitespace inside strings and HTML as literal text', () => {
const input = String.raw`{"\u006b":"\u003cscript\u003e\r\n\t\/\\\"", "k":"a b,:{}[]", "html":"</script><img src=x onerror=alert(1)>"}`
test('formatting preserves escaped strings and whitespace inside values', () => {
const content = String.raw`{"\u006b":"\u003cscript\u003e\r\n\t\/\\\"", "k":"a b,:{}[]"}`
const expected = String.raw`{
"\u006b": "\u003cscript\u003e\r\n\t\/\\\"",
"k": "a b,:{}[]",
"html": "</script><img src=x onerror=alert(1)>"
"k": "a b,:{}[]"
}`
expect(formatSource(input, 'application/json')).toBe(expected)
expect(formatSource(content, 'application/json')).toBe(expected)
})
test.each(['null', 'true', 'false', '42', '-0', '1e400', '"a\\n b"', '[]', '{}'])(
'formats the top-level JSON value %s without changing its lexeme',
(value) => {
expect(formatSource(` \r\n${value}\t `, 'text/json')).toBe(value)
test('invalid JSON remains exactly as recorded instead of being repaired or discarded', () => {
const content = ' \r\n {"items":[1,]}\t '
expect(formatSource(content, 'application/json')).toBe(content)
})
test.each(['text/plain', 'source.json'])(
'JSON-looking content with MIME %s remains unformatted',
(mime) => {
const content = ' \r\n { "items": [1,2] }\t '
expect(formatSource(content, mime)).toBe(content)
}
)
test.each([
'',
' \r\n\t',
'{',
'{"a":1,}',
'[1,]',
'{"a":undefined}',
'{"a":NaN}',
'{"a":01}',
'{"a":"\\x01"}',
'{"a":"unescaped\nline"}',
'{"a":1} trailing',
'null null',
'/* comment */ {}',
'{"a":1}\u00a0'
])('returns malformed JSON exactly as supplied: %j', (input) => {
expect(formatSource(input, 'application/json')).toBe(input)
test('oversized source remains complete without formatting', () => {
const content = '{"value":1}'.padEnd(SOURCE_SIZE_LIMIT + 1, ' ') + '\r\n'
expect(formatSource(content, 'application/json')).toBe(content)
})
test.each([
'text/html',
'application/xhtml+xml',
'text/css',
'text/javascript',
'application/xml',
'text/markdown',
'application/yaml',
'text/plain'
])('never formats non-JSON %s, even if its contents look like JSON', (mime) => {
const input = ' \r\n { "a": [1,2] }\r\n\t'
expect(formatSource(input, mime)).toBe(input)
test('pathological nesting falls back to the complete original source', () => {
const content = '['.repeat(1000) + 'null' + ']'.repeat(1000)
expect(formatSource(content, 'application/json')).toBe(content)
})
test('formats nested JSON iteratively, with a finite depth fallback', () => {
const nested = '['.repeat(30) + 'null' + ']'.repeat(30)
const formatted = formatSource(nested, 'application/json')
expect(formatted).not.toBe(nested)
expect(formatted.replace(/\s/g, '')).toBe(nested)
const deep = '['.repeat(101) + 'null' + ']'.repeat(101)
expect(formatSource(deep, 'application/json')).toBe(deep)
})
test('formatting that would exceed the output budget leaves the original intact', () => {
const content = '[' + Array(30_000).fill('0').join(',') + ']'
test('formats at the input budget, but keeps every original character above it', () => {
const atLimit = '{}'.padEnd(SOURCE_SIZE_LIMIT, ' ')
expect(formatSource(atLimit, 'application/json')).toBe('{}')
const oversized = atLimit + '\r\n'
expect(formatSource(oversized, 'application/json')).toBe(oversized)
})
test('returns the entire original when pretty output would exceed the budget', () => {
const input = '[' + Array(20_000).fill('0').join(',') + ']'
expect(input.length).toBeLessThan(SOURCE_SIZE_LIMIT)
expect(formatSource(input, 'application/json')).toBe(input)
expect(content.length).toBeLessThan(SOURCE_SIZE_LIMIT)
expect(formatSource(content, 'application/json')).toBe(content)
})
@@ -1,321 +1,145 @@
import { beforeEach, expect, test, vi } from 'vitest'
import { expect, test, vi } from 'vitest'
import { createRawSnippet, type ComponentProps } from 'svelte'
import { render } from 'svelte/server'
import type { PublicWebPieceAsset } from '@luzzle/web.pieces'
import { makePublicConfig } from '#lib/server/config.fixture.js'
import {
publishedPiece as rawPublishedPiece,
asset
} from '#lib/pieces/viewer/fixtures/published.js'
import { publishedPiece } from '#lib/pieces/viewer/fixtures/published.js'
import Overview from './+page.svelte'
import PreviewLayout from './+layout.svelte'
const publishedPiece = {
...rawPublishedPiece,
templates: {
icon: { name: 'default', fields: { image: 'cover' } },
page: { name: 'default', fields: { image: 'cover' } },
opengraph: { name: 'default', fields: { image: 'cover' } }
}
}
const { page, resolve } = vi.hoisted(() => ({
vi.mock('$app/state', () => ({
page: {
url: new URL('https://app.test/admin/piece/books/book.md/preview/job'),
url: new URL('https://app.test/admin/piece/books/book.md/preview/current-job'),
route: { id: '/(auth)/admin/piece/[...path]/preview/[jobId]' }
},
resolve: vi.fn((route: string, params: { path: string; jobId?: string }) =>
}
}))
vi.mock('$app/paths', () => ({
resolve: (route: string, params: { path: string; jobId?: string }) =>
route
.replace('/(auth)', '')
.replace('[...path]', () => params.path)
.replace('[jobId]', () => params.jobId ?? '')
)
}))
vi.mock('$app/state', () => ({ page }))
vi.mock('$app/paths', () => ({ resolve }))
// Only the layout's five app-navigation destinations may use this mocked resolver.
// Resource/helper URLs must remain independent; this is not native Kit resolution evidence.
function expectAppNavigation(jobs = ['job']) {
const calls = jobs.flatMap((job) => {
const route = '/(auth)/admin/piece/[...path]/preview/[jobId]'
const path = 'books/book.md'
const jobId = encodeURIComponent(job)
return [
['/(auth)/admin/piece/[...path]/source', { path }],
[route, { path, jobId }],
[`${route}/page`, { path, jobId }],
[`${route}/icon`, { path, jobId }],
[`${route}/share`, { path, jobId }]
]
})
for (const call of resolve.mock.calls) expect(calls).toContainEqual(call)
}
type Data = ComponentProps<typeof Overview>['data']
type Status = 'waiting' | 'running' | 'completed' | 'failed' | 'expired'
function data(status: Status = 'completed'): Data {
const common = {
pieceTypes: ['books'],
config: makePublicConfig({
url: {
app: 'https://public.test/site',
app_assets: '/static',
luzzle_assets: 'https://cdn.test/published'
}
}),
meta: { title: 'Garden', description: 'Description' },
preview: true as const,
file: 'books/book.md',
job: 'job',
createdAt: '1970-01-01T00:00:00.000Z',
finishedAt:
status === 'completed' || status === 'failed' || status === 'expired'
? '1970-01-01T00:00:02.000Z'
: null,
phases: [
{
job_id: 'job',
phase: 'transform',
status: 'completed',
started_at: 1000,
finished_at: 2000,
message: null
}
],
logs: [
{
job_id: 'job',
phase: 'transform',
line_number: 1,
ts: 1000,
level: 'info',
message: 'Preview log <script>escaped()</script>'
}
]
}
if (status === 'failed') return { ...common, status, errorMessage: 'Preview failed fixture' }
if (status !== 'completed') return { ...common, status }
const outputs = [
asset('image.original', 'generated/cover.jpg', 'image'),
asset('image.s.webp', 'generated/s.webp', 'image', 'image/webp'),
const job = {
pieceTypes: ['books'],
config: makePublicConfig({
url: {
app: 'https://public.test/site',
app_assets: '/static',
luzzle_assets: 'https://cdn.test/published'
}
}),
meta: { title: 'Garden', description: 'Description' },
preview: true as const,
file: 'books/book.md',
job: 'current-job',
createdAt: '2026-11-01T12:00:00.000Z',
finishedAt: null,
phases: [
{
...asset('markdown', 'generated/body.html', 'piece', 'text/html'),
content: '<script>privateBody()</script><h1>Body</h1>'
},
job_id: 'current-job',
phase: 'transform',
status: 'completed',
started_at: Date.UTC(2026, 10, 1, 12),
finished_at: Date.UTC(2026, 10, 1, 12, 0, 1),
message: null
}
],
logs: [
{
...asset('markdown.description', null, 'piece', 'text/html'),
content: '<p>privateDescription()</p>'
},
{
...asset('palette', null, 'piece', 'application/json'),
content: '{"color":"#abc","count":42}'
},
asset('custom.invalid', '../private.pdf', 'piece', 'application/pdf'),
asset('custom.missing', null, 'piece', 'application/pdf')
job_id: 'current-job',
phase: 'transform',
line_number: 1,
ts: Date.UTC(2026, 10, 1, 12),
level: 'info',
message: 'Preview seed line'
}
]
return {
...common,
status,
outputs,
piece: {
...publishedPiece,
title: 'Preview <Draft>',
assets: [...outputs, asset('attachment', 'archive/manual.pdf')]
}
const generatedCover: PublicWebPieceAsset = {
asset_key: 'cover',
transformation: 'image.original',
asset_path: 'generated/cover.jpg',
mime_type: 'image/jpeg'
}
const archiveAttachment: PublicWebPieceAsset = {
asset_key: 'manual',
transformation: 'attachment',
asset_path: 'archive/manual.pdf',
mime_type: 'application/pdf'
}
const completedPreview = {
...job,
status: 'completed',
finishedAt: '2026-11-01T12:00:02.000Z',
outputs: [generatedCover],
piece: {
...publishedPiece,
title: 'Preview <Draft>',
templates: {
icon: { name: 'default', fields: { image: 'cover' } },
page: { name: 'default', fields: { image: 'cover' } },
opengraph: { name: 'default', fields: { image: 'cover' } }
},
tags: []
}
}
assets: [generatedCover, archiveAttachment]
},
tags: []
} satisfies Data
function tree(props: Data) {
function renderPreview(data: Data) {
const params = { path: data.file, jobId: data.job }
const children = createRawSnippet(() => ({
render: () =>
render(Overview, { props: { data: props, params: { path: props.file, jobId: props.job } } })
.body
render: () => render(Overview, { props: { data, params } }).body
}))
return render(PreviewLayout, {
props: { data: props, params: { path: props.file, jobId: props.job }, children }
}).body.replace(/<!--[^]*?-->/g, '')
return render(PreviewLayout, { props: { data, params, children } }).body
}
beforeEach(() => {
page.url = new URL('https://app.test/admin/piece/books/book.md/preview/job')
page.route.id = '/(auth)/admin/piece/[...path]/preview/[jobId]'
vi.clearAllMocks()
})
test.each(['running', 'completed'] as const)(
'%s Preview renders duplicated seed logs once and retains new lines',
'%s Preview deduplicates replayed seed logs while retaining distinct lines',
(status) => {
const props = data(status)
const first = { ...props.logs[0], message: 'Duplicated seed line' }
props.logs = [first, { ...first }, { ...first, line_number: 2, message: 'New seed line' }]
const html = tree(props)
expect(html.match(/Duplicated seed line/g)).toHaveLength(1)
expect(html.match(/New seed line/g)).toHaveLength(1)
expect(html).toContain('2 lines')
const replayed = { ...job.logs[0], message: 'Replayed seed line' }
const logs = [replayed, { ...replayed }, { ...replayed, line_number: 2, message: 'Later line' }]
let data: Data = { ...job, status: 'running', logs }
if (status === 'completed') data = { ...completedPreview, logs }
const html = renderPreview(data)
expect(html.match(/Replayed seed line/g)).toHaveLength(1)
expect(html.match(/Later line/g)).toHaveLength(1)
}
)
test('completed Preview reads title, consolidated facts, Preview logs, then assets with a single main/title', () => {
const html = tree(data())
expect(html.match(/<main\b/g)).toHaveLength(1)
expect(html.match(/<h1\b/g)).toHaveLength(1)
test('completed Preview inspects only generated outputs in the current job namespace', () => {
const html = renderPreview(completedPreview)
expect(html).toContain('Preview &lt;Draft>')
expect(html).toContain('Preview assets</h2>')
expect(html).toContain('Preview logs</h2>')
expect(html).toContain('Preview log &lt;script>escaped()&lt;/script>')
expect(html.indexOf('Preview &lt;Draft>')).toBeLessThan(html.indexOf('Preview</caption>'))
expect(html.indexOf('Preview</caption>')).toBeLessThan(html.indexOf('Preview logs</h2>'))
expect(html.indexOf('Preview logs</h2>')).toBeLessThan(html.indexOf('Preview assets</h2>'))
const facts = html.slice(html.indexOf('<caption'), html.indexOf('</table>'))
const logSection = html.slice(
html.indexOf('Preview logs</h2>'),
html.indexOf('Preview assets</h2>')
)
expect(facts).toContain('books/book.md</bdi>')
expect(facts).toContain('data-status="completed"')
expect(facts).toContain('>Completed</span>')
expect(facts).toContain('1s total time')
expect(facts).not.toContain('<svg')
expect(logSection).not.toContain('books/book.md')
expect(logSection).not.toContain('Preview completed')
expect(logSection).not.toContain('total time')
expect(html).not.toContain('status-card')
expect(html.indexOf('Preview logs</h2>')).toBeLessThan(html.indexOf('Preview log &lt;script>'))
expect(html).toContain('datetime="1970-01-01T00:00:00.000Z"')
expect(html).toContain('datetime="1970-01-01T00:00:02.000Z"')
for (const caption of ['Preview', 'Image', 'Markdown', 'Palette', 'Custom'])
expect(html).toContain(`${caption}</caption>`)
expect(html.match(/Image<\/caption>/g)).toHaveLength(1)
expect(html.match(/Markdown<\/caption>/g)).toHaveLength(1)
for (const output of ['image.original', 'image.s.webp', 'markdown.description'])
expect(html).toContain(`${output}</bdi>`)
for (const path of ['generated/cover.jpg', 'generated/s.webp', 'generated/body.html'])
expect(html).toContain(`href="/admin/preview/job/asset/${path}"`)
expect(html).not.toMatch(/ download(?:[= >])/)
expect(html).toContain('aria-label="Show Markdown: Asset 2, Content"')
expect(html).toContain('aria-label="Show Markdown.description: Asset 2, Content"')
expect(html).toContain('aria-label="Show Palette: Asset 2, Content"')
expect(html).toContain('aria-expanded="false"')
expect(html).not.toContain('{"color":"#abc","count":42}')
expect(html).not.toContain('color</bdi>')
expect(html).not.toContain('count</bdi>')
expect(html).toContain('Unavailable</small>')
expect(html).toContain('Not recorded')
for (const absent of [
'privateBody()',
'privateDescription()',
'<script>',
'<iframe',
'archive/manual.pdf',
'Attachment</caption>',
'Last publish',
'Publication</caption>',
'Matches publish',
'https://public.test',
'https://cdn.test',
'/pieces/assets/',
'/admin/asset/',
'background-color: #abc'
])
expect(html).not.toContain(absent)
expectAppNavigation()
})
test.each(['waiting', 'running', 'failed', 'expired'] as const)(
'%s Preview puts current facts above logs without inventing assets or publication facts',
(status) => {
const props = data(status)
const html = tree(props)
expect(html.match(/<main\b/g)).toHaveLength(1)
expect(html).toContain('Piece overview')
expect(html).toContain('Preview</caption>')
expect(html).toContain('Preview logs</h2>')
const label = {
waiting: 'Queued',
running: 'Building',
failed: 'Failed',
expired: 'Expired'
}[status]
expect(html).toContain(`>${label}</span>`)
expect(html).toContain('datetime="1970-01-01T00:00:00.000Z"')
if (status === 'waiting' || status === 'running')
expect(html).not.toContain('datetime="1970-01-01T00:00:02.000Z"')
expect(html.indexOf('Preview</caption>')).toBeLessThan(html.indexOf('Preview logs</h2>'))
expect(html).not.toContain('Preview assets')
expect(html).not.toContain('Preview outputs')
expect(html).not.toContain('Publication</caption>')
expect(html).not.toContain('Preview &lt;Draft>')
expect(html).not.toContain('/asset/generated/')
expectAppNavigation()
}
)
test('completed workflow without an assembled result keeps facts/logs and omits assets rather than crashing', () => {
const running = data('running')
if (running.status !== 'running') throw new Error('Expected running fixture')
const props = { ...running, status: 'completed' as const }
const html = tree(props)
expect(html.match(/<main\b/g)).toHaveLength(1)
expect(html.match(/<h1\b/g)).toHaveLength(1)
expect(html).toContain('Preview</h1>')
expect(html).toContain('Preview</caption>')
expect(html).toContain('>Completed</span>')
expect(html).toContain('Preview logs</h2>')
expect(html).toContain('Preview log &lt;script>escaped()&lt;/script>')
expect(html.indexOf('Preview</caption>')).toBeLessThan(html.indexOf('Preview logs</h2>'))
expect(html).not.toContain('Preview assets')
expect(html).not.toContain('None recorded')
expect(html).not.toContain('/asset/generated/')
expectAppNavigation()
})
test('recorded generated Share PNG remains a View link in Overview', () => {
const props = data()
if (props.status !== 'completed' || !props.outputs) throw new Error('Expected assembled fixture')
props.outputs = [
...props.outputs,
asset('opengraph', 'generated/share.png', 'piece', 'image/png')
]
const html = tree(props)
const png = html
.match(/<a\b[^>]*>/g)
?.find((tag) => tag.includes('href="/admin/preview/job/asset/generated/share.png"'))
expect(png).toBeDefined()
expect(png).toContain('aria-label="View opengraph: generated/share.png"')
expect(png).toContain('target="_blank"')
expect(png).toContain('rel="external noopener noreferrer"')
expect(png).not.toMatch(/\bdownload(?:=|\s|>)/)
expect(html.match(/<main\b/g)).toHaveLength(1)
expectAppNavigation()
})
test('empty generated outputs do not turn SDK passthroughs into downloadable preview artifacts', () => {
const props = data()
if (props.status !== 'completed') throw new Error('Expected completed fixture')
props.outputs = []
const html = tree(props)
expect(html).toContain('Preview assets</h2>')
expect(html).toContain('None recorded')
expect(html).not.toContain('Image</caption>')
expect(html).toContain('Preview logs')
expect(html).toContain('Preview seed line')
expect(html).toContain('href="/admin/preview/current-job/asset/generated/cover.jpg"')
expect(html).not.toContain('archive/manual.pdf')
expect(html).not.toContain('/asset/generated/')
expectAppNavigation()
expect(html).not.toContain('/pieces/assets/')
expect(html.indexOf('Preview logs')).toBeLessThan(html.indexOf('Preview assets'))
expect(html.match(/<main\b/g)).toHaveLength(1)
})
test('replacement completed snapshots use only the new job namespace and metadata with mocked app navigation', () => {
tree(data())
const props = data()
if (props.status !== 'completed' || !props.piece) throw new Error('Expected assembled fixture')
props.job = 'new-job'
props.piece = { ...props.piece, title: 'New result' }
props.outputs = [asset('custom', 'new.txt', 'new', 'text/plain')]
page.url = new URL('https://app.test/admin/piece/books/book.md/preview/new-job')
page.route.id = '/(auth)/admin/piece/[...path]/preview/[jobId]'
const html = tree(props)
expect(html).toContain('New result')
expect(html).toContain('href="/admin/preview/new-job/asset/new.txt"')
expect(html).not.toContain('/preview/job/asset/')
test('empty generated outputs do not fall back to SDK passthrough assets', () => {
const html = renderPreview({ ...completedPreview, outputs: [] })
expect(html).toContain('Preview assets')
expect(html).toContain('None recorded')
expect(html).not.toContain('generated/cover.jpg')
expectAppNavigation(['job', 'new-job'])
expect(html).not.toContain('archive/manual.pdf')
})
test('a completed job without an assembled result keeps its facts and logs rather than crashing', () => {
const html = renderPreview({ ...job, status: 'completed' })
expect(html).toContain('Preview seed line')
expect(html).toContain('Preview logs')
expect(html).toContain('Completed')
expect(html).not.toContain('Preview assets')
expect(html.match(/<main\b/g)).toHaveLength(1)
})
@@ -1,45 +1,25 @@
import { readFileSync } from 'node:fs'
import { beforeEach, expect, test, vi } from 'vitest'
import { render } from 'svelte/server'
import type { ComponentProps } from 'svelte'
import { MarkdownPublicationStatus } from '#lib/pieces/types.js'
import { editorHref, publishedHref } from '#lib/pieces/navigation.js'
import { makePublicConfig } from '#lib/server/config.fixture.js'
import {
publishedPiece as rawPublishedPiece,
asset
} from '#lib/pieces/viewer/fixtures/published.js'
import { publishedPiece as originalPiece, asset } from '#lib/pieces/viewer/fixtures/published.js'
import Overview from './+page.svelte'
import PublishedTree from './fixtures/PublishedTree.svelte'
const publishedPiece = {
...rawPublishedPiece,
templates: {
icon: { name: 'default', fields: { image: 'cover' } },
page: { name: 'default', fields: { image: 'cover' } },
opengraph: { name: 'default', fields: { image: 'cover' } }
}
}
const { page, resolve } = vi.hoisted(() => ({
const { page } = vi.hoisted(() => ({
page: {
url: new URL('https://app.test/'),
route: { id: '/(auth)/admin/piece/[...path]/published' },
params: {},
data: { config: { url: { app: '/garden', luzzle_assets: '/cdn' } } }
},
resolve: vi.fn((route: string, params?: { path?: string; tag?: string }) => {
if (route === '/tags/[tag]' && params?.tag !== undefined) return `/tags/${params.tag}`
if (route.startsWith('/(auth)/admin/piece/[...path]/') && params?.path !== undefined)
return route.replace('/(auth)', '').replace('[...path]', params.path)
if (params || route.includes('[')) throw new Error(`Unexpected route: ${route}`)
return route.startsWith('/') ? route : `/${route}`
})
}
}))
vi.mock('$app/state', () => ({ page }))
vi.mock('$app/paths', () => ({ resolve }))
// These synchronous renderer stubs establish route props, URLs and landmarks;
// they do not exercise native Kit resolution, the wrappers' static catalogs or client prop updates.
vi.mock('$app/paths', () => ({
resolve: (route: string, params?: { path?: string; tag?: string }) => {
if (route === '/tags/[tag]') return `/tags/${params?.tag}`
return route.replace('/(auth)', '').replace('[...path]', () => params?.path ?? '')
}
}))
vi.mock('#lib/pieces/components/page.svelte', async () => ({
default: (await import('#lib/pieces/viewer/fixtures/PageRenderer.svelte')).default
}))
@@ -49,550 +29,119 @@ vi.mock('#lib/pieces/components/icon.svelte', async () => ({
vi.mock('#lib/pieces/components/opengraph.svelte', async () => ({
default: (await import('#lib/pieces/viewer/fixtures/Composition.svelte')).default
}))
function data(
status: MarkdownPublicationStatus = MarkdownPublicationStatus.MATCHING,
time: number | null = 1234
): ComponentProps<typeof Overview>['data'] {
const config = makePublicConfig({
url: { app: '/garden', app_assets: '/static', luzzle_assets: '/cdn' }
})
config.content.text = { title: 'Garden', description: 'Description' }
function publishedData(): ComponentProps<typeof Overview>['data'] {
return {
pieceTypes: ['books'],
file: 'books/<draft #?%>.books.md',
piece: publishedPiece,
file: 'books/draft #?.books.md',
piece: {
...originalPiece,
templates: {
icon: { name: 'default', fields: { image: 'cover' } },
page: { name: 'default', fields: { image: 'cover' } },
opengraph: { name: 'default', fields: { image: 'cover' } }
},
assets: [
...originalPiece.assets,
asset('opengraph', 'generated/share.png', 'piece', 'image/png')
]
},
tags: [{ slug: 'fiction', tag: 'Fiction' }],
publication: { lastPublishedAt: time, markdownStatus: status },
publishedUrl: 'https://public.test/garden/pieces/books/published%20%23%3F%25',
config,
publication: {
lastPublishedAt: Date.UTC(2026, 10, 1, 12),
markdownStatus: MarkdownPublicationStatus.MATCHING
},
publishedUrl: 'https://public.test/pieces/books/published-book',
config: makePublicConfig({
url: { app: '/garden', app_assets: '/static', luzzle_assets: '/cdn' }
}),
meta: { title: 'Garden', description: 'Description' }
}
}
beforeEach(() => {
vi.clearAllMocks()
page.url = new URL(publishedHref(data().file), 'https://app.test')
page.route.id = '/(auth)/admin/piece/[...path]/published'
})
test('Overview shows escaped published identity, actual completion time and external public link', () => {
const html = render(Overview, { props: { data: data(), params: { path: data().file } } }).body
test('overview shows the published snapshot, recorded completion time and public destinations', () => {
const data = publishedData()
const html = render(Overview, { props: { data, params: { path: data.file } } }).body
expect(html).toContain('Published &lt;Book>')
expect(html).not.toContain('books/&lt;draft #?%>.books.md')
expect(html).toContain('Type')
expect(html).toContain('books')
expect(html).toContain('<caption')
expect(html).toContain('Publication</caption>')
expect(html).toMatch(/<th[^>]*scope="row"[^>]*>Last publish<\/th>/)
expect(html).not.toMatch(/<th[^>]*scope="row"[^>]*>Source<\/th>/)
expect(html).not.toContain('Excludes')
expect(html).not.toContain('Unsaved edits · attachment bytes')
expect(html).not.toContain('Status covers indexed Markdown')
expect(html).toContain('datetime="1970-01-01T00:00:01.234Z"')
expect(html).toContain('UTC')
expect(html).toContain(
'href="https://public.test/garden/pieces/books/published%20%23%3F%25" target="_blank" rel="noopener noreferrer"'
)
expect(html).toContain(`<bdi dir="ltr">${data().publishedUrl}</bdi>`)
expect(html).not.toContain(`href="${editorHref(data().file)}"`)
expect(html).not.toContain('>Open<')
expect(html).not.toContain('<details')
expect(html).toContain('href="/cdn/pieces/assets/published/cover.jpg"')
expect(html.match(/<main /g)).toHaveLength(1)
expect(html).not.toContain('<draft')
expect(html).not.toContain('Serving metadata only')
expect(html).not.toContain('/admin/asset')
})
test('only the public URL remains a linked field value; long source paths are absent', () => {
const props = data()
props.piece = { ...publishedPiece, assets: [] }
props.file = `${'nested folder/'.repeat(24)}雪 <long & "draft">.books.md`
props.publishedUrl = `https://public.test/${'long-prefix/'.repeat(24)}published`
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
const links = html.match(/<a\b(?:[^>"']|"[^"]*"|'[^']*')*>[\s\S]*?<\/a>/g) ?? []
expect(links).toHaveLength(1)
for (const link of links) {
expect(link).toMatch(/class="truncated[^"]*"/)
expect(link).toMatch(/<bdi[^>]*dir="ltr"[^>]*>/)
expect(link).not.toContain('aria-label=')
}
expect(html).not.toContain(editorHref(props.file))
expect(html).not.toContain('nested folder/')
expect(links[0]).toContain(`href="${props.publishedUrl}"`)
expect(links[0]).toContain(`>${props.publishedUrl}</bdi>`)
expect(links[0]).toContain('target="_blank" rel="noopener noreferrer"')
for (const excluded of ['<details', '<summary', '>Open<', '<long'])
expect(html).not.toContain(excluded)
})
test('publication rows retain explicit table semantics when styled as narrow-screen fields', () => {
const props = data()
props.piece = { ...publishedPiece, assets: [] }
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
const publication = html.slice(0, html.indexOf('</table>') + '</table>'.length)
expect(publication).toMatch(/<table[^>]*role="table"/)
expect(publication).toMatch(/<tbody[^>]*role="rowgroup"/)
expect(publication.match(/<tr[^>]*role="row"/g)).toHaveLength(4)
expect(publication.match(/<th[^>]*scope="row"[^>]*role="rowheader"/g)).toHaveLength(4)
expect(publication.match(/<td[^>]*role="cell"/g)).toHaveLength(4)
})
test('publication and asset tables use the same caption, cell padding and metadata size', () => {
const sources = [
readFileSync(
new URL('../../../../../../lib/pieces/viewer/PieceOverview.svelte', import.meta.url),
'utf8'
),
readFileSync(
new URL('../../../../../../lib/pieces/viewer/AssetOutputs.svelte', import.meta.url),
'utf8'
)
]
for (const source of sources) {
const styles = source.slice(source.indexOf('<style>'))
expect(styles.match(/\bcaption\)?\s*\{([^}]+)\}/)?.[1]).toContain(
'padding: var(--space-2) var(--space-3)'
)
expect(
styles.match(/\bth\)?,\s*(?:\.overview :global\(\.overview-facts )?td\)?\s*\{([^}]+)\}/)?.[1]
).toContain('padding: var(--space-2) var(--space-3)')
const tableRules = [...styles.matchAll(/\btable\)?\s*\{([^}]+)\}/g)]
expect(tableRules.some((rule) => rule[1].includes('font-size: var(--font-size-xs)'))).toBe(true)
expect(styles.slice(styles.indexOf('@media'))).toContain(
'padding: var(--space-2) var(--space-3)'
)
}
})
test('epoch-zero completion is a recorded time, not an Unknown state', () => {
const props = data(MarkdownPublicationStatus.MATCHING, 0)
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
expect(html).toContain('datetime="1970-01-01T00:00:00.000Z"')
expect(html).toContain('datetime="2026-11-01T12:00:00.000Z"')
expect(html).toContain('data-status="matching"')
expect(html).toContain(`href="${data.publishedUrl}"`)
expect(html).toContain('href="/cdn/pieces/assets/generated/share.png"')
expect(html).not.toContain('/admin/preview/')
expect(html.match(/<main\b/g)).toHaveLength(1)
})
test('uses the app base when no asset CDN is configured', () => {
const data = publishedData()
data.config.url.luzzle_assets = ''
const html = render(Overview, {
props: { data, params: { path: data.file } }
}).body
expect(html).toContain('href="/garden/pieces/assets/generated/share.png"')
})
test('unpublished changes do not hide the previous publication or claim it matches the source', () => {
const data = publishedData()
data.publication.markdownStatus = MarkdownPublicationStatus.CHANGED
const html = render(Overview, { props: { data, params: { path: data.file } } }).body
expect(html).toContain('data-status="changed"')
expect(html).toContain('Unpublished changes')
expect(html).not.toContain('Matches publish')
expect(html).toContain('datetime="2026-11-01T12:00:00.000Z"')
expect(html).toContain(`href="${data.publishedUrl}"`)
})
test('missing publication evidence remains unknown rather than using source dates', () => {
const data = publishedData()
data.publication = { lastPublishedAt: null, markdownStatus: MarkdownPublicationStatus.UNKNOWN }
const html = render(Overview, { props: { data, params: { path: data.file } } }).body
expect(html).toContain('data-status="unknown"')
expect(html).toContain('Unknown')
expect(html).not.toMatch(/<time\b/)
})
test('the reset layout retains configured branding and noindex without adding public SEO', () => {
const data = publishedData()
data.config.content.images.favicon = '/assets/favicon/custom.svg'
const result = render(PublishedTree, { props: { data } })
expect(result.head).toContain('rel="icon" href="/assets/favicon/custom.svg"')
expect(result.head).toContain('Published: Published &lt;Book>')
expect(result.head).toContain('name="robots" content="noindex"')
expect(result.head).not.toContain('property="og:')
expect(result.head).not.toContain('name="description"')
expect(result.body).toContain('href="/admin/piece/books/draft%20%23%3F.books.md/source"')
})
test.each([
[MarkdownPublicationStatus.MATCHING, 'Matches publish'],
[MarkdownPublicationStatus.CHANGED, 'Unpublished changes'],
[MarkdownPublicationStatus.UNKNOWN, 'Unknown']
] as const)(
'status %s and Unknown timestamp are truthful and independent of source dates',
(status, copy) => {
const html = render(Overview, {
props: { data: data(status, null), params: { path: data().file } }
}).body
expect(html).toMatch(new RegExp(`data-status="${status}"[^>]*>${copy}</span>`))
expect(html).toMatch(/<th[^>]*scope="row"[^>]*>State<\/th>/)
expect(html.replace(/<!--[^]*?-->/g, '').replace(/\s+/g, ' ')).toMatch(
/Last publish<\/th>\s*<td[^>]*>\s*<span[^>]*>Unknown<\/span>/
)
expect(html).not.toContain('Unsaved edits · attachment bytes')
expect(html).not.toContain('Indexed Markdown')
expect(html).not.toContain('<time')
}
)
{ view: 'page', marker: 'data-renderer="page"' },
{ view: 'icon', marker: 'data-renderer="icon"' },
{ view: 'share', marker: 'data-renderer="composition"' }
] as const)('the $view route delegates the published piece to its renderer', ({ view, marker }) => {
const data = publishedData()
page.route.id = `/(auth)/admin/piece/[...path]/published/${view}`
test.each(['https://cdn.test/prefix/', '/cdn/prefix/', ''])(
'Overview selects public CDN/app fallback for base %s',
(base) => {
const props = data()
props.config.url.luzzle_assets = base
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
expect(html).toContain(
`href="${(base || '/garden').replace(/\/+$/, '')}/pieces/assets/published/cover.jpg"`
)
}
)
const html = render(PublishedTree, { props: { data, view } }).body
test('Overview, not Share, links only the actual recorded generated output', () => {
const props = data()
props.piece = {
...publishedPiece,
assets: [asset('opengraph', 'piece/generated.png', 'piece', 'image/png')]
expect(html).toContain(marker)
expect(html).toContain('Published &lt;Book>')
expect(html.match(/<main\b/g)).toHaveLength(1)
if (view === 'page') {
expect(html).toContain('href="/tags/fiction"')
expect(html).toContain('Fiction')
}
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
const generatedLink = html.match(
/<a[^>]*href="\/cdn\/pieces\/assets\/piece\/generated.png"[^>]*>/
)?.[0]
expect(generatedLink).toContain('target="_blank"')
expect(generatedLink).toContain('rel="external noopener noreferrer"')
expect(html).toMatch(/<a[^>]*href="\/cdn\/pieces\/assets\/piece\/generated.png"[^>]*>View<\/a>/)
expect(html).not.toContain('>Open<')
expect(html).not.toContain('<details')
expect(html).not.toContain('<img')
expect(html).not.toContain('Rendered share composition')
if (view === 'share') expect(html).not.toContain('generated/share.png')
})
test('Overview keeps JSON and HTML collapsed behind Show without exposing or executing either', () => {
const props = data()
props.piece = {
...publishedPiece,
assets: [
{
...asset('palette', null, 'piece', 'application/json'),
content: '{"background":"#123456","accent":"#abcdef"}'
},
{
...asset('markdown', null, 'piece', 'text/html'),
content: '<script>alert(1)</script><h1>Serving body</h1>'
},
asset('custom.output', 'published/output.pdf', 'piece', 'application/pdf')
]
}
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
expect(html).toContain('Palette</caption>')
expect(html).toContain('aria-label="Show Palette: Asset 1, Content"')
expect(html).not.toContain('{"background":"#123456","accent":"#abcdef"}')
expect(html).not.toContain('background</bdi>')
expect(html).not.toContain('accent</bdi>')
expect(html).not.toContain('style=')
expect(html).toContain('Markdown</caption>')
expect(html).toMatch(/<button\b[^>]*>[\s\S]*?Show[\s\S]*?<\/button>/)
expect(html).toContain('aria-expanded="false"')
expect(html).not.toContain('alert(1)')
expect(html).not.toContain('Serving body')
expect(html).not.toContain('<script>')
expect(html).not.toContain('<h1>Serving body')
expect(html).toContain('href="/cdn/pieces/assets/published/output.pdf"')
expect(html).not.toContain('Other published files')
expect(html.match(/<main /g)).toHaveLength(1)
})
test('Overview retains every recorded Opengraph output for inspection, including invalid and non-PNG rows', () => {
const props = data()
props.piece = {
...publishedPiece,
assets: [
asset('opengraph', 'piece/image.png', 'piece', 'image/png'),
asset('opengraph', 'piece/image.jpg', 'piece', 'image/jpeg'),
asset('opengraph', '../unsafe.png', 'piece', 'image/png')
]
}
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
expect(html).toContain('href="/cdn/pieces/assets/piece/image.png"')
expect(html).toContain('href="/cdn/pieces/assets/piece/image.jpg"')
expect(html).toContain('../unsafe.png</bdi>')
expect(html).not.toContain('href="/cdn/pieces/assets/..')
expect(html).toContain('Unavailable</small>')
})
test('Overview hides internal asset identifiers and preserves related outputs with readable groups', () => {
const props = data()
const first = `internal-asset-${'a'.repeat(64)}`
const second = `internal-asset-${'b'.repeat(64)}`
const piece = `internal-piece-${'c'.repeat(64)}`
props.piece = {
...publishedPiece,
assets: [
asset('image.original', 'one/cover.jpg', first),
asset('image.s.webp', 'one/s.webp', first, 'image/webp'),
asset('attachment', 'one/doc.pdf', first, 'application/pdf'),
{ ...asset('highlight', null, first, 'text/html'), content: '<code>one</code>' },
asset('image.original', 'two/cover.jpg', second),
asset('image.s.webp', 'two/s.webp', second, 'image/webp'),
asset('attachment', 'two/doc.pdf', second, 'application/pdf'),
{ ...asset('highlight', null, second, 'text/html'), content: '<code>two</code>' },
{ ...asset('palette', null, piece, 'application/json'), content: '{"background":"#123456"}' }
]
}
const html = render(Overview, {
props: { data: props, params: { path: props.file } }
}).body.replace(/<!--[^]*?-->/g, '')
for (const key of [first, second, piece]) expect(html).not.toContain(key)
for (const excluded of ['>Key<', 'Technical details', '<details'])
expect(html).not.toContain(excluded)
expect(html).toContain('Asset 1')
expect(html).toContain('Asset 2')
for (const path of [
'one/cover.jpg',
'one/s.webp',
'one/doc.pdf',
'two/cover.jpg',
'two/s.webp',
'two/doc.pdf'
]) {
expect(html).toContain(`href="/cdn/pieces/assets/${path}"`)
}
expect(html).toContain('aria-label="Show Highlight: Asset 1, Content"')
expect(html).toContain('aria-label="Show Highlight: Asset 2, Content"')
expect(html).not.toContain('&lt;code>one&lt;/code>')
expect(html).not.toContain('&lt;code>two&lt;/code>')
expect(html).toContain('aria-label="Show Palette: Asset 3, Content"')
expect(html).not.toContain('{"background":"#123456"}')
expect(html).not.toContain('style=')
expect(html.match(/Image<\/caption>/g)).toHaveLength(1)
expect(html).toContain('image.original</bdi>')
expect(html).toContain('image.s.webp</bdi>')
expect(html).not.toContain('>Variant<')
expect(html.match(/<main /g)).toHaveLength(1)
})
test('every transform uses View or inline Show with no color or image roles', () => {
const props = data()
props.piece = {
...publishedPiece,
assets: [
asset('pictures.original', 'published/cover.jpg', 'cover'),
asset('pictures.s.webp', 'published/s.webp', 'cover', 'image/webp'),
{
...asset('rendered-note', null, 'piece', 'text/html'),
content: '<script>bad()</script><h1>Serving HTML</h1>'
},
{
...asset('colors.v2', null, 'piece', 'application/json'),
content: '{"background":"#123456","swatches":["#fff","#abc"]}'
},
{
...asset('custom.output', 'published/body.html', 'piece', 'text/html'),
content: '<b>File and inline</b>'
}
]
}
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
for (const caption of ['Pictures', 'Rendered-note', 'Colors', 'Custom'])
expect(html).toContain(`${caption}</caption>`)
for (const path of ['published/cover.jpg', 'published/s.webp', 'published/body.html'])
expect(html).toContain(`href="/cdn/pieces/assets/${path}"`)
expect(html).toContain('aria-label="Show Rendered-note: Asset 2, Content"')
expect(html).toContain('aria-label="Show Colors.v2: Asset 2, Content"')
expect(html).not.toContain('{"background":"#123456","swatches":["#fff","#abc"]}')
expect(html).toContain('aria-label="Show Custom.output: Asset 2, Content"')
expect(html).not.toContain('bad()')
expect(html).not.toContain('File and inline')
expect(html).not.toContain('background</bdi>')
expect(html).not.toContain('swatches</bdi>')
expect(html).not.toContain('style=')
expect(html.match(/<button /g)).toHaveLength(3)
expect(html).not.toMatch(/ download(?:[= >])/)
expect(html).not.toContain('<script>')
expect(html).not.toContain('<h1>Serving HTML')
expect(html).not.toContain('Colors (palette)</caption>')
expect(html.match(/<main /g)).toHaveLength(1)
})
test('inline Show controls keep complete long JSON and text out of the collapsed page', () => {
const props = data()
const big = ' 雪😀\n'.repeat(300)
props.piece = {
...publishedPiece,
assets: [
{ ...asset('future-text-job', null, 'piece', 'text/plain'), content: big },
{
...asset('markdown.description', null, 'piece', 'application/json'),
content: JSON.stringify({ count: 42, label: 'short', nested: { long: big } })
},
asset('future-file-job', 'published/download.zip', 'piece', 'application/zip')
]
}
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
expect(html).toContain('aria-label="Show Future-text-job: Asset 1, Content"')
expect(html).toContain('aria-label="Show Markdown.description: Asset 1, Content"')
expect(html).not.toContain('42</code>')
expect(html).not.toContain('short</code>')
expect(html).not.toContain('Show Markdown.description: Asset 1, count')
expect(html).not.toContain('Show Markdown.description: Asset 1, label')
expect(html).not.toContain(big)
expect(html).toContain('href="/cdn/pieces/assets/published/download.zip"')
expect(html).toContain('aria-label="View future-file-job: published/download.zip"')
expect(html).not.toMatch(/ download(?:[= >])/)
expect(html).not.toContain('<textarea')
expect(html).not.toContain('<script>')
})
test('original image and palette names receive the same ordinary presentation as renamed jobs', () => {
const props = data()
props.piece = {
...publishedPiece,
assets: [
asset('image.original', 'published/a.jpg', 'piece'),
asset('image.s.webp', 'published/a.webp', 'piece', 'image/webp'),
{
...asset('palette', null, 'piece', 'application/json'),
content: '{"color":"#abc","swatches":["#abc","#fff"]}'
},
{
...asset('colors.v2', null, 'piece', 'application/json'),
content: '{"color":"#abc","swatches":["#abc","#fff"]}'
}
]
}
const html = render(Overview, { props: { data: props, params: { path: props.file } } }).body
for (const caption of ['Image', 'Palette', 'Colors'])
expect(html).toContain(`${caption}</caption>`)
for (const excluded of [
'>Images</caption>',
'>Variant<',
'Colors (palette)',
'background-color',
'class="swatch',
'None recorded'
])
expect(html).not.toContain(excluded)
expect(html).toContain('aria-label="Show Palette: Asset 1, Content"')
expect(html).toContain('aria-label="Show Colors.v2: Asset 1, Content"')
expect(html.match(/<button /g)).toHaveLength(2)
expect(html).not.toContain('{"color":"#abc","swatches":["#abc","#fff"]}')
expect(html).not.toContain('color</bdi>')
expect(html).not.toContain('swatches</bdi>')
for (const path of ['published/a.jpg', 'published/a.webp'])
expect(html).toContain(`href="/cdn/pieces/assets/${path}"`)
expect(html).not.toMatch(/ download(?:[= >])/)
})
test('Overview renders one table per family with every image and Markdown output retained in an Output column', () => {
const props = data()
props.piece = {
...publishedPiece,
assets: [
asset('image.original', 'published/original.jpg', 'image'),
asset('image.s.webp', 'published/s.webp', 'image', 'image/webp'),
asset('image.m.avif', 'published/m.avif', 'image', 'image/avif'),
{
...asset('markdown', 'published/body.html', 'piece', 'text/html'),
content: '<h1>Body</h1>'
},
{
...asset('markdown.description', null, 'piece', 'text/html'),
content: '<p>Description</p>'
},
{
...asset('markdown.metadata', null, 'piece', 'application/json'),
content: '{"title":"Recorded title","count":42}'
}
]
}
const html = render(Overview, {
props: { data: props, params: { path: props.file } }
}).body.replace(/<!--[^]*?-->/g, '')
expect([...html.matchAll(/<caption[^>]*>([^<]*)<\/caption>/g)].map((match) => match[1])).toEqual([
'Publication',
'Image',
'Markdown'
])
for (const output of [
'image.original',
'image.s.webp',
'image.m.avif',
'markdown',
'markdown.description',
'markdown.metadata'
])
expect(html).toContain(`${output}</bdi>`)
expect(html.match(/scope="col"[^>]*>Output<\/th>/g)).toHaveLength(2)
for (const path of [
'published/original.jpg',
'published/s.webp',
'published/m.avif',
'published/body.html'
])
expect(html).toContain(`href="/cdn/pieces/assets/${path}"`)
expect(html).not.toMatch(/ download(?:[= >])/)
expect(html.match(/<button /g)).toHaveLength(3)
expect(html).toContain('aria-label="Show Markdown.description: Asset 2, Content"')
expect(html).toContain('aria-label="Show Markdown.metadata: Asset 2, Content"')
expect(html).not.toContain('{"title":"Recorded title","count":42}')
expect(html).not.toContain('title</bdi>')
expect(html).not.toContain('count</bdi>')
for (const excluded of [
'Markdown values</caption>',
'Body</h1>',
'Description</p>',
'>Variant<',
'style=',
'None recorded'
])
expect(html).not.toContain(excluded)
})
test.each(['/static', 'https://cdn.test/site-assets', ''])(
'Published reset layout renders the resolved default favicon for original asset base %j',
(base) => {
const props = data()
props.config = makePublicConfig({
url: { ...props.config.url, app: 'https://public.test/garden', app_assets: base }
})
expect(props.config.content.images).toEqual({
favicon: `${base}/images/favicon.png`,
opengraph: new URL(`${base}/images/opengraph.png`, 'https://public.test').href
})
// Defaults are already in the payload, not recalculated by the consumer.
props.config.url.app_assets = 'https://unrelated-cdn.test/assets'
const result = render(PublishedTree, { props: { data: props } })
expect(result.head).toContain(`rel="icon" href="${base}/images/favicon.png"`)
expect(result.head.match(/rel="icon"/g)).toHaveLength(1)
expect(result.head).toContain('Published: Published &lt;Book>')
expect(result.head).toContain('name="robots" content="noindex"')
expect(result.head).not.toContain('property="og:')
expect(result.head).not.toContain('name="description"')
expect(result.head).not.toContain('/cdn/images/favicon.png')
expect(result.body.match(/<main\b/g)).toHaveLength(1)
}
)
test.each([false, true])(
'Published reset layout consumes resolved favicon selection %s',
(selected) => {
const props = data()
props.config.url.app_assets = 'https://unrelated-cdn.test/assets'
props.config.url.app = 'https://public.test'
const favicon = selected ? '/assets/favicon/selected-icon.svg' : '/static/images/favicon.png'
const opengraph = selected
? 'https://public.test/assets/opengraph/selected-card.png'
: 'https://public.test/static/images/opengraph.png'
props.config.content.images = { favicon, opengraph }
const result = render(PublishedTree, { props: { data: props, view: 'share' } })
expect(result.head).toContain(`rel="icon" href="${props.config.content.images.favicon}"`)
expect(result.head.match(/rel="icon"/g)).toHaveLength(1)
expect(result.head).not.toContain('property="og:')
expect(result.head).not.toContain('assets/opengraph')
expect(result.body).toContain('data-renderer="composition"')
}
)
test('reset layout keeps its inspector title/noindex and adds only the favicon, not public SEO', () => {
const result = render(PublishedTree, { props: { data: data() } })
expect(result.head).toContain('Published: Published &lt;Book>')
expect(result.head).toContain('name="robots" content="noindex"')
expect(result.head).not.toContain('og:')
expect(result.body).toContain('Back to editor')
expect(result.body).toContain(`href="${editorHref(data().file)}"`)
expect(result.body).toContain(`href="${publishedHref(data().file)}" aria-current="page"`)
expect(result.body.match(/aria-current="page"/g)).toHaveLength(1)
expect(result.body).not.toMatch(/<th[^>]*>Source<\/th>/)
expect(result.body.match(/<main /g)).toHaveLength(1)
})
test.each(['page', 'icon', 'share'] as const)(
'Published %s delegates serving data and uses published assets without nested main',
(view) => {
page.url = new URL(`${publishedHref(data().file)}/${view}`, 'https://app.test')
page.route.id = `/(auth)/admin/piece/[...path]/published/${view}`
const result = render(PublishedTree, { props: { data: data(), view } })
expect(result.body).toContain(
`href="${publishedHref(data().file)}/${view}" aria-current="page"`
)
expect(result.body.match(/aria-current="page"/g)).toHaveLength(1)
expect(result.body.match(/<main /g)).toHaveLength(1)
expect(result.body).toContain('src="/cdn/pieces/assets/published/cover.jpg"')
expect(result.body).not.toContain('/admin/asset')
expect(result.body).not.toContain('/admin/preview')
if (view === 'page') {
expect(result.body).toContain('data-renderer="page"')
expect(result.body).toContain('href="/tags/fiction"')
expect(result.body).toContain('Fiction')
expect(resolve).toHaveBeenCalledWith('/tags/[tag]', { tag: 'fiction' })
}
if (view === 'icon') {
expect(result.body).toContain('aria-label="Piece icon"')
expect(result.body).toContain('data-width="500"')
expect(result.body).toContain('loading="eager"')
}
if (view === 'share') {
expect(result.body).toContain('aria-label="Share"')
expect(result.body).toContain('data-renderer="composition"')
expect(result.body).not.toContain('Rendered share composition')
expect(result.body).not.toContain('not the generated PNG')
expect(result.body).not.toContain('Open generated share image')
}
}
)
+18 -2
View File
@@ -25,8 +25,24 @@ pnpm lint
pnpm build
```
Keep coverage from dropping. If you change Docker or publishing, exercise that
path too. A passing unit test is not a running container.
If you change Docker or publishing, exercise that path too.
A passing unit test is not a running container.
## Testing
Tests should protect plausible application-owned regressions. Name what could
break and why it matters before adding or repairing a test. Remove low-value
tests without replacement; test counts and coverage percentages are not goals.
Use the cheapest valid proof. Each layer should protect a distinct boundary,
not repeat an existing matrix. Keep real database, HTTP or browser tests where
mocks cannot prove the behavior. Test our decisions and integration; trust
established dependencies to do their jobs.
Write scenarios in setup, action and outcome order, using named facts and simple
fixtures. String assertions and small functions can protect important contracts;
cosmetic inventories, private implementation details and hypothetical failures
usually do not.
## Keep it boring
+1 -1
View File
@@ -1,3 +1,3 @@
[tools]
node = "26.4.0"
node = "26.5.0"
pnpm = "11.2.2"