Files
cms-website/themes/pilot-minimal/test/render-cases.test.ts
T
浅念 f916f723ab feat(theme): 六类页面渲染与路由分派
单一动态路由 [...slug].astro 读 routes.json html 纯路径行分派六模板
(home/typeKey 详情/分类前缀列表/news 主列表),无法分派即构建明错;
404 独立静态页。夹具扩展为六页集(真实 hesupipe 抽样形状脱敏化:
knownFields.postDate/title、media 块 attachmentRefs.* fieldPath、
/products/ 内容页占据、编码路径 + .html 字面路径防御)。

字面 .html 路径对齐钩子(astro.config 内联 integration):Astro 页面
产物在 directory/file/preserve 三态均产不出 dist/news/x.html 字面文件,
构建收尾把包装目录折平为字面文件——target_file 契约对齐。

构建级测试 8 例(render-cases):全量映射/编码字节原样/首页截断/
产品参数相册/新闻字面 .html/列表两类/SEO 基线。372 → 380。

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

173 lines
8.9 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 4 构建级用例:六页夹具(真实 hesupipe 抽样形状脱敏化)→ astro build 真跑 → dist/ 断言。
// 与 Container API 组件测试(components.test.ts)分层:此处验证的是路由分派 + 模板组装 +
// 产物映射(尾斜杠→<path>/index.html、.html 后缀→字面文件、百分号编码路径→编码原样目录)
// 的整链,不只是单组件渲染。
import { execFileSync } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { RouteEntry } from '../src/lib/load-render-data.js'
const THEME_ROOT = fileURLToPath(new URL('..', import.meta.url))
const FIXTURE = fileURLToPath(new URL('./fixtures/render-input', import.meta.url))
const ASTRO_ENTRY = fileURLToPath(new URL('../node_modules/astro/astro.js', import.meta.url))
const ORIGIN = 'https://demo.example'
const ENCODED = '%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac'
/** 路由 → dist 相对文件路径(Task 5 管线同款映射规则:target_file 形态是唯一权威) */
function distRel(routePath: string): string {
if (routePath === '/') return 'index.html'
const rel = routePath.replace(/^\//, '').replace(/\/+$/, '')
return rel.endsWith('.html') ? rel : `${rel}/index.html`
}
let dist = ''
function page(routePath: string): string {
return readFileSync(join(dist, distRel(routePath)), 'utf8')
}
beforeAll(() => {
dist = mkdtempSync(join(tmpdir(), 'sf-render-cases-'))
execFileSync(process.execPath, [ASTRO_ENTRY, 'build', '--outDir', dist], {
cwd: THEME_ROOT,
env: { ...process.env, SF_BUILD_INPUT: FIXTURE },
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
})
}, 180_000)
afterAll(() => {
rmSync(dist, { recursive: true, force: true })
})
describe('六类页面构建(astro build × render-input 夹具)', () => {
it('全量落盘映射:html 纯路径行 → <path>/index.html(尾斜杠)或字面 .html 文件;redirect 不产页;404.html 存在', () => {
const routes = JSON.parse(
readFileSync(join(FIXTURE, 'routes.json'), 'utf8'),
) as RouteEntry[]
const htmlRoutes = routes.filter((r) => r.routeKind === 'html' && r.queryKey === null)
expect(htmlRoutes).toHaveLength(14)
for (const r of htmlRoutes) {
const rel = distRel(r.routePath)
const f = join(dist, rel)
// isFile 而非 exists:.html 字面路径若被 Astro 折成同名目录,existsSync 仍真——形态必须钉死
expect(statSync(f).isFile(), `${r.routePath} → ${rel}(须为文件)`).toBe(true)
}
// redirect 行不产页(301 的处置在 preview/route-manifest 域,不在静态产物域)
expect(existsSync(join(dist, 'old-path'))).toBe(false)
expect(existsSync(join(dist, 'old-path', 'index.html'))).toBe(false)
// 404 独立静态页(Astro 特例 → 根级 404.html)
expect(statSync(join(dist, '404.html')).isFile()).toBe(true)
expect(page('/')).toContain('demo-site')
})
it(`编码路径目录原样落盘:dist/${ENCODED}/ 字节不变(前置事实 7——禁 percent-decode)`, () => {
const roots = readdirSync(dist)
expect(roots).toContain(ENCODED)
expect(roots).not.toContain('关于我们') // 无解码形态孪生目录
const html = page(`/${ENCODED}/`)
expect(html).toContain('<title>关于我们</title>')
expect(html).toContain('关于我们页面正文。')
// canonical 拼接保持编码形态(canonicalUrl 只做 origin + path 原样连接)
expect(html).toContain(`href="${ORIGIN}/${ENCODED}/"`)
})
it('首页:site name hero + 最新 3 新闻卡 + 最新 3 产品卡(第 4 条最旧被截断)+ 内链 routeHrefFor + 图片根相对', () => {
const html = page('/')
expect(html.match(/<h1[\s>]/g)).toHaveLength(1) // 恰一 h1(门禁③纪律)
expect(html).toContain('demo-site')
// 最新 3 新闻(postDate 降序:a 2026-10-01 / b 09-20 / c 08-15;d 07-01 被截断)
expect(html).toContain('Fixture News A')
expect(html).toContain('Fixture News B')
expect(html).toContain('Fixture News C')
expect(html).not.toContain('Fixture News D')
// 最新 3 产品(old 2025-12-01 被截断)
expect(html).toContain('Fixture Product')
expect(html).toContain('Fixture Product B')
expect(html).toContain('Fixture Product C')
expect(html).not.toContain('Fixture Product Old')
// 内链用路由表真值(routeHrefFor——产品卡可点到详情页)
expect(html).toContain('href="/product/fixture-product/"')
expect(html).toContain('href="/fixture-news-b/"')
// 封面图 src = media 块引用的 public_path(根相对原样)
expect(html).toContain('src="/wp-content/uploads/2026/10/fixture-a.png"')
})
it('产品详情:h1 标题 + 参数表键值 + 相册三图(featured + gallery[0..1] 根相对)+ 正文;extras.phpRaw 不渲染', () => {
const html = page('/product/fixture-product/')
expect(html).toContain('Fixture Product')
expect(html).toContain('产品说明。')
// 参数表(data.specifications 纯对象转表)
expect(html).toContain('specifications')
expect(html).toContain('材质')
expect(html).toContain('不锈钢')
expect(html).toContain('DN400')
// 相册:featured 首图 + gallery 按序号,src 根相对
expect(html).toContain('src="/wp-content/uploads/2026/10/fixture-a.png"')
expect(html).toContain('src="/wp-content/uploads/2026/10/fixture-b.jpg"')
expect(html).toContain('src="/wp-content/uploads/2026/10/fixture-c.jpg"')
// extras.phpRaw 明确忽略
expect(html).not.toContain('a:1:{')
})
it('新闻详情(.html 字面路径):dist/news/fixture-news-a.html 是文件而非目录;日期/正文/根相对图片/canonical', () => {
const f = join(dist, 'news', 'fixture-news-a.html')
expect(statSync(f).isFile()).toBe(true)
const html = readFileSync(f, 'utf8')
expect(html).toContain('Fixture News A')
expect(html).toMatch(/<time[^>]*datetime="2026-10-01 09:00:00"/)
expect(html).toContain('正文引用图片')
expect(html).toContain('src="/wp-content/uploads/2026/10/fixture-a.png"')
expect(html).toContain(`href="${ORIGIN}/news/fixture-news-a.html"`)
})
it('新闻列表 /news/:News 标题 + 全部 4 条新闻卡(分页 V1 不做——单页全列)', () => {
const html = page('/news/')
expect(html).toContain('News')
expect(html).toContain('href="/news/fixture-news-a.html"')
expect(html).toContain('href="/fixture-news-b/"')
expect(html).toContain('Fixture News C')
expect(html).toContain('Fixture News D') // 列表全列,不截断
})
it('分类列表 /product-category/pipe-fittings/:面包屑(首页 › 当前分类)+ term 名标题 + 4 产品卡可点', () => {
const html = page('/product-category/pipe-fittings/')
expect(html).toContain('aria-label="面包屑"')
expect(html).toContain('href="/"')
expect(html).toContain('aria-current="page"')
expect(html).toContain('Pipe Fittings')
// term.contentEntryIds 反查 contents——4 产品全卡片(含首页被截断的 old)
expect(html).toContain('href="/product/fixture-product/"')
expect(html).toContain('href="/product/fixture-product-b/"')
expect(html).toContain('href="/product/fixture-product-c/"')
expect(html).toContain('href="/product/fixture-product-old/"')
expect(html).toContain('Fixture Product Old')
})
it('SEO 基线:六页集 + /products/ 全部有 title / meta description / canonical(origin+自身路径)', () => {
const cases: Array<[string, string, string]> = [
['/', 'demo-site', `${ORIGIN}/`],
['/about-us/', 'About Us', `${ORIGIN}/about-us/`],
[`/${ENCODED}/`, '关于我们', `${ORIGIN}/${ENCODED}/`],
['/product-category/pipe-fittings/', 'Pipe Fittings', `${ORIGIN}/product-category/pipe-fittings/`],
['/product/fixture-product/', 'Fixture Product', `${ORIGIN}/product/fixture-product/`],
['/news/', 'News', `${ORIGIN}/news/`],
['/news/fixture-news-a.html', 'Fixture News A', `${ORIGIN}/news/fixture-news-a.html`],
// 前置事实:/products/ 被 WP "Products" 内容页占据——落 GenericPage(非列表壳)
['/products/', 'Products', `${ORIGIN}/products/`],
]
for (const [routePath, title, canonical] of cases) {
const html = page(routePath)
expect(html, routePath).toContain(`<title>${title}</title>`)
expect(html, routePath).toMatch(/<meta name="description" content="[^"]+"/)
expect(html, routePath).toContain(`<link rel="canonical" href="${canonical}">`)
}
// 内容页 description 优先取修订 seo.description(yoast 面)
expect(page('/about-us/')).toContain('About us fixture page description.')
expect(page('/news/fixture-news-a.html')).toContain('Fixture news A meta description.')
})
})