fix(desktop): update from the splash only after the user clicks (#3280)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 17:25:34 +02:00
1 parent f1373a63c3
commit 7937d5f802
90 files changed
+6888 -11255

No files matched your search

@@ -0,0 +1,197 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {describe, test} from 'node:test';
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
installElectronStub();
const {DesktopUpdateRun} = await import('@electron/main/DesktopUpdateRun');
const LAUNCH_ATTEMPT = Object.freeze({committed: {fluxer_renderer: 'b'.repeat(64)}});
function harness({
probes = [{shellLatestVersion: '2026.1008.2', shellNewer: false, modulesChanged: true}],
canSelfUpdateShell = true,
shellResults = [],
installFails = false,
settled = true,
} = {}) {
const steps = [];
const published = [];
let probeIndex = 0;
let shellIndex = 0;
const quiet = () => undefined;
const run = new DesktopUpdateRun({
probe: async () => {
const next = probes[Math.min(probeIndex, probes.length - 1)];
probeIndex += 1;
steps.push('probe');
if (next instanceof Error) throw next;
return next;
},
canSelfUpdateShell,
runShellSelfUpdate: async () => {
steps.push('shell-update');
const result = shellResults[Math.min(shellIndex, shellResults.length - 1)];
shellIndex += 1;
return result;
},
installModules: async () => {
steps.push('install');
if (installFails) throw new Error('disk full');
return LAUNCH_ATTEMPT;
},
takeover: {
begin: async () => {
steps.push('takeover');
},
restore: () => {
steps.push('restore');
},
closeApp: async () => {
steps.push('close');
},
reopen: (launchAttempt) => {
steps.push(launchAttempt == null ? 'reopen' : 'reopen-updated');
},
},
publish: (check) => {
published.push(check);
},
openDownloadsPage: async () => {
steps.push('downloads-page');
},
logger: {info: quiet, warn: quiet, error: quiet},
});
if (settled) run.markLaunchSettled();
return {run, steps, published};
}
describe('DesktopUpdateRun', () => {
test('the background check only looks, it never downloads, closes or installs anything', async () => {
const {run, steps, published} = harness();
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: true});
assert.deepEqual(steps, ['probe']);
assert.deepEqual(published, []);
});
test('a click takes the window over, checks, and only then closes the app and installs', async () => {
const {run, steps, published} = harness();
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'install', 'reopen-updated']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
});
test('a click while offline gives the same windows back instead of tearing the app down', async () => {
const {run, steps, published} = harness({probes: [new Error('offline')]});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'restore']);
assert.deepEqual(published, [], 'the icon stays so the next click can try again');
});
test('a click after the update was withdrawn gives the windows back and hides the icon', async () => {
const {run, steps, published} = harness({
probes: [{shellLatestVersion: '2026.1008.1', shellNewer: false, modulesChanged: false}],
});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'restore']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
});
test('a shell update runs before any module download, and modules install only when the shell did not restart', async () => {
const {run, steps} = harness({
probes: [{shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: true}],
shellResults: [{reason: 'install-failed', detail: 'nope'}],
});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'shell-update', 'install', 'reopen-updated']);
});
test('a shell feed that lags the manifest stops offering the shell until the manifest moves on', async () => {
const lagging = {shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false};
const {run, steps, published} = harness({
probes: [lagging, lagging, {...lagging, shellLatestVersion: '2026.1008.3'}],
shellResults: [{reason: 'no-update', detail: null}],
});
await run.start();
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
assert.deepEqual(
await run.check(),
{shellNewer: false, modulesChanged: false},
'the poll no longer shows the icon',
);
assert.deepEqual(await run.check(), {shellNewer: true, modulesChanged: false}, 'a newer manifest shell does');
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'shell-update', 'reopen', 'probe', 'probe']);
});
test('a shell update that failed on the network is retried by the next click', async () => {
const shell = {shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false};
const {run, steps, published} = harness({
probes: [shell],
shellResults: [
{reason: 'timed-out', detail: 'slow link'},
{reason: 'check-failed', detail: 'offline'},
],
});
await run.start();
await run.start();
assert.deepEqual(published, [
{shellNewer: true, modulesChanged: false},
{shellNewer: true, modulesChanged: false},
]);
assert.equal(steps.filter((step) => step === 'shell-update').length, 2);
assert.equal(steps.includes('downloads-page'), false);
});
test('only a shell update that failed to install falls back to the downloads page', async () => {
const shell = {shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false};
const {run, steps} = harness({probes: [shell], shellResults: [{reason: 'install-failed', detail: 'apply'}]});
await run.start();
steps.length = 0;
await run.start();
assert.deepEqual(steps, ['downloads-page']);
});
test('a shell that cannot update itself never offers the shell part', async () => {
const {run} = harness({
canSelfUpdateShell: false,
probes: [{shellLatestVersion: '2026.1008.2', shellNewer: true, modulesChanged: false}],
});
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: false});
});
test('a failed module install reopens the app on the installed set and keeps the icon', async () => {
const {run, steps, published} = harness({installFails: true});
await run.start();
assert.deepEqual(steps, ['probe', 'takeover', 'close', 'install', 'reopen']);
assert.deepEqual(published, [{shellNewer: false, modulesChanged: true}]);
});
test('a click before the renderer confirmed its launch does nothing', async () => {
const {run, steps} = harness({settled: false});
await run.start();
assert.deepEqual(steps, []);
});
});