mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
300 lines
10 KiB
JavaScript
300 lines
10 KiB
JavaScript
// 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,
|
|
now = () => 0,
|
|
} = {}) {
|
|
const steps = [];
|
|
const published = [];
|
|
const failures = [];
|
|
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');
|
|
},
|
|
reloadInPlace: async () => {
|
|
steps.push('reload-in-place');
|
|
},
|
|
},
|
|
publish: (check) => {
|
|
published.push(check);
|
|
},
|
|
openDownloadsPage: async () => {
|
|
steps.push('downloads-page');
|
|
},
|
|
reportFailure: (failure) => {
|
|
failures.push(failure);
|
|
},
|
|
logger: {info: quiet, warn: quiet, error: quiet},
|
|
now,
|
|
});
|
|
if (settled) run.markLaunchSettled();
|
|
return {run, steps, published, failures};
|
|
}
|
|
|
|
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 manual check offers the shell again even after the platform feed said it had nothing', async () => {
|
|
const lagging = {shellLatestVersion: '2026.1012.154032', shellNewer: true, modulesChanged: false};
|
|
const {run} = harness({probes: [lagging], shellResults: [{reason: 'no-update', detail: null}]});
|
|
await run.start();
|
|
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: false});
|
|
|
|
assert.deepEqual(
|
|
await run.check({manual: true}),
|
|
{shellNewer: true, modulesChanged: false},
|
|
'one stale answer from the platform feed must not make Check for updates say up to date for the rest of the session',
|
|
);
|
|
assert.deepEqual(await run.check(), {shellNewer: true, modulesChanged: false});
|
|
});
|
|
|
|
test('the background check offers a lagging shell again once the platform feed had time to catch up', async () => {
|
|
let clock = 0;
|
|
const lagging = {shellLatestVersion: '2026.1012.154032', shellNewer: true, modulesChanged: false};
|
|
const {run} = harness({
|
|
probes: [lagging],
|
|
shellResults: [{reason: 'no-update', detail: null}],
|
|
now: () => clock,
|
|
});
|
|
await run.start();
|
|
|
|
clock = 9 * 60_000;
|
|
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: false});
|
|
clock = 10 * 60_000;
|
|
assert.deepEqual(await run.check(), {shellNewer: true, modulesChanged: false});
|
|
});
|
|
|
|
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, []);
|
|
});
|
|
});
|
|
|
|
describe('DesktopUpdateRun installing a renderer only update in place', () => {
|
|
const MODULES_ONLY = {shellLatestVersion: '2026.1008.2', shellNewer: false, modulesChanged: true};
|
|
const SHELL_TOO = {shellLatestVersion: '2026.1008.3', shellNewer: true, modulesChanged: true};
|
|
|
|
test('a click after the background check found only modules never shows the splash or closes a window', async () => {
|
|
const {run, steps, published, failures} = harness({probes: [MODULES_ONLY]});
|
|
await run.check();
|
|
|
|
await run.start();
|
|
|
|
assert.deepEqual(steps, ['probe', 'probe', 'install', 'reload-in-place']);
|
|
assert.deepEqual(published, [{shellNewer: false, modulesChanged: false}]);
|
|
assert.deepEqual(failures, []);
|
|
});
|
|
|
|
test('a shell that turned newer since the background check still updates behind the splash', async () => {
|
|
const {run, steps} = harness({
|
|
probes: [MODULES_ONLY, SHELL_TOO],
|
|
shellResults: [{reason: 'no-update', detail: null}],
|
|
});
|
|
await run.check();
|
|
|
|
await run.start();
|
|
|
|
assert.deepEqual(steps, ['probe', 'probe', 'takeover', 'close', 'shell-update', 'install', 'reopen-updated']);
|
|
});
|
|
|
|
test('a download that fails leaves the running app alone and says so', async () => {
|
|
const {run, steps, published, failures} = harness({probes: [MODULES_ONLY], installFails: true});
|
|
await run.check();
|
|
|
|
await run.start();
|
|
|
|
assert.deepEqual(steps, ['probe', 'probe', 'install']);
|
|
assert.deepEqual(published, [{shellNewer: false, modulesChanged: true}]);
|
|
assert.deepEqual(failures, [{reason: 'download-failed', detail: 'disk full'}]);
|
|
});
|
|
|
|
test('a click while offline changes nothing on screen', async () => {
|
|
const {run, steps, failures} = harness({probes: [MODULES_ONLY, new Error('offline')]});
|
|
await run.check();
|
|
|
|
await run.start();
|
|
|
|
assert.deepEqual(steps, ['probe', 'probe']);
|
|
assert.deepEqual(failures, [{reason: 'check-failed', detail: 'offline'}]);
|
|
});
|
|
|
|
test('a check works from the first window, a click only after the renderer confirmed its launch', async () => {
|
|
const {run, steps} = harness({probes: [MODULES_ONLY], settled: false});
|
|
|
|
assert.deepEqual(await run.check(), {shellNewer: false, modulesChanged: true});
|
|
await run.start();
|
|
|
|
assert.deepEqual(steps, ['probe']);
|
|
|
|
run.markLaunchSettled();
|
|
await run.start();
|
|
|
|
assert.deepEqual(steps, ['probe', 'probe', 'install', 'reload-in-place']);
|
|
});
|
|
});
|