f916f723ab
单一动态路由 [...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>
173 lines
8.9 KiB
TypeScript
173 lines
8.9 KiB
TypeScript
// 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.')
|
||
})
|
||
})
|