mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
253 lines
6.5 KiB
JavaScript
253 lines
6.5 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {readFileSync} from 'node:fs';
|
|
import {describe, test} from 'node:test';
|
|
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
|
|
|
|
const openWindows = [];
|
|
|
|
class FakeWindow {
|
|
constructor(name, {vetoUnload = false, closable = true} = {}) {
|
|
this.name = name;
|
|
this.vetoUnload = vetoUnload;
|
|
this.closable = closable;
|
|
this.visible = true;
|
|
this.destroyed = false;
|
|
this.listeners = new Map();
|
|
this.unloadListeners = [];
|
|
this.webContents = {
|
|
once: (event, listener) => {
|
|
if (event === 'will-prevent-unload') this.unloadListeners.push(listener);
|
|
},
|
|
};
|
|
openWindows.push(this);
|
|
}
|
|
|
|
once(event, listener) {
|
|
this.listeners.set(event, listener);
|
|
}
|
|
|
|
isDestroyed() {
|
|
return this.destroyed;
|
|
}
|
|
|
|
isVisible() {
|
|
return this.visible;
|
|
}
|
|
|
|
isClosable() {
|
|
return this.closable;
|
|
}
|
|
|
|
hide() {
|
|
this.visible = false;
|
|
}
|
|
|
|
show() {
|
|
this.visible = true;
|
|
}
|
|
|
|
close() {
|
|
if (!this.closable) return;
|
|
if (this.vetoUnload) {
|
|
let prevented = false;
|
|
for (const listener of this.unloadListeners) {
|
|
listener({preventDefault: () => (prevented = true)});
|
|
}
|
|
if (!prevented) return;
|
|
}
|
|
this.destroy();
|
|
}
|
|
|
|
destroy() {
|
|
this.destroyed = true;
|
|
this.visible = false;
|
|
this.listeners.get('closed')?.();
|
|
}
|
|
}
|
|
|
|
const noop = () => undefined;
|
|
const ipcHandlers = new Map();
|
|
|
|
class ElectronEmitterStub {
|
|
on() {
|
|
return this;
|
|
}
|
|
|
|
once() {
|
|
return this;
|
|
}
|
|
|
|
off() {
|
|
return this;
|
|
}
|
|
|
|
removeListener() {
|
|
return this;
|
|
}
|
|
|
|
removeAllListeners() {
|
|
return this;
|
|
}
|
|
|
|
emit() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
installElectronStub({
|
|
app: Object.assign(new ElectronEmitterStub(), {
|
|
isReady: () => true,
|
|
isPackaged: false,
|
|
getPath: () => '/tmp/fluxer-desktop-test',
|
|
getAppPath: () => '/tmp/fluxer-desktop-test',
|
|
getVersion: () => '0.0.0',
|
|
getLocale: () => 'en-US',
|
|
setPath: noop,
|
|
exit: noop,
|
|
quit: noop,
|
|
whenReady: () => Promise.resolve(),
|
|
commandLine: {appendSwitch: noop, appendArgument: noop},
|
|
}),
|
|
BrowserWindow: class extends ElectronEmitterStub {
|
|
static fromWebContents() {
|
|
return null;
|
|
}
|
|
|
|
static getAllWindows() {
|
|
return openWindows.filter((window) => !window.isDestroyed());
|
|
}
|
|
|
|
static getFocusedWindow() {
|
|
return null;
|
|
}
|
|
},
|
|
Menu: {buildFromTemplate: () => ({}), setApplicationMenu: noop},
|
|
Notification: Object.assign(class extends ElectronEmitterStub {}, {isSupported: () => false}),
|
|
clipboard: {},
|
|
desktopCapturer: {},
|
|
dialog: {showMessageBoxSync: () => 0, showErrorBox: noop},
|
|
globalShortcut: {},
|
|
ipcMain: Object.assign(new ElectronEmitterStub(), {
|
|
handle: (channel, handler) => {
|
|
ipcHandlers.set(channel, handler);
|
|
},
|
|
removeHandler: noop,
|
|
}),
|
|
nativeImage: {createFromPath: () => ({isEmpty: () => true})},
|
|
nativeTheme: new ElectronEmitterStub(),
|
|
net: {fetch: () => Promise.reject(new Error('outbound network is not available in tests'))},
|
|
powerMonitor: new ElectronEmitterStub(),
|
|
powerSaveBlocker: {start: () => 0, stop: noop, isStarted: () => false},
|
|
protocol: {registerSchemesAsPrivileged: noop, handle: noop, unhandle: noop, isProtocolHandled: () => false},
|
|
safeStorage: {isEncryptionAvailable: () => false},
|
|
screen: Object.assign(new ElectronEmitterStub(), {
|
|
getPrimaryDisplay: () => ({workAreaSize: {width: 1920, height: 1080}}),
|
|
getAllDisplays: () => [],
|
|
}),
|
|
session: {defaultSession: {}},
|
|
shell: {openExternal: noop, showItemInFolder: noop},
|
|
systemPreferences: {},
|
|
});
|
|
|
|
const {
|
|
beginMainWindowTakeover,
|
|
closeAppWindowsForUpdate,
|
|
endMainWindowTakeover,
|
|
hideAppWindowsForUpdate,
|
|
onMainWindowTakeoverEnded,
|
|
restoreAppWindowsAfterUpdate,
|
|
isMainWindowTakenOver,
|
|
showWindow,
|
|
} = await import('@electron/main/Window');
|
|
|
|
describe('the update takes the app windows over', () => {
|
|
test('the windows hide at once and only the splash stays on screen', () => {
|
|
openWindows.length = 0;
|
|
const splash = new FakeWindow('splash');
|
|
const main = new FakeWindow('main');
|
|
const popout = new FakeWindow('voice popout');
|
|
|
|
hideAppWindowsForUpdate(splash);
|
|
|
|
assert.equal(splash.isVisible(), true);
|
|
assert.equal(main.isVisible(), false);
|
|
assert.equal(popout.isVisible(), false);
|
|
});
|
|
|
|
test('a check that finds nothing gives back exactly the windows it hid', () => {
|
|
openWindows.length = 0;
|
|
const splash = new FakeWindow('splash');
|
|
const main = new FakeWindow('main');
|
|
const trayHidden = new FakeWindow('hidden popout');
|
|
trayHidden.hide();
|
|
|
|
const hidden = hideAppWindowsForUpdate(splash);
|
|
restoreAppWindowsAfterUpdate(hidden);
|
|
|
|
assert.deepEqual(hidden, [main]);
|
|
assert.equal(main.isVisible(), true);
|
|
assert.equal(trayHidden.isVisible(), false, 'a window the user had hidden stays hidden');
|
|
});
|
|
|
|
test('a window that cannot be closed is destroyed at once instead of holding the update back', async () => {
|
|
openWindows.length = 0;
|
|
const splash = new FakeWindow('splash');
|
|
const guard = new FakeWindow('screen capture guard', {closable: false});
|
|
const startedAt = Date.now();
|
|
|
|
await closeAppWindowsForUpdate(splash);
|
|
|
|
assert.equal(guard.isDestroyed(), true);
|
|
assert.ok(Date.now() - startedAt < 1000, 'the 5 second close deadline never came into it');
|
|
});
|
|
|
|
test('ending the takeover tells its listeners once', () => {
|
|
let ended = 0;
|
|
const stop = onMainWindowTakeoverEnded(() => {
|
|
ended += 1;
|
|
});
|
|
beginMainWindowTakeover(() => undefined);
|
|
endMainWindowTakeover();
|
|
endMainWindowTakeover();
|
|
stop();
|
|
|
|
assert.equal(ended, 1);
|
|
});
|
|
|
|
test('the windows really close, past a renderer that would veto the unload', async () => {
|
|
openWindows.length = 0;
|
|
const splash = new FakeWindow('splash');
|
|
const main = new FakeWindow('main', {vetoUnload: true});
|
|
const popout = new FakeWindow('theme studio');
|
|
|
|
await closeAppWindowsForUpdate(splash);
|
|
|
|
assert.equal(main.isDestroyed(), true);
|
|
assert.equal(popout.isDestroyed(), true);
|
|
assert.equal(splash.isDestroyed(), false);
|
|
});
|
|
|
|
test('showing the app during the update raises the splash instead of a window that is gone', () => {
|
|
let focused = 0;
|
|
beginMainWindowTakeover(() => {
|
|
focused += 1;
|
|
});
|
|
assert.equal(isMainWindowTakenOver(), true);
|
|
|
|
showWindow();
|
|
|
|
assert.equal(focused, 1);
|
|
endMainWindowTakeover();
|
|
assert.equal(isMainWindowTakenOver(), false);
|
|
showWindow();
|
|
assert.equal(focused, 1);
|
|
});
|
|
|
|
test('close to hide never swallows the close the update asks for', () => {
|
|
const source = readFileSync(new URL('./Window.ts', import.meta.url), 'utf8');
|
|
assert.match(source, /if \(!isQuitting && !closingMainWindowForUpdate && shouldHideMainWindowOnClose\(\)\) \{/);
|
|
});
|
|
});
|