diff --git a/apps/electron-backend/src/app/services/app-update.service.spec.ts b/apps/electron-backend/src/app/services/app-update.service.spec.ts index 5266d8a62..f32405542 100644 --- a/apps/electron-backend/src/app/services/app-update.service.spec.ts +++ b/apps/electron-backend/src/app/services/app-update.service.spec.ts @@ -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(); diff --git a/apps/electron-backend/src/app/services/app-update.service.ts b/apps/electron-backend/src/app/services/app-update.service.ts index 5912b808e..ffb628578 100644 --- a/apps/electron-backend/src/app/services/app-update.service.ts +++ b/apps/electron-backend/src/app/services/app-update.service.ts @@ -439,7 +439,7 @@ export class AppUpdateService { } private async checkGitHubReleaseForManualUpdate(): Promise { - 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 { + while (!this.loadedAllReleases && this.releases.length === 0) { + await this.ensureReleasePageLoaded(this.loadedReleasePages + 1); + } + } + private async findReleaseIndex( version: string | undefined ): Promise { diff --git a/apps/web/src/app/app-update-notification-panel.component.spec.ts b/apps/web/src/app/app-update-notification-panel.component.spec.ts index 17bbbd77a..110f07e51 100644 --- a/apps/web/src/app/app-update-notification-panel.component.spec.ts +++ b/apps/web/src/app/app-update-notification-panel.component.spec.ts @@ -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(); + }); }); diff --git a/apps/web/src/app/app-update-notification-panel.component.ts b/apps/web/src/app/app-update-notification-panel.component.ts index 55aae8973..a597e6d94 100644 --- a/apps/web/src/app/app-update-notification-panel.component.ts +++ b/apps/web/src/app/app-update-notification-panel.component.ts @@ -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 }} @if (status()?.progress; as progress) { - + } @@ -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" > - download - {{ 'SETTINGS.APP_UPDATE_DOWNLOAD' | translate }} + {{ primaryActionIcon() }} + {{ primaryActionLabelKey() | translate }} } @@ -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();