feat(island): 增加常驻内容并沿用 ClassIsland 尺寸与避让设置
This commit is contained in:
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);
|
||||
}
|
||||
Reference in new issue
Block a user