mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
fix(desktop): update from the splash only after the user clicks (#3280)
This commit is contained in:
1 parent
f1373a63c3
commit
7937d5f802
90 files changed
+6888
-11255
No files matched your search
@@ -0,0 +1,252 @@
|
||||
// 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\(\)\) \{/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user