Files
Scoreboard/console/main/displays.test.js
T
AskaEth 793d2ac064 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 例全过。
2026-09-11 00:13:09 +08:00

336 lines
10 KiB
JavaScript

'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)
})