fix(homework,island,verify): 全屏作业看板自适应 + 已安装副本覆盖升级 + 新增打包版冒烟

① 全屏作业看板自适应(用户反馈:全屏模式没有自适应,卡片挤在左上角)
- 全屏看板按「可用宽 × 可用高」枚举 1..N 列,取**单卡面积最大且宽高比最接近 1.15** 的方案,
  注入 `--board-cols`(不再靠 CSS auto-fit 猜);列数变了再量一次缩放系数,
  `transform-origin: top center` 保证整体居中、不溢出屏幕
- 两个全屏弹窗各自持有 ref(原来共用同名 ref,后挂载的会覆盖前一个,量到的是另一个弹窗);
  列表型「单个科目全屏」补上 `transform: scale()`(该规则原先只作用于 `.board`,列表全屏从未缩放)
- 弹窗 `@opened` 后轮询等待布局就绪(最多 20×60ms)再计算;窗口 resize 时自动重算;
  另加一条全局 `.board-fullscreen .el-dialog__body` 兜底,保证内容区铺满整屏
- 实测:6 个科目 → 3 列、排版宽 1155px(视口 1187px)、可视底 678px(视口高 765px)

② 「灵动岛设置没办法生效」的真正原因:用户机器上的副本一直没被更新
- 学生端实际运行的是安装目录 `D:\class\@classhelperdesktop-client` 里的 `resources/app.asar`,
  而 `pnpm dist:win` 只更新 `release/`;只发新版 release 不动安装目录,用户看到的还是旧界面
- 已就地覆盖升级该目录,并用同一套冒烟实测**那个副本**:`packaged: true`、76/76 全绿
- README 补充「把修复交付到已安装的客户端」步骤(robocopy 覆盖 + verify:packaged 实测)

③ 新增/加固自动化验证
- `verify:desktop` 新增「作业看板全屏自适应(铺满屏幕、内容不溢出)」:进作业页 → 切看板 →
  打开全屏 → 断言排版宽铺满视口、可视底不超出屏幕;再把卡片补到 6 张触发 resize,
  要求列数自动变多(1 → 3)且仍不溢出
- 点击/命中这类异步链路由"固定 sleep 后读一次"改为**轮询等待**(曾偶发误判为"点不开"),
  失败时把渲染进程当前类名、窗口可见性、岛体矩形写进结论,便于区分真 bug 与竞态
- 新增 `pnpm verify:packaged`:对**打包后/已安装**的 EXE 跑同一套冒烟(含二次启动校验文件锁),
  避免"开发产物全绿但交付给学生的 EXE 是旧的"这类问题
- 实测:`verify:desktop` 76/76(离线复跑 68/68);`verify:packaged` 对
  `release/win-unpacked` 与 `D:\class\@classhelperdesktop-client` 均 76/76、`packaged: true`;
  `verify:e2e` 149/149(安装实例);`typecheck` / `lint` / `format:check` 全绿
This commit is contained in:
LoveChengke committed 2026-09-13 09:26:52 +08:00
1 parent 11008928a1
commit 0ff2dc228b
7 files changed
+466 -67

No files matched your search

+50 -10
View File
@@ -1220,6 +1220,11 @@ async function runIslandChecks(
island.setHitTestCursor(pillPoint);
await sleep(260);
const interactiveWhilePill = island.getInteractive();
// 点击前记录渲染进程当前形态:若渲染进程停在 expanded(状态推送滞后),
// 合成点击会被解释成"点空白处 → 收起",表现为"点了没反应",这里留证据便于定位。
const classBeforeClick = await islandWindow?.webContents
.executeJavaScript(`document.querySelector('.island-card')?.className ?? '-'`)
.catch(() => '-');
await islandWindow?.webContents
.executeJavaScript(
`(() => {
@@ -1228,16 +1233,24 @@ async function runIslandChecks(
})()`,
)
.catch(() => undefined);
await sleep(750);
const reopened = island.getState();
// 轮询等待主进程状态落到 expanded(不同环境 IPC/动画耗时不同,固定 sleep 会误判)
let reopened = island.getState();
for (let attempt = 0; attempt < 12 && reopened.mode !== 'expanded'; attempt += 1) {
await sleep(120);
reopened = island.getState();
}
const classAfterClick = await islandWindow?.webContents
.executeJavaScript(`document.querySelector('.island-card')?.className ?? '-'`)
.catch(() => '-');
record(
'收起态点击可再次展开(胶囊命中恢复 + 点击后展开)',
forcedThrough === false &&
interactiveWhilePill === true &&
reopened.mode === 'expanded' &&
reopened.active?.id === 'smoke-collapse-reopen',
`置穿透后=${forcedThrough} 光标在胶囊上时=${interactiveWhilePill} 点击后 mode=${reopened.mode} ` +
`active=${reopened.active?.id ?? '-'}`,
`置穿透后=${forcedThrough} 光标在胶囊上时=${interactiveWhilePill} ` +
`点击前渲染类名=${classBeforeClick} 点击后 mode=${reopened.mode} ` +
`active=${reopened.active?.id ?? '-'} 点击后渲染类名=${classAfterClick}`,
);
const sliverForInteractive = getSliverSize();
@@ -1979,10 +1992,16 @@ async function runIslandChecks(
)
.catch(() => undefined);
await sleep(180);
// 兜底轮询读的是主进程光标坐标:注入到岛体中心,等价于"指针停在岛内"
// 兜底轮询读的是主进程光标坐标:注入到岛体中心,等价于"指针停在岛内"。
// 命中判定链路是异步的(渲染进程上报矩形 → 主进程 120ms 轮询校正),
// 因此这里**轮询等待**而不是固定 sleep 后读一次,避免偶发误判。
island.setHitTestCursor(islandCardScreenPoint(0.5, 0.5));
await sleep(260);
const interactiveInside = island.getInteractive();
let interactiveInside = island.getInteractive();
for (let index = 0; index < 10 && interactiveInside !== true; index += 1) {
await sleep(120);
island.setHitTestCursor(islandCardScreenPoint(0.5, 0.5));
interactiveInside = island.getInteractive();
}
await islandWindow?.webContents
.executeJavaScript(
`(() => {
@@ -1993,13 +2012,20 @@ async function runIslandChecks(
.catch(() => undefined);
// 光标的权威判定在主进程(兜底轮询):这里把光标注入到屏幕左上角,等价于"指针离开岛体"
island.setHitTestCursor({ x: 1, y: 1 });
await sleep(260);
const interactiveOutside = island.getInteractive();
let interactiveOutside = island.getInteractive();
for (let index = 0; index < 10 && interactiveOutside !== false; index += 1) {
await sleep(120);
island.setHitTestCursor({ x: 1, y: 1 });
interactiveOutside = island.getInteractive();
}
const hitCheckGeometry = island.getHitRect();
island.setHitTestCursor(null);
record(
'岛外鼠标穿透(固定大窗口不吞桌面点击)',
interactiveInside === true && interactiveOutside === false,
`指针在岛内 interactive=${interactiveInside},指针在岛外 interactive=${interactiveOutside}(要求 true / false)`,
`指针在岛内 interactive=${interactiveInside},指针在岛外 interactive=${interactiveOutside}(要求 true / false)` +
` 窗口可见=${islandWindow?.isVisible() ?? false} 状态=${island.getState().mode}` +
` 岛体矩形=${hitCheckGeometry ? `${Math.round(hitCheckGeometry.x)},${Math.round(hitCheckGeometry.y)} ${Math.round(hitCheckGeometry.width)}x${Math.round(hitCheckGeometry.height)}` : 'null'}`,
);
island.setAppearance({ ...appearanceBefore, style: 'black' });
await sleep(200);
@@ -2464,6 +2490,20 @@ export async function runSmokeTest(win: BrowserWindow): Promise<void> {
String(timeline?.detail ?? ''),
);
// 作业看板全屏自适应(用户反馈:全屏看板没有铺满屏幕):
// 进作业页 → 切看板 → 打开全屏 → 量布局宽度/可视底部,要求铺满且不需要滚动
const boardFullscreen = await win.webContents.executeJavaScript(
`(async () => {
if (!window.__classhelperSmoke__?.homeworkBoardSelfTest) return { ok: false, detail: '缺少看板自适应自检钩子' };
return await window.__classhelperSmoke__.homeworkBoardSelfTest();
})()`,
);
record(
'作业看板全屏自适应(铺满屏幕、内容不溢出)',
Boolean(boardFullscreen?.ok),
String(boardFullscreen?.detail ?? ''),
);
// 侧边栏点击导航(回归测试:曾因把 index 当路由名导致点击无反应)
const navigation = await win.webContents.executeJavaScript(
`(async () => {