Files
浅念 1cd309d56d feat(theme): pilot-minimal 骨架与布局组件
Plan 4 Task 3:
- themes/pilot-minimal 入 workspace(astro ^5.18 纯静态;依赖仅 astro)
- theme.manifest:contracts isThemeManifest 守卫通过——key=pilot-minimal/
  七模板条目(home/page/product-list/product-detail/news-list/news-detail/
  not-found);brand 刻意不入 requiredSiteConfig(恒可能 {},Task 1 移交契约)
- load-render-data:全主题唯一取数入口(五禁纪律结构保证,JSDoc 钉死)——
  SF_BUILD_INPUT 六文件、缺文件/形状破裂明确错误不静默空、同目录缓存单例、
  name 空→'Untitled Site'/brand 缺席→{}、编码 slug 原样贯穿(源码令牌扫描
  禁 percent-decode)
- 布局/组件:BaseLayout/SeoHead(title/desc/canonical/og)/SiteHeader(导航
  路由缺席容忍隐藏)/SiteFooter/RichText(信任链:清洗+重写后直出)/
  ResponsiveMedia/Breadcrumbs/ProductCard/NewsCard/Pagination(V1 禁用态)/
  NotFound + tokens.css(75rem/系统字体/4-8-16-24-48/中性色+单 accent)
- 根 test 链并入主题(vitest 3 + astro getViteConfig,Container API 组件
  渲染测试);typecheck 四段变五段;.astro/ 入 gitignore

354 根测试 + 16 主题测试全绿。

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

90 lines
3.4 KiB
TypeScript
Raw Permalink 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 用例 3:brand 容忍——brand:{} 与 brand:{logoText:'X'} 两态下 Header/Footer
// 均渲染不落空(logoText 缺省用 name);导航缺席容忍(routes 无对应路径 → 隐藏项)。
// 经 Astro Container API 做真实组件渲染(非仅纯函数断言)。
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { describe, expect, it } from 'vitest'
import SiteFooter from '../src/components/SiteFooter.astro'
import SiteHeader from '../src/components/SiteHeader.astro'
import type { RouteEntry, SiteConfig } from '../src/lib/load-render-data.js'
const htmlRoute = (routePath: string): RouteEntry => ({
host: '',
routePath,
routeKind: 'html',
queryPolicy: 'ignore',
queryKey: null,
targetStatus: '200',
targetContentId: null,
targetFile: null,
redirectStatus: null,
redirectLocation: null,
indexable: true,
priority: 1000,
verificationStatus: 'verified_html',
})
const ROUTES: RouteEntry[] = [htmlRoute('/'), htmlRoute('/products/'), htmlRoute('/news/')]
const SITE_EMPTY_BRAND: SiteConfig = {
name: 'demo-site',
origin: 'https://demo.example',
locale: 'zh-CN',
brand: {},
}
const SITE_LOGO_TEXT: SiteConfig = {
...SITE_EMPTY_BRAND,
brand: { logoText: 'ACME 商标' },
}
type Container = Awaited<ReturnType<typeof AstroContainer.create>>
let container: Container | undefined
async function getContainer(): Promise<Container> {
container ??= await AstroContainer.create()
return container
}
describe('brand 容忍(Task 1 审查移交契约)', () => {
it('brand:{} → Header/Footer 渲染 site.name(logoText 缺省用 name),无 undefined 落空', async () => {
const c = await getContainer()
const header = await c.renderToString(SiteHeader, {
props: { site: SITE_EMPTY_BRAND, routes: ROUTES },
})
expect(header).toContain('demo-site')
expect(header).not.toContain('undefined')
const footer = await c.renderToString(SiteFooter, { props: { site: SITE_EMPTY_BRAND } })
expect(footer).toContain('demo-site')
expect(footer).toContain(`© ${new Date().getFullYear()}`)
expect(footer).not.toContain('undefined')
})
it("brand:{logoText:'X'} → Header/Footer 显示 logoText", async () => {
const c = await getContainer()
const header = await c.renderToString(SiteHeader, {
props: { site: SITE_LOGO_TEXT, routes: ROUTES },
})
expect(header).toContain('ACME 商标')
const footer = await c.renderToString(SiteFooter, { props: { site: SITE_LOGO_TEXT } })
expect(footer).toContain('ACME 商标')
})
it('brand 对象上无 logoText 键之外的杂键也不炸(恒按缺省容忍读)', async () => {
const c = await getContainer()
const site: SiteConfig = { ...SITE_EMPTY_BRAND, brand: { unknownKey: 'v' } as SiteConfig['brand'] }
const header = await c.renderToString(SiteHeader, { props: { site, routes: ROUTES } })
expect(header).toContain('demo-site')
expect(header).not.toContain('undefined')
})
it('导航缺席容忍:routes 无 /products/ → 隐藏对应项,/news/ 存在则保留', async () => {
const c = await getContainer()
const partial = ROUTES.filter((r) => r.routePath !== '/products/')
const header = await c.renderToString(SiteHeader, {
props: { site: SITE_EMPTY_BRAND, routes: partial },
})
expect(header).not.toContain('href="/products/"')
expect(header).toContain('href="/news/"')
expect(header).toContain('href="/"')
})
})