138 lines
5.5 KiB
TypeScript
138 lines
5.5 KiB
TypeScript
|
|
// 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'
|
|||
|
|
|
|||
|
|
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 })
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|