421 lines
13 KiB
TypeScript
421 lines
13 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 AionUi (aionui.com)
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
initialUpdateNotificationState,
|
|
type UpdateNotificationState,
|
|
updateNotificationCapabilities,
|
|
updateNotificationReducer,
|
|
} from '@/renderer/components/settings/updateNotificationState';
|
|
|
|
describe('updateNotificationReducer', () => {
|
|
it('opens the notification when auto-update reports an available version', () => {
|
|
const result = updateNotificationReducer(initialUpdateNotificationState, {
|
|
type: 'autoStatusAvailable',
|
|
version: '2.2.0',
|
|
currentVersion: '2.1.9',
|
|
releaseNotes: 'release notes',
|
|
});
|
|
|
|
expect(result.state.visible).toBe(true);
|
|
expect(result.state.status).toBe('available');
|
|
expect(result.state.autoUpdateAvailable).toBe(true);
|
|
expect(result.state.currentVersion).toBe('2.1.9');
|
|
expect(result.state.autoUpdateInfo).toEqual({
|
|
version: '2.2.0',
|
|
releaseNotes: 'release notes',
|
|
});
|
|
expect(result.effects).toEqual([{ type: 'loadManualReleaseInfoForDisplay' }]);
|
|
});
|
|
|
|
it('preserves the active download when an entry opens during downloading', () => {
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: false,
|
|
status: 'downloading',
|
|
activeTask: {
|
|
kind: 'manual',
|
|
id: 'manual-1',
|
|
},
|
|
progress: {
|
|
percent: 42,
|
|
transferred: 42,
|
|
total: 100,
|
|
speed: '1.0 MB/s',
|
|
},
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadingState, {
|
|
type: 'openRequested',
|
|
source: 'about',
|
|
userInitiated: true,
|
|
});
|
|
|
|
expect(result.state.visible).toBe(true);
|
|
expect(result.state.status).toBe('downloading');
|
|
expect(result.state.activeTask).toEqual({
|
|
kind: 'manual',
|
|
id: 'manual-1',
|
|
});
|
|
expect(result.state.progress).toEqual({
|
|
percent: 42,
|
|
transferred: 42,
|
|
total: 100,
|
|
speed: '1.0 MB/s',
|
|
});
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('hides the notification on later without clearing the active download', () => {
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloading',
|
|
activeTask: {
|
|
kind: 'auto',
|
|
id: 'auto',
|
|
},
|
|
progress: {
|
|
percent: 64,
|
|
transferred: 64,
|
|
total: 100,
|
|
speed: '2.0 MB/s',
|
|
},
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadingState, {
|
|
type: 'dismissRequested',
|
|
reason: 'later',
|
|
});
|
|
|
|
expect(result.state.visible).toBe(false);
|
|
expect(result.state.status).toBe('downloading');
|
|
expect(result.state.activeTask).toEqual({
|
|
kind: 'auto',
|
|
id: 'auto',
|
|
});
|
|
expect(result.state.progress.percent).toBe(64);
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('cancels an active download and restores the available update state', () => {
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloading',
|
|
currentVersion: '2.1.13',
|
|
autoUpdateAvailable: true,
|
|
autoUpdateInfo: {
|
|
version: '2.1.14',
|
|
releaseNotes: 'notes',
|
|
},
|
|
activeTask: {
|
|
kind: 'auto',
|
|
id: 'auto',
|
|
},
|
|
progress: {
|
|
percent: 58,
|
|
transferred: 58,
|
|
total: 100,
|
|
speed: '1.0 MB/s',
|
|
},
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadingState, {
|
|
type: 'cancelDownloadRequested',
|
|
} as never);
|
|
|
|
expect(result.state.visible).toBe(true);
|
|
expect(result.state.status).toBe('available');
|
|
expect(result.state.activeTask).toBeNull();
|
|
expect(result.state.progress.percent).toBe(0);
|
|
expect(result.state.autoUpdateInfo?.version).toBe('2.1.14');
|
|
expect(result.effects).toEqual([{ type: 'cancelDownload', task: { kind: 'auto', id: 'auto' } }]);
|
|
});
|
|
|
|
it('minimizes and restores the active download without changing progress owner', () => {
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloading',
|
|
activeTask: {
|
|
kind: 'manual',
|
|
id: 'manual-1',
|
|
},
|
|
progress: {
|
|
percent: 33,
|
|
transferred: 33,
|
|
total: 100,
|
|
speed: '1.0 MB/s',
|
|
},
|
|
};
|
|
|
|
const minimized = updateNotificationReducer(downloadingState, {
|
|
type: 'minimizeRequested',
|
|
} as never);
|
|
|
|
expect(minimized.state.visible).toBe(true);
|
|
expect((minimized.state as UpdateNotificationState & { presentation: string }).presentation).toBe('mini');
|
|
expect(minimized.state.activeTask).toEqual({ kind: 'manual', id: 'manual-1' });
|
|
expect(minimized.state.progress.percent).toBe(33);
|
|
|
|
const restored = updateNotificationReducer(minimized.state, {
|
|
type: 'restoreRequested',
|
|
} as never);
|
|
|
|
expect((restored.state as UpdateNotificationState & { presentation: string }).presentation).toBe('card');
|
|
expect(restored.state.activeTask).toEqual({ kind: 'manual', id: 'manual-1' });
|
|
expect(restored.state.progress.percent).toBe(33);
|
|
});
|
|
|
|
it('keeps a 100 percent progress snapshot when auto download completes', () => {
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloading',
|
|
activeTask: {
|
|
kind: 'auto',
|
|
id: 'auto',
|
|
},
|
|
progress: {
|
|
percent: 74,
|
|
transferred: 74,
|
|
total: 100,
|
|
speed: '2.0 MB/s',
|
|
},
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadingState, {
|
|
type: 'autoDownloaded',
|
|
});
|
|
|
|
expect(result.state.status).toBe('downloaded');
|
|
expect(result.state.activeTask).toBeNull();
|
|
expect(result.state.progress.percent).toBe(100);
|
|
});
|
|
|
|
it('restores a completed auto-update without requiring an active download owner', () => {
|
|
const result = updateNotificationReducer(initialUpdateNotificationState, {
|
|
type: 'autoDownloadedRestored',
|
|
version: '2.1.14',
|
|
currentVersion: '2.1.13',
|
|
} as never);
|
|
|
|
expect(result.state.visible).toBe(true);
|
|
expect(result.state.status).toBe('downloaded');
|
|
expect(result.state.autoUpdateAvailable).toBe(true);
|
|
expect(result.state.currentVersion).toBe('2.1.13');
|
|
expect(result.state.autoUpdateInfo).toEqual({ version: '2.1.14' });
|
|
expect(result.state.activeTask).toBeNull();
|
|
expect(result.state.presentation).toBe('card');
|
|
expect(result.state.progress.percent).toBe(100);
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('enters preparing-install after an auto-update install request', () => {
|
|
const downloadedState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloaded',
|
|
autoUpdateAvailable: true,
|
|
autoUpdateInfo: { version: '2.2.0' },
|
|
progress: {
|
|
percent: 100,
|
|
transferred: 100,
|
|
total: 100,
|
|
speed: '',
|
|
},
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadedState, {
|
|
type: 'autoPreparingInstall',
|
|
version: '2.2.0',
|
|
} as never);
|
|
|
|
expect(result.state.visible).toBe(true);
|
|
expect(result.state.status).toBe('preparing-install');
|
|
expect(result.state.autoUpdateInfo?.version).toBe('2.2.0');
|
|
expect(result.state.progress.percent).toBe(100);
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('shows an error when preparing-install fails before app exit', () => {
|
|
const preparingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'preparing-install',
|
|
autoUpdateAvailable: true,
|
|
autoUpdateInfo: { version: '2.2.0' },
|
|
};
|
|
|
|
const result = updateNotificationReducer(preparingState, {
|
|
type: 'autoError',
|
|
message: 'Preparing installation timed out. Please try again later.',
|
|
});
|
|
|
|
expect(result.state.status).toBe('error');
|
|
expect(result.state.activeTask).toBeNull();
|
|
expect(result.state.errorMsg).toBe('Preparing installation timed out. Please try again later.');
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('shows an error when native readiness fails after auto download completes', () => {
|
|
const downloadedState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloaded',
|
|
autoUpdateAvailable: true,
|
|
autoUpdateInfo: { version: '2.2.0' },
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadedState, {
|
|
type: 'autoError',
|
|
message: 'Preparing installation failed. Please try again later.',
|
|
});
|
|
|
|
expect(result.state.status).toBe('error');
|
|
expect(result.state.activeTask).toBeNull();
|
|
expect(result.state.errorMsg).toBe('Preparing installation failed. Please try again later.');
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('ignores stale manual progress from a different download id', () => {
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloading',
|
|
activeTask: {
|
|
kind: 'manual',
|
|
id: 'current-download',
|
|
},
|
|
progress: {
|
|
percent: 58,
|
|
transferred: 58,
|
|
total: 100,
|
|
speed: '3.0 MB/s',
|
|
},
|
|
};
|
|
|
|
const result = updateNotificationReducer(downloadingState, {
|
|
type: 'manualProgress',
|
|
downloadId: 'stale-download',
|
|
status: 'downloading',
|
|
progress: {
|
|
percent: 99,
|
|
transferred: 99,
|
|
total: 100,
|
|
speed: '9.0 MB/s',
|
|
},
|
|
});
|
|
|
|
expect(result.state.progress).toEqual({
|
|
percent: 58,
|
|
transferred: 58,
|
|
total: 100,
|
|
speed: '3.0 MB/s',
|
|
});
|
|
expect(result.state.status).toBe('downloading');
|
|
expect(result.effects).toEqual([]);
|
|
});
|
|
|
|
it('keeps notification singleton capabilities platform independent', () => {
|
|
expect(updateNotificationCapabilities).toEqual({
|
|
notificationCard: true,
|
|
singletonState: true,
|
|
autoDownloadSingleFlight: true,
|
|
manualDownloadDedupe: true,
|
|
});
|
|
});
|
|
|
|
it('shows a one-shot installer failure marker without clobbering update download state', () => {
|
|
const marker = {
|
|
schemaVersion: 1,
|
|
kind: 'app-cannot-be-closed' as const,
|
|
phase: 'customCheckAppRunning' as const,
|
|
silent: true,
|
|
updated: true,
|
|
retryCount: 3,
|
|
instDir: 'D:\\AionUi',
|
|
logPath: 'C:\\Users\\me\\AppData\\Local\\Temp\\aionui-installer-2.1.27-20260702-151830-ab12cd34ef56.log',
|
|
at: '2026-07-01T00:00:00.000Z',
|
|
};
|
|
|
|
const result = updateNotificationReducer(initialUpdateNotificationState, {
|
|
type: 'installerLastFailureConsumed',
|
|
marker,
|
|
} as never);
|
|
|
|
expect(result.state.visible).toBe(true);
|
|
expect(result.state.status).toBe('installer-last-failure');
|
|
expect(result.state.installerLastFailure).toEqual(marker);
|
|
expect(result.state.activeTask).toBeNull();
|
|
expect(result.effects).toEqual([]);
|
|
|
|
const downloadingState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloading',
|
|
activeTask: { kind: 'auto', id: 'auto' },
|
|
};
|
|
const ignored = updateNotificationReducer(downloadingState, {
|
|
type: 'installerLastFailureConsumed',
|
|
marker,
|
|
} as never);
|
|
|
|
expect(ignored.state.status).toBe('downloading');
|
|
expect(ignored.state.installerLastFailure).toBeUndefined();
|
|
});
|
|
|
|
it('queues an installer failure marker during downloaded and preparing states until it can be displayed', () => {
|
|
const marker = {
|
|
schemaVersion: 1,
|
|
kind: 'app-cannot-be-closed' as const,
|
|
phase: 'customCheckAppRunning' as const,
|
|
silent: true,
|
|
updated: true,
|
|
retryCount: 3,
|
|
instDir: 'D:\\AionUi',
|
|
logPath: 'C:\\Users\\me\\AppData\\Local\\Temp\\aionui-installer-2.1.27-20260702-151830-ab12cd34ef56.log',
|
|
at: '2026-07-01T00:00:00.000Z',
|
|
};
|
|
const downloadedState: UpdateNotificationState = {
|
|
...initialUpdateNotificationState,
|
|
visible: true,
|
|
status: 'downloaded',
|
|
autoUpdateAvailable: true,
|
|
autoUpdateInfo: { version: '2.2.0' },
|
|
};
|
|
|
|
const queued = updateNotificationReducer(downloadedState, {
|
|
type: 'installerLastFailureConsumed',
|
|
marker,
|
|
} as never);
|
|
|
|
expect(queued.state.status).toBe('downloaded');
|
|
expect(queued.state.installerLastFailure).toBeUndefined();
|
|
expect(queued.state.pendingInstallerLastFailure).toEqual(marker);
|
|
|
|
const preparing = updateNotificationReducer(queued.state, {
|
|
type: 'autoPreparingInstall',
|
|
version: '2.2.0',
|
|
} as never);
|
|
|
|
expect(preparing.state.status).toBe('preparing-install');
|
|
expect(preparing.state.pendingInstallerLastFailure).toEqual(marker);
|
|
|
|
const displayed = updateNotificationReducer(preparing.state, {
|
|
type: 'autoError',
|
|
message: 'Preparing installation failed. Please try again later.',
|
|
});
|
|
|
|
expect(displayed.state.visible).toBe(true);
|
|
expect(displayed.state.status).toBe('installer-last-failure');
|
|
expect(displayed.state.installerLastFailure).toEqual(marker);
|
|
expect(displayed.state.pendingInstallerLastFailure).toBeUndefined();
|
|
});
|
|
});
|