fix(updater): tighten manual release fallback

This commit is contained in:
4gray committed 2026-06-28 17:15:39 +02:00
1 parent 93498182d5
commit 3db409cb5b
4 files changed
+100 -8

No files matched your search

@@ -176,6 +176,37 @@ describe('AppUpdateService', () => {
});
});
it('loads additional GitHub release pages when the first page has no stable releases', async () => {
const prereleasePage = Array.from({ length: 10 }, (_, index) => ({
...githubReleases[2],
name: `v0.25.${index}-beta`,
tag_name: `v0.25.${index}-beta`,
}));
const fetcher = createReleaseFetcher([
prereleasePage,
[githubReleases[0]],
]);
const { service } = createService({
env: {},
fetcher,
platform: 'linux',
});
await service.checkForUpdates();
expect(fetcher).toHaveBeenCalledTimes(2);
expect(
fetcher.mock.calls.map(([url]) =>
new URL(url).searchParams.get('page')
)
).toEqual(['1', '2']);
expect(service.getStatus()).toMatchObject({
latestVersion: '0.24.0',
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Available,
supportedSelfUpdate: false,
});
});
it('starts a packaged startup check without downloading automatically', async () => {
const { service, updater } = createService();
@@ -439,7 +439,7 @@ export class AppUpdateService {
}
private async checkGitHubReleaseForManualUpdate(): Promise<void> {
await this.ensureReleasePageLoaded(1);
await this.ensureFirstStableReleaseLoaded();
const latestRelease = this.releases[0];
if (!latestRelease) {
@@ -467,6 +467,12 @@ export class AppUpdateService {
});
}
private async ensureFirstStableReleaseLoaded(): Promise<void> {
while (!this.loadedAllReleases && this.releases.length === 0) {
await this.ensureReleasePageLoaded(this.loadedReleasePages + 1);
}
}
private async findReleaseIndex(
version: string | undefined
): Promise<number> {
@@ -65,7 +65,9 @@ describe('AppUpdateNotificationPanelComponent', () => {
fixture.detectChanges();
expect(
fixture.nativeElement.querySelector('[data-test-id="app-update-notification"]')
fixture.nativeElement.querySelector(
'[data-test-id="app-update-notification"]'
)
).not.toBeNull();
});
@@ -86,7 +88,9 @@ describe('AppUpdateNotificationPanelComponent', () => {
})
);
expect(
fixture.nativeElement.querySelector('[data-test-id="app-update-notification"]')
fixture.nativeElement.querySelector(
'[data-test-id="app-update-notification"]'
)
).not.toBeNull();
});
@@ -103,4 +107,34 @@ describe('AppUpdateNotificationPanelComponent', () => {
expect(window.electron.downloadAppUpdate).toHaveBeenCalledTimes(1);
});
it('labels unsupported platform action as a GitHub release link', () => {
const open = jest.spyOn(window, 'open').mockImplementation(() => null);
statusHandler?.({
...availableStatus,
supportedSelfUpdate: false,
});
fixture.detectChanges();
const button = fixture.nativeElement.querySelector(
'[data-test-id="app-update-notification-download"]'
) as HTMLButtonElement;
expect(button.textContent).toContain(
'SETTINGS.APP_UPDATE_OPEN_RELEASE'
);
expect(button.querySelector('mat-icon')?.textContent?.trim()).toBe(
'open_in_new'
);
button.click();
expect(window.electron.downloadAppUpdate).not.toHaveBeenCalled();
expect(open).toHaveBeenCalledWith(
availableStatus.manualDownloadUrl,
'_blank',
'noreferrer'
);
open.mockRestore();
});
});
@@ -1,4 +1,11 @@
import { Component, computed, OnDestroy, OnInit, signal, inject } from '@angular/core';
import {
Component,
computed,
OnDestroy,
OnInit,
signal,
inject,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@@ -46,7 +53,10 @@ import { AppUpdateReleaseNotesDialogComponent } from './settings/app-update-rele
}}
</strong>
@if (status()?.progress; as progress) {
<progress [value]="progress.percent" max="100"></progress>
<progress
[value]="progress.percent"
max="100"
></progress>
}
</div>
@@ -76,13 +86,14 @@ import { AppUpdateReleaseNotesDialogComponent } from './settings/app-update-rele
mat-flat-button
type="button"
[disabled]="
status()?.status === appUpdateStatuses.Downloading
status()?.status ===
appUpdateStatuses.Downloading
"
(click)="downloadUpdate()"
data-test-id="app-update-notification-download"
>
<mat-icon>download</mat-icon>
{{ 'SETTINGS.APP_UPDATE_DOWNLOAD' | translate }}
<mat-icon>{{ primaryActionIcon() }}</mat-icon>
{{ primaryActionLabelKey() | translate }}
</button>
}
</div>
@@ -175,6 +186,16 @@ export class AppUpdateNotificationPanelComponent implements OnInit, OnDestroy {
? 'SETTINGS.APP_UPDATE_DOWNLOADED'
: 'SETTINGS.APP_UPDATE_AVAILABLE'
);
readonly primaryActionLabelKey = computed(() =>
this.status()?.supportedSelfUpdate === false
? 'SETTINGS.APP_UPDATE_OPEN_RELEASE'
: 'SETTINGS.APP_UPDATE_DOWNLOAD'
);
readonly primaryActionIcon = computed(() =>
this.status()?.supportedSelfUpdate === false
? 'open_in_new'
: 'download'
);
readonly isVisible = computed(() => {
const status = this.status();