Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCardCurrentVersionStyle.test.ts
T
suzmii 2a2e4c1cea 按 review 修:动画在途拦截、样式守卫扩到三面板、窄屏差异显式放行、用例不再依赖 cwd
- 在途拦截补齐生成动画:提交记录的「属于哪张资源」从 draftKey 拆成 resourcePath,
  `resourceCanvasResourceEditSubmissionBlocksResource` 被快速编辑与生成动画共用;
  此前动画这条键对不上、拦不住,重开面板再提交会复用同一 operationId 卡在原生锁上,
  或(改了提示词)直接多出一笔付费生成
- 样式缺口守卫扩到三块面板(动画面板 / 快速编辑面板 / 生图面板),判据改为按
  「选择器分支 + 声明」逐条比对;两个面板的实测缺口一并照抄补上(快速编辑面板的
  `overflow: auto → visible` 与窄屏底栏两条按宿主差异显式登记,不静默漂移)
- 窄屏只搬动画面板那一支:网页端 ≤760px 会把快速编辑浮层也变成整宽底栏,AGC 的
  快速编辑是「居中贴资源卡」的绝对定位,跟随它会压住栏目工具栏与任务侧栏
- 用例路径不再依赖 cwd:新增 tests/repoPath.ts(按 import.meta.url 反推仓库根),
  19 个用例文件的 `resolve(process.cwd(), …)` 统一改用它;顺手修掉
  `HOST_STYLE_SHEETS.filter(existsSync)` 把仓库相对路径交给 cwd 解析的假红
- 清掉两处死代码:不可达的 `quickEditPanel.status === 'generating'` 判据与
  未被使用的 prune 导出
2026-09-21 01:23:35 +08:00

148 lines
6.0 KiB
TypeScript
Raw 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 'node:fs';
import { describe, expect, it } from 'vitest';
import { repoPath } from './repoPath';
/**
* 「当前使用」资源卡的发光档案:声明级断言。
*
* jsdom 不加载样式表、也不做动画,肉眼效果只能由真机确认;这里钉住的是这次改动的实质——
* 外向扩散层的档位(旧值 14% 是描边不是发光)、呼吸的实现方式(伪元素只动 opacity)、
* 选中态提权的形态,以及 `prefers-reduced-motion` 下仍然可见。
*/
function stylesSource() {
return readFileSync(
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
}
/** 取 CSS 源文件里某条规则的声明体。 */
function ruleBody(styles: string, selector: string) {
const match = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, 'su').exec(styles);
expect(match, `${selector} 规则缺失`).not.toBeNull();
return match![1]!;
}
/**
* 取 `box-shadow` 里「外向扩散层」的透明度档位(百分比数字)。
*
* 外向扩散层 = 偏移为 `0 0` 的层(`0 0 0 Npx` 的 ring 或 `0 0 Npx` 的发光),
* 与 `0 6px 18px` 这类带偏移的投影区分开。只在层边界(下一个层以 `0 ` 开头)拆逗号,
* 避免把 `rgb(216 115 66 / 14%)` 里的空格当分隔符。
*/
function outwardGlowAlphas(ruleBodyText: string) {
// 先只取 `box-shadow` 的值(到第一个分号),否则第一层会粘着属性名而认不出来。
const value = /box-shadow:\s*([^;]+);/u.exec(ruleBodyText)?.[1] ?? '';
return value
.split(/,(?=\s*0 )/u)
.map((layer) => layer.trim())
.filter((layer) => layer.startsWith('0 0 '))
.map((layer) => Number(/\/\s*(\d+)%/u.exec(layer)?.[1] ?? 0));
}
/** 选择器权重里可数的那部分:类与伪类的个数(只在同类选择器之间比高低)。 */
function selectorClassCount(selector: string) {
return (selector.match(/[.:]/gu) ?? []).length;
}
/** 按花括号配平取出包含指定选择器的 `@media (prefers-reduced-motion: reduce)` 块。 */
function reducedMotionBlockFor(styles: string, selectorFragment: string) {
const marker = '@media (prefers-reduced-motion: reduce)';
let index = styles.indexOf(marker);
while (index >= 0) {
const open = styles.indexOf('{', index);
let depth = 0;
let end = open;
for (let cursor = open; cursor < styles.length; cursor += 1) {
if (styles[cursor] === '{') {
depth += 1;
} else if (styles[cursor] === '}') {
depth -= 1;
if (depth === 0) {
end = cursor;
break;
}
}
}
const block = styles.slice(open + 1, end);
if (block.includes(selectorFragment)) {
return block;
}
index = styles.indexOf(marker, index + marker.length);
}
return null;
}
describe('当前使用资源卡的发光档案', () => {
it('当前版本资源卡带常驻呼吸光环,选中态提权压过它', () => {
const styles = stylesSource();
// 「当前使用」必须在卡片本体的 box-shadow 上有真的外向扩散层。旧值是
// `0 0 0 2px rgb(216 115 66 / 14%)`——那是描边不是发光,本用例就是钉这次改动的。
const base = ruleBody(styles, '\\.game-resource-card\\.is-current-version');
const glowAlphas = outwardGlowAlphas(base);
expect(glowAlphas.length).toBeGreaterThanOrEqual(2);
expect(Math.max(...glowAlphas)).toBeGreaterThanOrEqual(30);
expect(base).toMatch(/0 0 20px rgb\(216 115 66 \/ 3[0-9]%\)/u);
// 发光不靠加粗边框:边框口径仍是 1px。
expect(base).toMatch(/border:\s*1px solid/u);
// 「呼吸」挂在伪元素上,只动 opacity(本页既有写法),不碰布局也不吃点击。
const ring = ruleBody(
styles,
'\\.game-resource-card\\.is-current-version::after',
);
expect(ring).toMatch(
/animation:\s*game-resource-card-current-version-breathe/u,
);
expect(ring).toMatch(/pointer-events:\s*none/u);
expect(ring).toMatch(/border-radius:\s*inherit/u);
const breathe =
/@keyframes game-resource-card-current-version-breathe\s*\{([\s\S]*?)\n\}/u.exec(
styles,
)?.[1] ?? '';
// 声明档位 38%,呼吸区间 0.58 ↔ 1 ⇒ 视觉上约 22% ↔ 38%。
expect(outwardGlowAlphas(ring)).toEqual([38, 38]);
expect(breathe).toMatch(/0%,\s*100%\s*\{\s*opacity:\s*0\.58;/u);
expect(breathe).toMatch(/50%\s*\{\s*opacity:\s*1;/u);
// 选中 / 悬停 / 拖拽必须赢过常驻光环:显式提权到 (0,3,0),
// 而不是继续和 `.is-selected` 拼源文件顺序(旧写法下当前版本卡看不出被选中)。
const selectedVariant =
/(\.game-resource-card\.is-current-version\.is-selected,[\s\S]*?)\{([^}]*)\}/u.exec(
styles,
);
expect(selectedVariant).not.toBeNull();
expect(selectorClassCount(selectedVariant![1]!)).toBeGreaterThan(
selectorClassCount('.game-resource-card.is-current-version'),
);
// 选中态自己那层阴影真的写进了这个变体。
expect(selectedVariant![2]).toContain('0 8px 22px rgb(195 105 62 / 15%)');
expect(
Math.max(...outwardGlowAlphas(selectedVariant![2]!)),
).toBeGreaterThanOrEqual(30);
// 收敛动画但不收敛可见性:静态档位必须仍然看得见。
const reduced = reducedMotionBlockFor(
styles,
'.game-resource-card.is-current-version::after',
);
expect(reduced).not.toBeNull();
expect(reduced).toMatch(/animation:\s*none/u);
expect(
Number(/opacity:\s*(0?\.\d+)/u.exec(reduced!)?.[1]),
).toBeGreaterThanOrEqual(0.5);
// 「点击版本卡后高亮绑定资源」的旧规则只写 `border-color`,而卡片本体 `border: 0`
// 从未生效;现在必须是能落地的边框 + ring。
const relation = ruleBody(
styles,
'\\.game-resource-card\\.is-relation-version-binding',
);
expect(relation).toMatch(/border:\s*1px solid/u);
expect(Math.max(...outwardGlowAlphas(relation))).toBeGreaterThanOrEqual(30);
});
});