fix(desktop,island): 设置页灵动岛外观改动即生效——IPC 只接受纯数据 + 真实 UI 冒烟回归

用户反馈"灵动岛设置无法生效并且无法实时预览"。根因是设置页把 ref 的响应式 Proxy 直接交给 contextBridge,结构化克隆在进入 preload 函数体之前就抛 `An object could not be cloned.`,界面数字照常变化却半静默失败。

- SettingsView:应用/保存外观时展开成纯对象({ ...island.value })
- desktop.d.ts / preload:显式写明"必须传纯数据",记录该失败模式
- smoke.ts:新增「拖拽设置页真实滑块」用例,同时断言页面数值与主进程外观都变化
This commit is contained in:
LoveChengke committed 2026-09-27 10:43:57 +08:00
1 parent d900280760
commit 14ac16ff29
4 files changed
+113 -4

No files matched your search

+87
View File
@@ -2455,6 +2455,87 @@ async function runIslandRealtimeCheck(
return { delivered, deliveryDetail, markRead, markReadDetail };
}
/**
* 设置页「真实 UI 链路」自检(用户反馈:**灵动岛设置无法生效并且无法实时预览**)。
*
* 为什么必须单独来一条:上面的用例都是直接调 `window.desktop.islandSetAppearance(...)`,
* 绕过了设置页的控件,于是漏掉了真正的失败模式 —— 设置页把 Vue 的响应式 Proxy(`ref.value`)
* 直接交给 `ipcRenderer`,结构化克隆会失败并抛出 `An object could not be cloned.`。
* 这种失败是"半静默"的:滑块数字照常变化、界面不报错,只有主进程里的外观一动不动,
* 用户看到的就是"拖了滑块没反应、预览也没变化"。
*
* 因此这里在真实设置页里合成一次拖拽(mousedown → mousemove → mouseup,与用户操作同一路径),
* 并同时断言「滑块自身的数值」与「主进程里的外观」都变了 —— 只断言前者会漏掉这个 bug。
*/
async function runSettingsPageAppearanceCheck(
win: BrowserWindow,
): Promise<{ ok: boolean; detail: string }> {
const result = (await win.webContents
.executeJavaScript(
`(async () => {
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const before = await window.desktop.islandGetAppearance();
// 先固定基准高度:避免"当前高度恰好等于拖拽落点"导致假通过或假失败
window.desktop.islandSetAppearance({ ...before, height: 44 });
await wait(300);
location.hash = '#/settings';
let runway = null;
for (let i = 0; i < 40 && !runway; i += 1) {
await wait(100);
runway = document.querySelector('.el-slider__runway');
}
if (!runway) return { ok: false, detail: '设置页未渲染出滑块(未登录或页面未挂载)' };
const rect = runway.getBoundingClientRect();
if (!rect.width) return { ok: false, detail: '滑块宽度为 0,无法拖拽' };
const x = rect.left + rect.width * 0.9;
const y = rect.top + rect.height / 2;
const fire = (target, type, extra) =>
target.dispatchEvent(
new MouseEvent(
type,
Object.assign({ bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }, extra),
),
);
fire(runway, 'mousedown', { buttons: 1 });
await wait(60);
fire(window, 'mousemove', { buttons: 1 });
await wait(60);
fire(window, 'mouseup', {});
await wait(600);
const dragged = await window.desktop.islandGetAppearance();
const label =
Array.from(document.querySelectorAll('.el-form-item__label'))
.map((node) => (node.textContent ?? '').trim())
.find((text) => text.startsWith('高度')) ?? '';
// 复原外观与路由,不影响后续用例
window.desktop.islandSetAppearance({ ...before });
location.hash = '#/schedule';
return { ok: true, baseline: 44, dragged: dragged.height, label };
})()`,
)
.catch((error: unknown) => ({
ok: false as const,
detail: `执行失败:${error instanceof Error ? error.message : String(error)}`,
}))) as
| { ok: true; baseline: number; dragged: number; label: string }
| { ok: false; detail: string }
| null;
if (result?.ok) {
return {
ok: result.dragged !== result.baseline && result.label === `高度 ${result.dragged}px`,
detail:
`拖动高度滑块到 90%:页面标签="${result.label}",` +
`主进程外观高度 ${result.baseline} → ${result.dragged}`,
};
}
return { ok: false, detail: result?.detail ?? '未执行' };
}
/**
* Electron 冒烟验证(ELECTRON_SMOKE_TEST=1 时触发,跑完自动退出)。
* 覆盖:preload 桥接、渲染进程挂载、登录页 DOM、IndexedDB 缓存读写、离线回退,
@@ -2645,6 +2726,12 @@ export async function runSmokeTest(win: BrowserWindow): Promise<void> {
String(online?.detail ?? ''),
);
// 设置页真实 UI 链路(用户反馈:"灵动岛设置无法生效并且无法实时预览"):
// 必须真的去拖设置页里的滑块 —— 直接调 window.desktop.* 会绕过
// 「Vue 响应式 Proxy 过不了 IPC 结构化克隆」这个失败模式。
const settingsAppearance = await runSettingsPageAppearanceCheck(win);
record('设置页真实 UI:拖拽滑块即时改变灵动岛外观', settingsAppearance.ok, settingsAppearance.detail);
// 真实通知链路:教师发通知 → 客户端实时通道 → 灵动岛胶囊;
// 随后在灵动岛点"标为已读",验证通知中心同步为已读(修复"点了已读仍显示未读")
const realtime = await runIslandRealtimeCheck(win);