feat(island): 增加常驻内容并沿用 ClassIsland 尺寸与避让设置

This commit is contained in:
LoveChengke committed 2026-10-17 14:29:25 +08:00
1 parent c83b03d8e6
commit 7f7d3925b2
12 files changed
+1092 -127

No files matched your search

@@ -0,0 +1,285 @@
import fs from 'node:fs';
import path from 'node:path';
import {
defaultIslandLayouts,
islandNode,
normalizeIslandLayouts,
ISLAND_SHADOW_PAD,
} from '@classhelper/shared';
import { getConfig, saveConfig } from './config.js';
import { island } from './island.js';
/** 常驻岛始终用隔离的冒烟配置和本地消息;不读取或标记真实班级通知。 */
export async function verifyResidentSmoke(
check: (name: string, action: () => Promise<boolean | string> | boolean) => Promise<void>,
) {
const win = island.getWindow()!;
const originalTouchMode = island.getTouchMode();
island.setTouchMode(false);
island.setHitTestCursor({ x: 0, y: 0 });
const pause = (ms = 600) => new Promise<void>((resolve) => setTimeout(resolve, ms));
async function shot(name: string) {
if (!process.env.ISLAND_SHOTS_DIR) return;
fs.mkdirSync(process.env.ISLAND_SHOTS_DIR, { recursive: true });
fs.writeFileSync(
path.join(process.env.ISLAND_SHOTS_DIR, `${name}.png`),
(await win.webContents.capturePage()).toPNG(),
);
}
const fixture = defaultIslandLayouts();
fixture.layout = 'custom';
fixture.residentEnabled = true;
const compactText = islandNode('text', 'resident-compact-text');
compactText.settings.text = '常驻概览';
const expandedText = islandNode('text', 'resident-expanded-text');
expandedText.settings.text = '今日安排';
const scheme = fixture.schemes[0]!;
scheme.rows[0]!.nodes = [
islandNode('clock', 'resident-clock'),
islandNode('separator', 'resident-separator'),
compactText,
];
scheme.expandedRows[0]!.nodes = [islandNode('date', 'resident-date'), expandedText];
island.handleAction({ action: 'dismiss-all' });
await pause();
await check('旧岛配置默认关闭常驻,并兼容补齐空闲展开布局', () => {
const legacy = JSON.parse(JSON.stringify(fixture));
delete legacy.residentEnabled;
delete legacy.schemes[0].expandedRows;
const value = normalizeIslandLayouts(legacy);
const oversized = JSON.parse(JSON.stringify(fixture));
Object.assign(oversized.schemes[0], { scale: 99, radius: 99, opacity: -1, spacing: 999 });
const bounds = normalizeIslandLayouts(oversized).schemes[0]!;
return (
!value.residentEnabled &&
value.schemes[0]!.expandedRows[0]!.nodes.length > 0 &&
bounds.scale === 2.5 &&
bounds.radius === 20 &&
bounds.opacity === 0 &&
bounds.spacing === 150
);
});
saveConfig({ islandLayout: fixture });
island.syncCustomLayout();
await pause(800);
let compactWidth = 0;
await check('无通知常驻岛显示自定义内容,并为铃铛预留对齐空间', async () => {
const layout = await win.webContents.executeJavaScript(`(() => {
const card=document.querySelector('.custom-card'), strip=card?.querySelector('.resident-compact'), bell=card?.querySelector('.resident-bell'), clock=card?.querySelector('.clock'), text=card?.querySelector('.text span'), digits=clock?.querySelector('span');
if(!card||!strip||!bell||!clock||!text||!digits)return null;
const b=bell.getBoundingClientRect(), c=clock.getBoundingClientRect(), r=card.getBoundingClientRect();
const t=text.getBoundingClientRect(),d=digits.getBoundingClientRect();
return {width:r.width, height:r.height, content:card.textContent.includes('常驻概览'), hidden:bell.classList.contains('bell-empty')&&!bell.querySelector('span'), aligned:Math.abs(b.top+b.height/2-c.top-c.height/2)<=1 && Math.abs(t.top+t.height/2-d.top-d.height/2)<=4};
})()`);
compactWidth = layout?.width ?? 0;
return Boolean(
layout?.content &&
layout.hidden &&
layout.aligned &&
Math.abs(layout.height - 40) <= 1 &&
island.getState().mode === 'hidden',
);
});
await shot('resident-compact');
await check('鼠标移入常驻背景淡化至5%并穿透,移出恢复', async () => {
const normalOpacity = win.getOpacity();
const rect = await win.webContents.executeJavaScript(
`(() => {const r=document.querySelector('.clock').getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2};})()`,
);
const box = win.getBounds();
island.setHitTestCursor({ x: box.x + rect.x, y: box.y + rect.y });
await pause(120);
const faded = Math.abs(win.getOpacity() - normalOpacity * 0.05) < 0.01 && !island.getInteractive();
island.setHitTestCursor({ x: 0, y: 0 });
await pause(120);
return faded && Math.abs(win.getOpacity() - normalOpacity) < 0.01;
});
await check('右侧展开入口不避让,触摸交互暂停指针淡化', async () => {
const rect = await win.webContents.executeJavaScript(
`(() => {const r=document.querySelector('.resident-bell').getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2};})()`,
);
const box = win.getBounds(),
normalOpacity = win.getOpacity();
island.setHitTestCursor({ x: box.x + rect.x, y: box.y + rect.y });
await pause(120);
const controlAvailable =
Math.abs(win.getOpacity() - normalOpacity) < 0.01 && (!win.isVisible() || island.getInteractive());
island.setTouchMode(true);
island.noteResidentTouch();
island.setHitTestCursor({ x: box.x + 20, y: box.y + 20 });
await pause(120);
const touchAvailable = Math.abs(win.getOpacity() - normalOpacity) < 0.01;
island.setHitTestCursor({ x: 0, y: 0 });
island.setTouchMode(false);
await pause(1100);
return controlAvailable && touchAvailable;
});
await check('ClassIsland 反转淡化和关闭淡化设置生效', async () => {
const config = getConfig().islandLayout,
normalOpacity = win.getOpacity();
config.pointerFadeReversed = true;
saveConfig({ islandLayout: config });
island.syncCustomLayout();
await pause(120);
const reversed = Math.abs(win.getOpacity() - normalOpacity * 0.05) < 0.01;
config.pointerFadeEnabled = false;
saveConfig({ islandLayout: config });
island.syncCustomLayout();
await pause(120);
const disabled = Math.abs(win.getOpacity() - normalOpacity) < 0.01;
config.pointerFadeReversed = false;
config.pointerFadeEnabled = true;
saveConfig({ islandLayout: config });
island.syncCustomLayout();
return reversed && disabled;
});
await check('点击常驻内容展开无通知布局', async () => {
await win.webContents.executeJavaScript(`document.querySelector('.custom-card .clock')?.click()`);
await pause();
return (
island.getState().mode === 'expanded' &&
!island.getState().active &&
(await win.webContents.executeJavaScript(
`(() => {const card=document.querySelector('.custom-card');return Boolean(card?.querySelector('.resident-panel-heading')&&card.textContent.includes('今日安排')&&!card.textContent.includes('常驻概览'));})()`,
))
);
});
await shot('resident-empty-expanded');
await check('无通知展开页可以收回常驻岛且尺寸恢复', async () => {
await win.webContents.executeJavaScript(`document.querySelector('[aria-label="收起常驻岛"]')?.click()`);
await pause(800);
return (
island.getState().mode === 'hidden' &&
(await win.webContents.executeJavaScript(
`Math.abs(document.querySelector('.custom-card').getBoundingClientRect().width-${compactWidth})<=2`,
))
);
});
const message = (id: string, title: string, priority: 'NORMAL' | 'URGENT') => ({
id,
title,
content: '本地隔离消息内容',
priority,
createdAt: new Date().toISOString(),
teacherName: '本地验证',
courseName: null,
});
await check('上课收到普通消息只亮铃铛,常驻内容不横移', async () => {
island.setClassState({ inClass: true, currentPeriodEnd: null, week: 1 });
island.pushNotification(message('resident-normal', '常驻通知一', 'NORMAL'));
await pause();
return (
island.getState().mode === 'pill' &&
(await win.webContents.executeJavaScript(`(() => {
const card=document.querySelector('.custom-card'),bell=card?.querySelector('.resident-bell');
return Boolean(bell&&!bell.classList.contains('bell-empty')&&bell.textContent.trim()==='1'&&Math.abs(card.getBoundingClientRect().width-${compactWidth})<=2&&!card.classList.contains('has-alert'));
})()`))
);
});
await shot('resident-bell');
await check('紧急消息也先提示铃铛,重复消息不增加数量', async () => {
island.pushNotification(message('resident-urgent', '常驻通知二', 'URGENT'));
island.pushNotification(message('resident-urgent', '常驻通知二', 'URGENT'));
await pause();
return (
island.getState().mode === 'pill' &&
(await win.webContents.executeJavaScript(
`document.querySelector('.resident-bell')?.textContent.trim()==='2'`,
))
);
});
await check('点击铃铛展开原通知列表,上课时也可主动查看', async () => {
await win.webContents.executeJavaScript(`document.querySelector('.resident-bell')?.click()`);
await pause(1800);
return (
island.getState().mode === 'expanded' &&
(await win.webContents.executeJavaScript(
`(() => {const host=document.querySelector('#custom-alert-host');return Boolean(host&&host.getBoundingClientRect().height>0&&host.querySelectorAll('.list-row').length===2&&host.textContent.includes('常驻通知一')&&host.textContent.includes('常驻通知二')&&!document.querySelector('.resident-panel-heading'));})()`,
))
);
});
await shot('resident-notifications');
await check('收起通知保留铃铛,键盘可重新展开', async () => {
await win.webContents.executeJavaScript(
`document.querySelector('#custom-alert-host .icon-btn')?.click()`,
);
await pause(800);
const collapsed = island.getState().mode === 'pill';
await win.webContents.executeJavaScript(
`document.querySelector('.custom-card')?.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}))`,
);
await pause();
return collapsed && island.getState().mode === 'expanded';
});
await check('处理完通知清除铃铛,恢复常驻内容与窗口留白', async () => {
await win.webContents.executeJavaScript(
`document.querySelector('#custom-alert-host .solid-btn')?.click()`,
);
await pause(800);
const box = win.getBounds();
const dimensions = await win.webContents.executeJavaScript(
`(() => {const card=document.querySelector('.custom-card'), r=card.getBoundingClientRect();return {width:r.width,height:r.height,empty:card.querySelector('.resident-bell')?.classList.contains('bell-empty'),content:card.textContent.includes('常驻概览')};})()`,
);
return (
!island.getState().active &&
island.getState().queued.length === 0 &&
dimensions.empty === true &&
dimensions.content &&
Math.abs(box.width - dimensions.width - 2 * ISLAND_SHADOW_PAD) <= 2 &&
Math.abs(box.height - dimensions.height - 2 * ISLAND_SHADOW_PAD) <= 2
);
});
await check('空闲展开时收到消息返回铃铛提示', async () => {
island.handleAction({ action: 'expand' });
await pause();
island.pushNotification(message('resident-after-open', '展开后新消息', 'NORMAL'));
await pause(800);
const collapsed = island.getState().mode === 'pill';
island.handleAction({ action: 'dismiss-all' });
await pause();
return collapsed;
});
await check('常驻和无通知展开内容可独立编辑并持久化', async () => {
island.editLayout();
await pause();
await win.webContents.executeJavaScript(
`(() => {const button=[...document.querySelectorAll('.surface-tabs button')].find(b=>b.textContent.includes('无通知'));button?.click();})()`,
);
await pause(200);
await win.webContents.executeJavaScript(
`document.querySelector('[data-node="resident-expanded-text"]')?.click()`,
);
await pause(200);
await win.webContents.executeJavaScript(
`(() => {const input=document.querySelector('.settings textarea');if(!input)throw Error('未找到展开文本设置');input.value='放学前检查';input.dispatchEvent(new Event('input',{bubbles:true}));input.dispatchEvent(new Event('change',{bubbles:true}));})()`,
);
await pause();
const saved = getConfig().islandLayout.schemes[0]!;
const independent =
saved.rows[0]!.nodes.find((n) => n.id === 'resident-compact-text')?.settings.text === '常驻概览' &&
saved.expandedRows[0]!.nodes.find((n) => n.id === 'resident-expanded-text')?.settings.text ===
'放学前检查';
await shot('resident-editor');
island.editLayout(false);
await pause();
const roundtrip = normalizeIslandLayouts(JSON.parse(JSON.stringify(getConfig().islandLayout)));
return (
independent &&
roundtrip.residentEnabled &&
roundtrip.schemes[0]!.expandedRows[0]!.nodes.some((n) => n.settings.text === '放学前检查')
);
});
await check('关闭常驻并选择经典岛后恢复原有隐藏方式', async () => {
island.setClassState({ inClass: false, currentPeriodEnd: null, week: 1 });
saveConfig({ islandLayout: defaultIslandLayouts() });
island.syncCustomLayout();
await pause();
return (
!win.isVisible() &&
!getConfig().islandLayout.residentEnabled &&
(await win.webContents.executeJavaScript(`!document.querySelector('.custom-card')`))
);
});
island.setHitTestCursor(null);
island.setTouchMode(originalTouchMode);
}