Files
WechatExplorer/tests/unit/report-hero-avatar-geometry.test.ts
Wxw-Gu b97e1f4bc6 fix: 问问微信恢复引用编号,日报修复成员头像与 hero 溢出
- 问问微信:Host 分配稳定 citationId 并注入模型上下文,正文 [E#] 与证据按钮同号
- 问问微信:回答返回前校验引用,幻觉编号移除并提示,不再出现不可信的引用按钮
- Agent Hub:收紧「最近会话」快捷路由,内容查询交回 Query Agent
2026-09-17 18:56:32 +08:00

103 lines
4.9 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.
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { describe, expect, it } from 'vitest'
import { REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS } from '../../src/shared/report-template-fragment-contract'
/**
* hero 头像的几何只有一份权威。
*
* 生产导出会注入 `report-template-fragment-contract.ts`,其中
* `img.tm-avatar.tm-avatar--hero` 用 `!important` 把头像钉在
* `clamp(28px, var(--tm-avatar-hero-size, 40px), 56px)`。
* 如果模板再给容器写死一个尺寸(旧版 `.avatar-grid` 是 58x58、单头像 28x28),
* 两边就会打架:头像实际 40px,2 列 x 40px + 3px gap = 83px 塞不进 58px 的盒子,
* 头像向右向下溢出容器,视觉上越过卡片内边距 —— 这就是"日报头像超出范围"。
*
* 这里断言的是**不变量**,不是措辞:
* 1. 容器尺寸必须来自 contract 的变量,不能写死;
* 2. 容器的最小可用宽度必须能容纳 2 列头像(由变量算出来,而不是靠猜);
* 3. contract 提供的变量在容器里被真正引用(改名/改值不会静默分叉)。
*/
const HERO_AVATAR_GAP_PX = 3
const readResource = (file: string): string => readFileSync(resolve('resources', file), 'utf8')
/** 取出某个选择器的声明块(第一个匹配),返回声明文本。 */
const ruleBody = (css: string, selector: string): string => {
const index = css.indexOf(selector)
if (index < 0) throw new Error(`selector not found: ${selector}`)
const open = css.indexOf('{', index)
const close = css.indexOf('}', open)
if (open < 0 || close < 0) throw new Error(`unterminated rule: ${selector}`)
return css.slice(open + 1, close)
}
/** contract 里 hero 头像的目标尺寸与变量名。 */
const contractHeroAvatar = (): { variable: string; fallbackPx: number } => {
const name = /--tm-avatar-hero-size:\s*([\d.]+)px/.exec(REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS)
expect(name, 'contract 必须声明 --tm-avatar-hero-size').not.toBeNull()
const forced =
/img\.tm-avatar\.tm-avatar--hero\s*\{[\s\S]*?var\((--tm-avatar-hero-size),\s*([\d.]+)px\)/.exec(
REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS
)
expect(forced, 'contract 必须用变量 + px 兜底约束 hero 头像').not.toBeNull()
return { variable: forced![1], fallbackPx: Number(forced![2] || name![1]) }
}
const LEGACY_TEMPLATES = ['mobile_daily_report_v1.html', 'mobile_daily_report_v2.html']
describe('日报 hero 头像几何 — 容器与 contract 不得分叉', () => {
it('contract 通过变量约束 hero 头像尺寸(含 px 兜底)', () => {
const { variable, fallbackPx } = contractHeroAvatar()
expect(variable).toBe('--tm-avatar-hero-size')
expect(fallbackPx).toBeGreaterThanOrEqual(28)
expect(fallbackPx).toBeLessThanOrEqual(56)
// 必须带 !important,否则模板的 width/height 会盖掉它,几何又会分裂
const heroRule = /img\.tm-avatar\.tm-avatar--hero\s*\{[\s\S]*?\}/.exec(
REPORT_TEMPLATE_FRAGMENT_CONTRACT_CSS
)![0]
expect(heroRule).toContain('!important')
})
for (const file of LEGACY_TEMPLATES) {
describe(file, () => {
const css = readResource(file)
it('avatar-grid 不再写死容器尺寸(写死就会小于头像本身)', () => {
const body = ruleBody(css, '.avatar-grid {')
expect(body, '容器写死 width 会让 40px 头像溢出').not.toMatch(/(^|\s)width\s*:/)
expect(body, '容器写死 height 会让第二行头像纵向溢出').not.toMatch(/(^|\s)height\s*:/)
})
it('列宽与行高都取 contract 的同一个变量', () => {
const { variable, fallbackPx } = contractHeroAvatar()
const body = ruleBody(css, '.avatar-grid {')
expect(body).toContain('grid-template-columns')
expect(body).toContain('grid-auto-rows')
// 变量名与兜底值都必须和 contract 一致 —— 否则主题化会分叉
expect(body).toContain(`var(${variable}, ${fallbackPx}px)`)
})
it('单头像时列宽等于一个头像,不再用 28px 的窄盒子', () => {
const { variable, fallbackPx } = contractHeroAvatar()
const body = ruleBody(css, '.avatar-grid.avatar-count-1')
expect(body).toContain('grid-template-columns')
expect(body).toContain(`var(${variable}, ${fallbackPx}px)`)
expect(body).not.toMatch(/(^|\s)width\s*:/)
expect(body).not.toMatch(/(^|\s)height\s*:/)
})
it('按 contract 尺寸算出的两列簇宽不会被任何固定宽度切断', () => {
const { fallbackPx } = contractHeroAvatar()
const clusterWidth = fallbackPx * 2 + HERO_AVATAR_GAP_PX
const body = ruleBody(css, '.avatar-grid {')
const fixed = /(^|\s)width\s*:\s*(\d+(?:\.\d+)?)px/.exec(body)
// 既没有固定宽度,也不存在比簇宽更小的固定宽度
if (fixed) expect(Number(fixed[2])).toBeGreaterThanOrEqual(clusterWidth)
expect(clusterWidth).toBeLessThanOrEqual(430 - 2 * 12 - 2 * 20 - 14)
})
})
}
})