mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-03 18:33:14 +08:00
- 问问微信:Host 分配稳定 citationId 并注入模型上下文,正文 [E#] 与证据按钮同号 - 问问微信:回答返回前校验引用,幻觉编号移除并提示,不再出现不可信的引用按钮 - Agent Hub:收紧「最近会话」快捷路由,内容查询交回 Query Agent
103 lines
4.9 KiB
TypeScript
103 lines
4.9 KiB
TypeScript
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)
|
||
})
|
||
})
|
||
}
|
||
})
|