1cd309d56d
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>
90 lines
3.4 KiB
TypeScript
90 lines
3.4 KiB
TypeScript
// 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="/"')
|
||
})
|
||
})
|