类名守门补上条件类与精确 selector 匹配,堵掉两处会静默放行的漏洞
这条守门是为「#193 把策划选择器整段误删」设的,但它自己有两处会放行:
一、条件类进不了清单。抽取时 `.replace(/\$\{[^}]*\}/g, ' ')` 把插值整段丢掉,而两个
组件里唯一的条件类恰好长在插值里:``plan-gdd-surface${showCard ? ' …--with-card' : ''}``。
清单 25 个类里没有 `plan-gdd-surface--with-card`。它不是装饰性的:
`.game-workbench-chat .plan-gdd-surface--with-card` 的 `grid-template-rows: auto
minmax(0, 1fr)` 是审批卡内滚的边界,策划面自己是 `display: grid` + `overflow: hidden`
的外壳,删掉这条,批准后的交付行连同路径和两个按钮会被切在壳外。实测把该类的两条
规则整段删掉,改前的测试照样绿。
改成只取插值里的字符串字面量——三元的条件和变量名不是类名,不能混进清单。清单
25 → 26,新进的正是 `plan-gdd-surface--with-card`,没有杂质。
二、`styles.includes('.' + name)` 是子串匹配,`.gdd-approval-card__header` 可以冒充
`.gdd-approval-card` 的存在证据。改成要求类名后面不再跟类名字符。当前 26 个类逐一
校验都有精确 selector,所以这一处堵的是潜在放行,不是正在掩盖的缺失。
另按评审要求补一条负向钉子:单独钉住上面那条承重的行模板。存在性检查只要 `--with-card`
还剩任意一条规则就绿,而两条里只有它是承重的。
三条负向都已验证非空转:删两条规则 → 红;只删承重那条留下分隔线 → 红;把
`.gdd-approval-card__details` 改名只留派生类 → 红(改前用 includes 会绿)。
评审同时建议做 computed-style/布局断言,这里没做:appSurface 跑 jsdom,没有布局引擎,
且这套 suite 不把 styles.css 挂进 DOM。要真断言得换真实浏览器 runner,是另一件事。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -546,7 +546,18 @@ export function registerPlanGddApprovalTests() {
|
||||
for (const match of componentSources.matchAll(
|
||||
/className=(?:"([^"]+)"|\{`([^`]+)`\})/g,
|
||||
)) {
|
||||
const literal = (match[1] ?? match[2] ?? '').replace(/\$\{[^}]*\}/g, ' ');
|
||||
// 条件类长在插值里:`plan-gdd-surface${showCard ? ' …--with-card' : ''}`。
|
||||
// 把 `${…}` 整段丢掉等于把它们排除在守门之外,而它们恰恰是最容易被顺手删干净
|
||||
// 的一档——`--with-card` 挂着审批卡的行模板,没有它卡片底部会被外壳的
|
||||
// `overflow: hidden` 切掉。只取插值里的字符串字面量:三元的条件、变量名都不是
|
||||
// 类名,不能混进清单。
|
||||
const literal = (match[1] ?? match[2] ?? '').replace(
|
||||
/\$\{([^}]*)\}/g,
|
||||
(_whole, expression: string) =>
|
||||
[...expression.matchAll(/'([^']*)'|"([^"]*)"/g)]
|
||||
.map((piece) => piece[1] ?? piece[2] ?? '')
|
||||
.join(' '),
|
||||
);
|
||||
for (const name of literal.split(/\s+/)) {
|
||||
if (name) {
|
||||
referencedClasses.add(name);
|
||||
@@ -559,9 +570,15 @@ export function registerPlanGddApprovalTests() {
|
||||
'utf8',
|
||||
);
|
||||
for (const name of referencedClasses) {
|
||||
// 子串匹配会把 `.gdd-approval-card__header` 当成 `.gdd-approval-card` 的证据:
|
||||
// 前缀类的规则被删光、只剩派生类时这里照样绿。要求类名后面不能再跟类名字符,
|
||||
// 才是「存在这个类的精确 selector」。
|
||||
const exactSelector = new RegExp(
|
||||
`\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`,
|
||||
);
|
||||
expect(
|
||||
styles.includes(`.${name}`),
|
||||
`styles.css 缺少 .${name} 的规则`,
|
||||
exactSelector.test(styles),
|
||||
`styles.css 缺少 .${name} 的精确 selector`,
|
||||
).toBe(true);
|
||||
}
|
||||
// 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进
|
||||
@@ -586,5 +603,12 @@ export function registerPlanGddApprovalTests() {
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat\s+\.project-supervisor-conversation:has\([^)]*\.planning-lane-runtime-strip[^)]*\)\s*\{[^}]*display:\s*flex/s,
|
||||
);
|
||||
// `--with-card` 有两条规则,上面的存在性检查只要还剩一条就绿。承重的是这一条:
|
||||
// 策划面是 `display: grid` + `overflow: hidden` 的外壳,审批卡的 `max-height`
|
||||
// 和内滚要靠这个行模板才有边界。只删它、留下那条分隔线,表现是批准后的交付行
|
||||
// 连同路径和两个按钮被切在壳外。
|
||||
expect(styles).toMatch(
|
||||
/\.game-workbench-chat\s+\.plan-gdd-surface--with-card\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\)/s,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user