Files
cms-website/themes/pilot-minimal/test/load-data.test.ts
T
浅念 eb81a57ebc fix(theme): payload 契约上提 contracts;空路由明错;ui.routeHrefFor;eslint astro ignore
审查修复(Plan 4 Task 3 review):
- 载荷类型单一事实来源:ContentEntry/TaxonomyEntry/TaxonomyTerm/RouteEntry/
  AssetEntry/RenderManifestMeta/SiteConfigPayload/BrandConfigPayload 上提
  @sf/contracts/render-snapshot(纯新增、不动既有导出——冻结面加法无需升版);
  物化端(apps/api build/render-snapshot.ts)与主题 load-render-data 同源引用,
  双份手写镜像漂移即编译期破裂;主题侧 re-export 保持既有 import 面
- loader 空路由表前置明错(正常发布站点至少含首页路由——静默渲染会产零页站点)
- ui.routeHrefFor:targetContentId → 纯路径 html 落点(redirect/查询行不参与,
  无落点 undefined 不虚构——Task 4 分派器入口)+ 测试
- 具夹 assets.json copyPath 去机器相关绝对路径(改仓库相对)
- eslint ignores 增 **/.astro/**(astro build 再生目录)

354 根测试 + 18 主题测试全绿;五段 typecheck;lint(.astro 存在态验证后清理)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 08:58:33 +08:00

168 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Task 3 用例 2/4:load-render-data 对六文件夹具的加载形状、缺文件明错、name 兜底、
// 缓存单例、编码 slug 原样贯穿(前置事实 7——不得 decodeURIComponent)。
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { loadRenderData, resetRenderDataCache } from '../src/lib/load-render-data.js'
import { routeHrefFor } from '../src/lib/ui.js'
const FIXTURE = fileURLToPath(new URL('./fixtures/render-input', import.meta.url))
const LOADER_SRC = fileURLToPath(new URL('../src/lib/load-render-data.ts', import.meta.url))
const PREV_ENV = process.env.SF_BUILD_INPUT
afterEach(() => {
resetRenderDataCache()
if (PREV_ENV === undefined) delete process.env.SF_BUILD_INPUT
else process.env.SF_BUILD_INPUT = PREV_ENV
})
/** 具夹目录的临时副本(变体测试可随意改写,用后即删) */
function tempCopy(name: string): string {
const dir = mkdtempSync(join(tmpdir(), `sf-theme-${name}-`))
cpSync(FIXTURE, dir, { recursive: true })
return dir
}
describe('load-render-data(渲染数据唯一入口)', () => {
it('六文件具夹 → 返回形状:site/contents/taxonomies/routes/assets Map/renderManifest', () => {
const data = loadRenderData(FIXTURE)
// 占位 name(siteKey 派生)原样贯穿——兜底只对空值生效
expect(data.site.name).toBe('demo-site')
expect(data.site.origin).toBe('https://demo.example')
expect(data.site.locale).toBe('zh-CN')
expect(data.site.brand).toStrictEqual({})
expect(data.contents).toHaveLength(3)
const news = data.contents.find((c) => c.slug === 'fixture-news-a')
expect(news?.typeKey).toBe('news_article')
expect(news?.data.bodyHtml).toContain('src="/wp-content/uploads/2026/10/fixture-a.png"')
expect(news?.media?.featured).toBe('/wp-content/uploads/2026/10/fixture-a.png')
expect(data.taxonomies[0]).toMatchObject({ key: 'product-category', objectType: 'product' })
expect(data.taxonomies[0]?.terms[0]?.contentEntryIds).toHaveLength(1)
const redirect = data.routes.find((r) => r.routeKind === 'redirect')
expect(redirect).toMatchObject({
routePath: '/old-path/',
redirectStatus: 301,
redirectLocation: '/',
indexable: false,
})
expect(data.assets.size).toBe(1)
expect(data.assets.get('/wp-content/uploads/2026/10/fixture-a.png')).toMatchObject({
detectedMimeType: 'image/png',
byteSize: 1234,
width: 100,
height: 80,
altText: null,
})
expect(data.renderManifest.counts).toStrictEqual({ contents: 3, taxonomies: 1, routes: 7, assets: 1 })
expect(data.renderManifest.renderSnapshotVersion).toBe(1)
})
it("site.name 空 → 'Untitled Site';brand 键缺席 → {}(Task 1 审查移交:缺省容忍)", () => {
const dir = tempCopy('name')
try {
writeFileSync(
join(dir, 'site.json'),
JSON.stringify({ name: '', origin: 'https://demo.example', locale: 'zh-CN' }),
)
const data = loadRenderData(dir)
expect(data.site.name).toBe('Untitled Site')
expect(data.site.brand).toStrictEqual({})
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('缺文件 → 明确错误(不静默空)', () => {
const dir = tempCopy('missing')
try {
rmSync(join(dir, 'routes.json'))
expect(() => loadRenderData(dir)).toThrow(/routes\.json/)
expect(() => loadRenderData(dir)).toThrow(/缺失|不完整/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('SF_BUILD_INPUT 未设置且未传目录 → 明确错误', () => {
delete process.env.SF_BUILD_INPUT
expect(() => loadRenderData()).toThrow(/SF_BUILD_INPUT/)
})
it('缺省读 SF_BUILD_INPUT 环境变量目录', () => {
process.env.SF_BUILD_INPUT = FIXTURE
expect(loadRenderData().site.name).toBe('demo-site')
})
it('缓存单例:同目录多次调用只读一次盘(引用同一 + 落盘变化不可见)', () => {
const dir = tempCopy('cache')
try {
const first = loadRenderData(dir)
writeFileSync(
join(dir, 'site.json'),
JSON.stringify({ name: 'changed-after-load', origin: 'https://demo.example', locale: 'zh-CN', brand: {} }),
)
const second = loadRenderData(dir)
expect(second).toBe(first)
expect(second.site.name).toBe('demo-site') // 未重读盘
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('编码 slug 原样贯穿:字节不变、路由编码路径原样(前置事实 7)', () => {
const data = loadRenderData(FIXTURE)
const page = data.contents.find((c) => c.typeKey === 'basic_page')
expect(page?.slug).toBe('%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac')
expect(decodeURIComponent(page!.slug)).not.toBe(page!.slug) // 夹具确为编码形态
expect(data.routes.some((r) => r.routePath === '/%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac/')).toBe(true)
// 结构保证:loader 源码不得引入 decodeURIComponent
expect(readFileSync(LOADER_SRC, 'utf8')).not.toContain('decodeURIComponent')
})
it('content.json 顶层非数组 → 明确错误(不静默空)', () => {
const dir = tempCopy('shape')
try {
writeFileSync(join(dir, 'content.json'), JSON.stringify({ oops: true }))
expect(() => loadRenderData(dir)).toThrow(/content\.json/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('routes.json 空表 → 明确错误(正常发布站点至少含首页路由)', () => {
const dir = tempCopy('noroutes')
try {
writeFileSync(join(dir, 'routes.json'), JSON.stringify([]))
expect(() => loadRenderData(dir)).toThrow(/路由表为空/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('routeHrefFor:targetContentId → 落点路由;无落点 undefined;redirect/查询行不参与', () => {
const data = loadRenderData(FIXTURE)
expect(routeHrefFor(data.routes, '33333333-3333-4333-8333-333333333333')).toBe('/news/fixture-news-a.html')
expect(routeHrefFor(data.routes, '44444444-4444-4444-8444-444444444444')).toBe('/product/fixture-product/')
// 无落点(含 redirect 行即便带 targetContentId 也不算内容页公开落点)
expect(routeHrefFor(data.routes, '00000000-0000-4000-8000-000000000000')).toBeUndefined()
const redirectWithTarget: typeof data.routes[number] = {
...data.routes.find((r) => r.routeKind === 'redirect')!,
targetContentId: '33333333-3333-4333-8333-333333333333',
}
expect(routeHrefFor([redirectWithTarget], '33333333-3333-4333-8333-333333333333')).toBeUndefined()
// 带 query_key 的 html 行不是内容页纯路径落点
const queryRoute: typeof data.routes[number] = {
...data.routes.find((r) => r.routePath === '/news/fixture-news-a.html')!,
queryKey: 'attachment_id',
}
expect(routeHrefFor([queryRoute], '33333333-3333-4333-8333-333333333333')).toBeUndefined()
})
})