fix(console): 修「全屏时反而画标题栏」——窗口模式必须与 URL 的 windowed 一致
用户实机反馈:单屏窗口化窗口按 F11 全屏后标题栏不消失,退回窗口化反而没标题栏,
表现与预期完全相反。
根因不在标题栏,而在 Console 侧的窗口模式管理:entry 没记录窗口是以什么模式建的,
layout() 每次排布都拿「整机是否单屏」的 single 去覆盖每个已存在窗口的几何与模式
(!single 时会 win.setKiosk(true) + 铺满),而页面 URL 里的 windowed=1 是建窗时定死的、
loadPage 只在页码变化时才重载 —— 于是窗口的真实模式与页面认知脱钩:
窗口化窗口被改成 kiosk 全屏而 URL 仍带 windowed=1(全屏却画标题栏),反向亦然。
修复:建立并保证「窗口几何/模式 ↔ URL 的 windowed 必须永远一致」
- entry 记 { win, page, single, manual, reloadTimer }
- createWindow(..., manual=false);openOn() 传 manual=true(手动开的窗口不被排布改模式)
- layout() 的 existing 分支:target = manual ? entry.single : single,applyGeometry 与
loadPage 都用同一个 target,且 modeChanged || page 变了都要重载 URL
- 删掉 isKioskWindow()(用 isFullScreen() 反推会把「窗口化窗口被 F11 全屏」误报成 kiosk),
换成 modeOf(entry)
- F11 通信时序:enter/leave-full-screen 回调里不再回读 win.isFullScreen()(切换瞬间可能读到
旧值,页面会收到相反状态),改为下发事件本身的确定值;F11 直接下发算出来的 next
回归测试 console/main/displays.test.js(8 例,assertModeConsistent 四处自洽)。
反向对照:修复前的 displays.js 跑同一份测试是 1 pass / 7 fail。
Console 语法检查 18 个文件、node --test 28 例全过。
This commit is contained in:
1 parent
5ae49b001c
commit
793d2ac064
3 files changed
+381
-25
No files matched your search
@@ -28,6 +28,7 @@ console/
|
|||||||
│ ├── instances.js # 已连接大屏实例看板:复用管理面板登录态读 /api/display/instances
|
│ ├── instances.js # 已连接大屏实例看板:复用管理面板登录态读 /api/display/instances
|
||||||
│ ├── account.js # 管理员账号重置:参数校验 + 调 scoreboard.exe passwd
|
│ ├── account.js # 管理员账号重置:参数校验 + 调 scoreboard.exe passwd
|
||||||
│ ├── displays.js # 大屏窗口管理:选屏、kiosk、排布、24h 重载
|
│ ├── displays.js # 大屏窗口管理:选屏、kiosk、排布、24h 重载
|
||||||
|
│ ├── displays.test.js # 大屏窗口模式与 URL 一致性的回归测试
|
||||||
│ ├── log-bus.js # 日志环形缓冲 + 落文件 + 广播
|
│ ├── log-bus.js # 日志环形缓冲 + 落文件 + 广播
|
||||||
│ └── tray-icon.js # 托盘图标(base64 内嵌,无外部资源文件)
|
│ └── tray-icon.js # 托盘图标(base64 内嵌,无外部资源文件)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
@@ -119,6 +120,7 @@ electron-builder 生成的卸载器结尾是 `RMDir /r $INSTDIR`,而**覆盖
|
|||||||
| 场景 | 行为 |
|
| 场景 | 行为 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不挂** `display-metrics-changed` 等监听(行为冻结);`F11` 手动切换全屏,窗口级全屏状态经 `display-preload.js` 下发给页面,页面据此收起自绘标题栏 |
|
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不挂** `display-metrics-changed` 等监听(行为冻结);`F11` 手动切换全屏,窗口级全屏状态经 `display-preload.js` 下发给页面,页面据此收起自绘标题栏 |
|
||||||
|
| 模式与 URL | **必须永远一致**:模式(窗口化 / kiosk)记在窗口的 entry 上,谁改模式谁按新模式重载 URL。`layout()` 只对排布自己开的窗口套用全局 `single`,管理面板手动开的窗口保持自己的模式——否则会出现「窗口化窗口被改成 kiosk 全屏、URL 里还留着 `windowed=1`」这种页面认知与窗口真实模式脱钩、标题栏画反的问题(回归测试见 `main/displays.test.js`)|
|
||||||
| 多块屏 | 除内置屏外的每块屏一个 kiosk 窗口,按 `display.bounds`(DIP)定位,窗口 `setIgnoreMouseEvents(true)`,光标由页面 CSS 隐藏 |
|
| 多块屏 | 除内置屏外的每块屏一个 kiosk 窗口,按 `display.bounds`(DIP)定位,窗口 `setIgnoreMouseEvents(true)`,光标由页面 CSS 隐藏 |
|
||||||
| 选屏 | 运行时枚举,不写死坐标;`selection` 可选 `all-secondary`(默认,用 `display.internal` 排除内置屏)/ `all` / `specified` |
|
| 选屏 | 运行时枚举,不写死坐标;`selection` 可选 `all-secondary`(默认,用 `display.internal` 排除内置屏)/ `all` / `specified` |
|
||||||
| 页面锁定 | 每块屏各锁一页,URL 形如 `http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>`,页序按 `display.pages` 轮转 |
|
| 页面锁定 | 每块屏各锁一页,URL 形如 `http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>`,页序按 `display.pages` 轮转 |
|
||||||
|
|||||||
+44
-25
@@ -22,7 +22,9 @@ class DisplayManager {
|
|||||||
this.enabled = false
|
this.enabled = false
|
||||||
this.single = true
|
this.single = true
|
||||||
this.boundSingle = null
|
this.boundSingle = null
|
||||||
this.windows = new Map() // displayId -> { win, page, reloadTimer }
|
// entry.single 记这个窗口是以哪种模式建的(true=窗口化,false=kiosk),
|
||||||
|
// entry.manual 记它是管理面板手动开的(不跟随自动排布改模式)
|
||||||
|
this.windows = new Map() // displayId -> { win, page, single, manual, reloadTimer }
|
||||||
this.screenListeners = []
|
this.screenListeners = []
|
||||||
this.layoutTimer = null
|
this.layoutTimer = null
|
||||||
}
|
}
|
||||||
@@ -102,10 +104,17 @@ class DisplayManager {
|
|||||||
// 窗口可能被外部关闭/崩溃销毁而没走 destroyEntry,这里当作不存在重建,
|
// 窗口可能被外部关闭/崩溃销毁而没走 destroyEntry,这里当作不存在重建,
|
||||||
// 否则 applyGeometry/loadPage 对 destroyed 窗口静默 return,现场只剩余黑屏
|
// 否则 applyGeometry/loadPage 对 destroyed 窗口静默 return,现场只剩余黑屏
|
||||||
if (existing && existing.win && !existing.win.isDestroyed()) {
|
if (existing && existing.win && !existing.win.isDestroyed()) {
|
||||||
this.applyGeometry(existing.win, display, single)
|
// 手动开的窗口保持建窗时定的模式,自动排布只负责摆位置;
|
||||||
if (existing.page !== page) {
|
// 排布自己开的窗口跟随本次排布(电视拔了就该变回窗口化)
|
||||||
|
const target = existing.manual ? existing.single : single
|
||||||
|
const modeChanged = target !== existing.single
|
||||||
|
existing.single = target
|
||||||
|
this.applyGeometry(existing.win, display, target)
|
||||||
|
// 模式变了必须连 URL 一起换:URL 里的 windowed 是页面对「要不要画标题栏」的全部认知,
|
||||||
|
// 只改几何不重载,页面就会带着旧参数画(全屏仍画标题栏、窗口化却空着一条)
|
||||||
|
if (modeChanged || existing.page !== page) {
|
||||||
existing.page = page
|
existing.page = page
|
||||||
this.loadPage(existing, display, page, single)
|
this.loadPage(existing, display, page, target)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -119,7 +128,7 @@ class DisplayManager {
|
|||||||
this.log.push('console', `大屏排布完成:${single ? '单屏窗口化' : `多屏 kiosk × ${displays.length}`},显示器 ${displays.map((d) => d.id).join(', ')}`)
|
this.log.push('console', `大屏排布完成:${single ? '单屏窗口化' : `多屏 kiosk × ${displays.length}`},显示器 ${displays.map((d) => d.id).join(', ')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
createWindow(display, index, single, page) {
|
createWindow(display, index, single, page, manual = false) {
|
||||||
const bounds = display.bounds
|
const bounds = display.bounds
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
...this.geometryFor(display, single),
|
...this.geometryFor(display, single),
|
||||||
@@ -149,26 +158,32 @@ class DisplayManager {
|
|||||||
win.setIgnoreMouseEvents(true)
|
win.setIgnoreMouseEvents(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 窗口级全屏只在主进程可见:页面拿不到 setFullScreen 的结果,标题栏也就不会让位。
|
||||||
|
// 负载优先用调用方给的确定值——在回调里回读 win.isFullScreen() 会读到切换瞬间的旧值
|
||||||
|
// (Windows 上尤其),页面就会收到相反的状态、把标题栏画反。只有加载完成那次窗口
|
||||||
|
// 状态已经稳定,才回落到「读一次」。
|
||||||
|
const notifyFullscreen = (fullscreen) => {
|
||||||
|
if (win.isDestroyed()) return
|
||||||
|
win.webContents.send('window:fullscreen-changed', fullscreen === undefined ? win.isFullScreen() : fullscreen === true)
|
||||||
|
}
|
||||||
|
|
||||||
// F11 只在单屏模式下有意义(现场调试用),多屏 kiosk 不允许切走
|
// F11 只在单屏模式下有意义(现场调试用),多屏 kiosk 不允许切走
|
||||||
if (single) {
|
if (single) {
|
||||||
win.webContents.on('before-input-event', (event, input) => {
|
win.webContents.on('before-input-event', (event, input) => {
|
||||||
if (input.type === 'keyDown' && input.key === 'F11') {
|
if (input.type === 'keyDown' && input.key === 'F11') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
win.setFullScreen(!win.isFullScreen())
|
const next = !win.isFullScreen()
|
||||||
|
win.setFullScreen(next)
|
||||||
|
notifyFullscreen(next)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口级全屏只在主进程可见:页面拿不到 setFullScreen 的结果,标题栏也就不会让位。
|
|
||||||
// 状态变化下发一次之外,加载完成还要补发一次——重载/渲染进程崩溃恢复后窗口可能
|
// 状态变化下发一次之外,加载完成还要补发一次——重载/渲染进程崩溃恢复后窗口可能
|
||||||
// 已经是全屏,只挂事件会让页面一直画着标题栏。
|
// 已经是全屏,只挂事件会让页面一直画着标题栏。
|
||||||
const notifyFullscreen = () => {
|
win.on('enter-full-screen', () => notifyFullscreen(true))
|
||||||
if (win.isDestroyed()) return
|
win.on('leave-full-screen', () => notifyFullscreen(false))
|
||||||
win.webContents.send('window:fullscreen-changed', win.isFullScreen())
|
win.webContents.on('did-finish-load', () => notifyFullscreen())
|
||||||
}
|
|
||||||
win.on('enter-full-screen', notifyFullscreen)
|
|
||||||
win.on('leave-full-screen', notifyFullscreen)
|
|
||||||
win.webContents.on('did-finish-load', notifyFullscreen)
|
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
win.once('ready-to-show', () => {
|
||||||
if (!win.isDestroyed()) win.show()
|
if (!win.isDestroyed()) win.show()
|
||||||
@@ -179,7 +194,9 @@ class DisplayManager {
|
|||||||
if (!win.isDestroyed()) win.webContents.reload()
|
if (!win.isDestroyed()) win.webContents.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
const entry = { win, page, reloadTimer: null }
|
// single 必须跟着 entry 走:页面 URL 里的 windowed 是按它生成的,
|
||||||
|
// 排布后来改了模式而这里没记,窗口的真实模式就和页面的认知脱钩了
|
||||||
|
const entry = { win, page, single, manual, reloadTimer: null }
|
||||||
|
|
||||||
// 窗口销毁(外部关闭/渲染进程崩溃)后必须把 entry 从 map 摘掉,
|
// 窗口销毁(外部关闭/渲染进程崩溃)后必须把 entry 从 map 摘掉,
|
||||||
// 否则 layout/relayout 一直拿到已销毁窗口而静默跳过,现场只剩余黑屏
|
// 否则 layout/relayout 一直拿到已销毁窗口而静默跳过,现场只剩余黑屏
|
||||||
@@ -299,10 +316,10 @@ class DisplayManager {
|
|||||||
this.layout()
|
this.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口的当前全屏状态:以窗口为准,配置改了 kiosk 但窗口没重建时也能报对
|
// 窗口模式只看 entry 记的建窗模式,不用 win.isKiosk()/isFullScreen() 反推:
|
||||||
isKioskWindow(win) {
|
// 窗口化窗口被 F11 全屏后 isFullScreen() 也是 true,反推会把窗口化误报成 kiosk
|
||||||
if (!win || win.isDestroyed()) return false
|
modeOf(entry) {
|
||||||
return Boolean(win.isKiosk() || win.isFullScreen())
|
return entry && entry.single === false ? 'kiosk' : 'windowed'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 管理面板手动为某块屏开窗:已经开着就只聚焦,重建会打断正在展示的大屏
|
// 管理面板手动为某块屏开窗:已经开着就只聚焦,重建会打断正在展示的大屏
|
||||||
@@ -315,8 +332,8 @@ class DisplayManager {
|
|||||||
if (existing.win.isMinimized()) existing.win.restore()
|
if (existing.win.isMinimized()) existing.win.restore()
|
||||||
existing.win.show()
|
existing.win.show()
|
||||||
existing.win.focus()
|
existing.win.focus()
|
||||||
// 已有窗口按它的真实状态报模式(可能与本次请求不同),面板才不会说反
|
// 已有窗口按它的真实模式报(可能与本次请求不同),面板才不会说反
|
||||||
return { created: false, mode: this.isKioskWindow(existing.win) ? 'kiosk' : 'windowed' }
|
return { created: false, mode: this.modeOf(existing) }
|
||||||
}
|
}
|
||||||
// 已销毁的窗口占着 map 会让后续 layout 静默跳过,先摘掉旧 entry 再建
|
// 已销毁的窗口占着 map 会让后续 layout 静默跳过,先摘掉旧 entry 再建
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -324,10 +341,12 @@ class DisplayManager {
|
|||||||
this.windows.delete(display.id)
|
this.windows.delete(display.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动开的窗口也计入 enabled:服务停止时 closeAll 才会把它一起收走
|
// 手动开的窗口也计入 enabled:服务停止时 closeAll 才会把它一起收走。
|
||||||
|
// manual=true:自动排布不许改它的模式,否则面板开的小窗会被下一次排布顶成 kiosk
|
||||||
this.enabled = true
|
this.enabled = true
|
||||||
this.windows.set(display.id, this.createWindow(display, 0, !kiosk, wantedPage))
|
const entry = this.createWindow(display, 0, !kiosk, wantedPage, true)
|
||||||
return { created: true, mode: kiosk ? 'kiosk' : 'windowed' }
|
this.windows.set(display.id, entry)
|
||||||
|
return { created: true, mode: this.modeOf(entry) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关掉某块屏上的大屏窗口:kiosk 窗口刻意忽略鼠标,误开后只能从这里收
|
// 关掉某块屏上的大屏窗口:kiosk 窗口刻意忽略鼠标,误开后只能从这里收
|
||||||
@@ -355,7 +374,7 @@ class DisplayManager {
|
|||||||
scaleFactor: display.scaleFactor,
|
scaleFactor: display.scaleFactor,
|
||||||
open,
|
open,
|
||||||
page: open ? entry.page : this.pageFor(index),
|
page: open ? entry.page : this.pageFor(index),
|
||||||
mode: open ? (this.isKioskWindow(entry.win) ? 'kiosk' : 'windowed') : null
|
mode: open ? this.modeOf(entry) : null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 单屏机器上谈「扩展屏全屏」没有意义,与 pickDisplays 同一口径
|
// 单屏机器上谈「扩展屏全屏」没有意义,与 pickDisplays 同一口径
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const Module = require('node:module')
|
||||||
|
const test = require('node:test')
|
||||||
|
const { EventEmitter } = require('node:events')
|
||||||
|
|
||||||
|
// displays.js 顶层 require('electron'),而开发机上的 electron 包没有下载 dist,直接 require 会抛。
|
||||||
|
// 这里在加载它之前把 electron 换成桩:本文件只验证「窗口模式 ↔ URL 里的 windowed」这条不变式,
|
||||||
|
// 不需要真窗口,也跑不了真的窗口级全屏(那只能在 Windows 实机上验)。
|
||||||
|
class FakeWebContents extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.url = ''
|
||||||
|
this.sent = []
|
||||||
|
this.loadCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
send(channel, payload) {
|
||||||
|
this.sent.push([channel, payload])
|
||||||
|
}
|
||||||
|
|
||||||
|
loadURL(url) {
|
||||||
|
this.url = url
|
||||||
|
this.loadCount += 1
|
||||||
|
this.emit('did-finish-load')
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
getURL() {
|
||||||
|
return this.url
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
this.loadCount += 1
|
||||||
|
this.emit('did-finish-load')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let created = []
|
||||||
|
|
||||||
|
class FakeBrowserWindow extends EventEmitter {
|
||||||
|
constructor(options) {
|
||||||
|
super()
|
||||||
|
this.options = options
|
||||||
|
this.webContents = new FakeWebContents()
|
||||||
|
this.destroyed = false
|
||||||
|
this.kiosk = options.kiosk === true
|
||||||
|
this.fullScreen = false
|
||||||
|
// 模拟真实 Electron 在切换瞬间回读到的旧值:设了它 isFullScreen() 就返回这个
|
||||||
|
this.staleFullScreen = undefined
|
||||||
|
this.bounds = { x: options.x, y: options.y, width: options.width, height: options.height }
|
||||||
|
this.shown = false
|
||||||
|
created.push(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Electron 的 BrowserWindow.loadURL 是 webContents.loadURL 的快捷方式,displays.js 用的是前者
|
||||||
|
loadURL(url) {
|
||||||
|
return this.webContents.loadURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
isDestroyed() {
|
||||||
|
return this.destroyed
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.destroyed = true
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
|
|
||||||
|
isFullScreen() {
|
||||||
|
return this.staleFullScreen === undefined ? this.fullScreen : this.staleFullScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
setFullScreen(value) {
|
||||||
|
this.fullScreen = value
|
||||||
|
}
|
||||||
|
|
||||||
|
isKiosk() {
|
||||||
|
return this.kiosk
|
||||||
|
}
|
||||||
|
|
||||||
|
setKiosk(value) {
|
||||||
|
this.kiosk = value
|
||||||
|
}
|
||||||
|
|
||||||
|
getBounds() {
|
||||||
|
return this.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
setBounds(bounds) {
|
||||||
|
this.bounds = bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
setIgnoreMouseEvents(value) {
|
||||||
|
this.ignoreMouse = value
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.shown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {}
|
||||||
|
|
||||||
|
isMinimized() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fakeScreen = {
|
||||||
|
displays: [],
|
||||||
|
primaryId: 1,
|
||||||
|
getAllDisplays() {
|
||||||
|
return this.displays
|
||||||
|
},
|
||||||
|
getPrimaryDisplay() {
|
||||||
|
return this.displays.find((item) => item.id === this.primaryId) || this.displays[0]
|
||||||
|
},
|
||||||
|
on() {},
|
||||||
|
removeListener() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const electronStub = { BrowserWindow: FakeBrowserWindow, screen: fakeScreen }
|
||||||
|
const originalLoad = Module._load
|
||||||
|
Module._load = function (request, parent, isMain) {
|
||||||
|
if (request === 'electron') return electronStub
|
||||||
|
return originalLoad.call(this, request, parent, isMain)
|
||||||
|
}
|
||||||
|
const { DisplayManager } = require('./displays')
|
||||||
|
Module._load = originalLoad
|
||||||
|
|
||||||
|
const WIDTH = 1920
|
||||||
|
const HEIGHT = 1080
|
||||||
|
|
||||||
|
function fakeDisplay(id, { internal = false } = {}) {
|
||||||
|
return { id, label: `显示器 ${id}`, internal, bounds: { x: 0, y: 0, width: WIDTH, height: HEIGHT }, scaleFactor: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeManager(t, { selection = 'all', kiosk = true } = {}) {
|
||||||
|
fakeScreen.displays = []
|
||||||
|
fakeScreen.primaryId = 1
|
||||||
|
created = []
|
||||||
|
const log = { lines: [], push(_scope, text) { this.lines.push(text) } }
|
||||||
|
const manager = new DisplayManager({
|
||||||
|
values: {
|
||||||
|
display: {
|
||||||
|
selection,
|
||||||
|
displays: [],
|
||||||
|
pages: ['standings', 'medals'],
|
||||||
|
pageParam: 'page',
|
||||||
|
kiosk,
|
||||||
|
ignoreMouse: true,
|
||||||
|
reloadEveryHours: 0,
|
||||||
|
backgroundColor: '#000000'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: { host: '0.0.0.0', port: 19540 },
|
||||||
|
log
|
||||||
|
})
|
||||||
|
t.after(() => manager.closeAll())
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 唯一不变式:窗口的几何/模式、页面 URL 里的 windowed 参数必须一致。
|
||||||
|
* 这条断言在每个用例里都跑,任何一种脱钩都会在这里炸出来。
|
||||||
|
*/
|
||||||
|
function assertModeConsistent(manager, displayId, expectedSingle) {
|
||||||
|
const entry = manager.windows.get(displayId)
|
||||||
|
assert.ok(entry, `display=${displayId} 上应该有窗口`)
|
||||||
|
|
||||||
|
assert.equal(entry.single, expectedSingle, `entry.single 应为 ${expectedSingle}`)
|
||||||
|
assert.equal(entry.win.isKiosk(), expectedSingle === false, 'kiosk 标记应与 entry.single 一致')
|
||||||
|
|
||||||
|
const url = new URL(entry.win.webContents.getURL())
|
||||||
|
assert.equal(url.pathname, '/display')
|
||||||
|
assert.equal(
|
||||||
|
url.searchParams.get('windowed'),
|
||||||
|
expectedSingle ? '1' : null,
|
||||||
|
`URL 的 windowed 应与 entry.single 一致:${url.href}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const bounds = entry.win.getBounds()
|
||||||
|
assert.equal(bounds.width, expectedSingle ? Math.round(WIDTH * 0.8) : WIDTH, '窗口宽度应与模式一致')
|
||||||
|
assert.equal(bounds.height, expectedSingle ? Math.round(HEIGHT * 0.8) : HEIGHT, '窗口高度应与模式一致')
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
test('单屏排布:窗口化窗口的 URL 带 windowed=1', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true })]
|
||||||
|
|
||||||
|
manager.enable()
|
||||||
|
|
||||||
|
const entry = assertModeConsistent(manager, 1, true)
|
||||||
|
assert.equal(entry.manual, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('多屏排布:kiosk 窗口的 URL 不带 windowed', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true }), fakeDisplay(2)]
|
||||||
|
|
||||||
|
manager.enable()
|
||||||
|
|
||||||
|
assertModeConsistent(manager, 1, false)
|
||||||
|
assertModeConsistent(manager, 2, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('回归:窗口化窗口被排布改成 kiosk 时,URL 一并换成不带 windowed 并重载', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true })]
|
||||||
|
manager.enable()
|
||||||
|
|
||||||
|
const before = assertModeConsistent(manager, 1, true)
|
||||||
|
const loadsBefore = before.win.webContents.loadCount
|
||||||
|
assert.match(before.win.webContents.getURL(), /windowed=1/)
|
||||||
|
|
||||||
|
// 插上第二块屏,整机从「单屏窗口化」变成「多屏 kiosk」
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true }), fakeDisplay(2)]
|
||||||
|
manager.layout()
|
||||||
|
|
||||||
|
const after = assertModeConsistent(manager, 1, false)
|
||||||
|
assert.equal(after.win, before.win, '同一个窗口应当就地改模式,而不是重建')
|
||||||
|
assert.equal(after.win.webContents.loadCount, loadsBefore + 1, '模式变了必须重载一次 URL')
|
||||||
|
assert.doesNotMatch(after.win.webContents.getURL(), /windowed=1/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('回归反向:kiosk 窗口被排布改成窗口化时,URL 补上 windowed=1', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true }), fakeDisplay(2)]
|
||||||
|
manager.enable()
|
||||||
|
|
||||||
|
const before = assertModeConsistent(manager, 1, false)
|
||||||
|
const loadsBefore = before.win.webContents.loadCount
|
||||||
|
|
||||||
|
// 拔掉扩展屏,只剩主屏 → 退化成窗口化
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true })]
|
||||||
|
manager.layout()
|
||||||
|
|
||||||
|
const after = assertModeConsistent(manager, 1, true)
|
||||||
|
assert.equal(after.win, before.win, '同一个窗口应当就地改模式,而不是重建')
|
||||||
|
assert.equal(after.win.webContents.loadCount, loadsBefore + 1, '模式变了必须重载一次 URL')
|
||||||
|
assert.match(after.win.webContents.getURL(), /windowed=1/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('管理面板手动开的窗口保持自己的模式,不被自动排布改成 kiosk', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
const primary = fakeDisplay(1, { internal: true })
|
||||||
|
fakeScreen.displays = [primary]
|
||||||
|
|
||||||
|
const opened = manager.openOn(primary, { page: 'standings', single: true })
|
||||||
|
assert.deepEqual(opened, { created: true, mode: 'windowed' })
|
||||||
|
assertModeConsistent(manager, 1, true)
|
||||||
|
|
||||||
|
// 之后整机变成多屏,自动排布也不许把这个手动开的小窗顶成 kiosk
|
||||||
|
fakeScreen.displays = [primary, fakeDisplay(2)]
|
||||||
|
manager.layout()
|
||||||
|
|
||||||
|
assertModeConsistent(manager, 1, true)
|
||||||
|
assertModeConsistent(manager, 2, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('模式上报以 entry 为准:窗口化窗口被 F11 全屏后仍报 windowed', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
const primary = fakeDisplay(1, { internal: true })
|
||||||
|
fakeScreen.displays = [primary]
|
||||||
|
|
||||||
|
manager.openOn(primary, { single: true })
|
||||||
|
const win = manager.windows.get(1).win
|
||||||
|
win.setFullScreen(true)
|
||||||
|
|
||||||
|
// 只聚焦不重建时,模式必须还说窗口化:拿 isFullScreen() 反推会误报成 kiosk
|
||||||
|
assert.deepEqual(manager.openOn(primary, { single: true }), { created: false, mode: 'windowed' })
|
||||||
|
assert.equal(manager.listDisplays().displays.find((item) => item.id === 1).mode, 'windowed')
|
||||||
|
assert.equal(manager.modeOf(manager.windows.get(1)), 'windowed')
|
||||||
|
|
||||||
|
// kiosk 窗口不受 F11 影响,仍报 kiosk
|
||||||
|
fakeScreen.displays = [primary, fakeDisplay(2)]
|
||||||
|
manager.layout()
|
||||||
|
assert.equal(manager.listDisplays().displays.find((item) => item.id === 2).mode, 'kiosk')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('F11 下发的是算出来的确定值,不回读窗口', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true })]
|
||||||
|
manager.enable()
|
||||||
|
|
||||||
|
const win = manager.windows.get(1).win
|
||||||
|
const [handler] = win.webContents.listeners('before-input-event')
|
||||||
|
assert.ok(handler, '窗口化窗口必须挂 F11 处理')
|
||||||
|
|
||||||
|
let prevented = 0
|
||||||
|
const pressF11 = () => handler({ preventDefault: () => { prevented += 1 } }, { type: 'keyDown', key: 'F11' })
|
||||||
|
|
||||||
|
win.webContents.sent.length = 0
|
||||||
|
pressF11()
|
||||||
|
assert.equal(prevented, 1)
|
||||||
|
assert.equal(win.isFullScreen(), true)
|
||||||
|
assert.deepEqual(win.webContents.sent, [['window:fullscreen-changed', true]])
|
||||||
|
|
||||||
|
win.webContents.sent.length = 0
|
||||||
|
pressF11()
|
||||||
|
assert.equal(win.isFullScreen(), false)
|
||||||
|
assert.deepEqual(win.webContents.sent, [['window:fullscreen-changed', false]])
|
||||||
|
|
||||||
|
// 切换瞬间回读可能拿到旧值:enter/leave 必须下发事件本身的确定值
|
||||||
|
win.webContents.sent.length = 0
|
||||||
|
win.staleFullScreen = false
|
||||||
|
win.emit('enter-full-screen')
|
||||||
|
assert.deepEqual(win.webContents.sent, [['window:fullscreen-changed', true]])
|
||||||
|
|
||||||
|
win.webContents.sent.length = 0
|
||||||
|
win.staleFullScreen = true
|
||||||
|
win.emit('leave-full-screen')
|
||||||
|
assert.deepEqual(win.webContents.sent, [['window:fullscreen-changed', false]])
|
||||||
|
win.staleFullScreen = undefined
|
||||||
|
|
||||||
|
// 加载完成那次窗口状态已经稳定,继续用「读一次」
|
||||||
|
win.webContents.sent.length = 0
|
||||||
|
win.fullScreen = true
|
||||||
|
win.webContents.emit('did-finish-load')
|
||||||
|
assert.deepEqual(win.webContents.sent, [['window:fullscreen-changed', true]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('kiosk 窗口不挂 F11 处理', (t) => {
|
||||||
|
const manager = makeManager(t)
|
||||||
|
fakeScreen.displays = [fakeDisplay(1, { internal: true }), fakeDisplay(2)]
|
||||||
|
|
||||||
|
manager.enable()
|
||||||
|
|
||||||
|
assert.equal(manager.windows.get(2).win.webContents.listeners('before-input-event').length, 0)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user