mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(updater): address release review feedback
This commit is contained in:
1 parent
b64e187a5e
commit
44014f6b38
8 files changed
+123
-28
No files matched your search
@@ -476,7 +476,7 @@ jobs:
|
||||
# TEMPORARY MASTER ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') && '1' || '0' }}
|
||||
run: pnpm run make:app
|
||||
run: pnpm run make:app -- --publish never
|
||||
|
||||
- name: Verify signed macOS app
|
||||
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
||||
@@ -535,7 +535,7 @@ jobs:
|
||||
# TEMPORARY PR TEST: change this back to '0' after manually
|
||||
# testing the macOS PR artifact with Embedded MPV included.
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && github.event_name == 'pull_request')) && '1' || '0' }}
|
||||
run: pnpm run make:app
|
||||
run: pnpm run make:app -- --publish never
|
||||
|
||||
- name: Verify packaged worker layout
|
||||
shell: bash
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { contextBridge, ipcRenderer, webUtils } from 'electron';
|
||||
import {
|
||||
APP_UPDATE_CHECK,
|
||||
APP_UPDATE_DOWNLOAD,
|
||||
APP_UPDATE_GET_RELEASE_NOTES,
|
||||
APP_UPDATE_GET_STATUS,
|
||||
APP_UPDATE_INSTALL,
|
||||
APP_UPDATE_STATUS_CHANGED,
|
||||
} from '@iptvnator/shared/interfaces/ipc-commands';
|
||||
import type {
|
||||
EmbeddedMpvBounds,
|
||||
EmbeddedMpvRecordingStartOptions,
|
||||
EmbeddedMpvSession,
|
||||
EmbeddedMpvSupport,
|
||||
ElectronBridgeApi,
|
||||
ElectronBridgeAppUpdateStatus,
|
||||
ElectronBridgeAppUpdateReleaseNotesRequest,
|
||||
ElectronBridgeAppUpdateStatus,
|
||||
ElectronBridgeDbOperationEvent,
|
||||
ElectronBridgeDownloadStartPayload,
|
||||
ElectronBridgeEpgLookupOptions,
|
||||
@@ -49,12 +57,6 @@ const WINDOW_TOGGLE_MAXIMIZE = 'WINDOW:TOGGLE_MAXIMIZE';
|
||||
const WINDOW_CLOSE = 'WINDOW:CLOSE';
|
||||
const WINDOW_GET_STATE = 'WINDOW:GET_STATE';
|
||||
const WINDOW_STATE_CHANGED = 'WINDOW:STATE_CHANGED';
|
||||
const APP_UPDATE_GET_STATUS = 'APP_UPDATE:GET_STATUS';
|
||||
const APP_UPDATE_CHECK = 'APP_UPDATE:CHECK';
|
||||
const APP_UPDATE_DOWNLOAD = 'APP_UPDATE:DOWNLOAD';
|
||||
const APP_UPDATE_INSTALL = 'APP_UPDATE:INSTALL';
|
||||
const APP_UPDATE_GET_RELEASE_NOTES = 'APP_UPDATE:GET_RELEASE_NOTES';
|
||||
const APP_UPDATE_STATUS_CHANGED = 'APP_UPDATE:STATUS_CHANGED';
|
||||
|
||||
const dbSaveContentProgressListeners = new Set<
|
||||
(
|
||||
|
||||
@@ -35,7 +35,8 @@ const githubReleases = [
|
||||
{
|
||||
body: 'beta notes',
|
||||
draft: false,
|
||||
html_url: 'https://github.com/4gray/iptvnator/releases/tag/v0.22.5-beta',
|
||||
html_url:
|
||||
'https://github.com/4gray/iptvnator/releases/tag/v0.22.5-beta',
|
||||
name: 'v0.22.5-beta',
|
||||
prerelease: true,
|
||||
published_at: '2026-06-27T00:00:00.000Z',
|
||||
@@ -106,7 +107,8 @@ describe('AppUpdateService', () => {
|
||||
|
||||
expect(service.getStatus()).toEqual({
|
||||
currentVersion: '0.22.0',
|
||||
manualDownloadUrl: 'https://github.com/4gray/iptvnator/releases/latest',
|
||||
manualDownloadUrl:
|
||||
'https://github.com/4gray/iptvnator/releases/latest',
|
||||
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Unsupported,
|
||||
supportedSelfUpdate: false,
|
||||
});
|
||||
@@ -159,6 +161,7 @@ describe('AppUpdateService', () => {
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'iptvnator/0.22.0',
|
||||
}),
|
||||
})
|
||||
);
|
||||
@@ -261,6 +264,27 @@ describe('AppUpdateService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores duplicate download requests while a download is already running', async () => {
|
||||
const { service, updater } = createService();
|
||||
let resolveDownload: (() => void) | undefined;
|
||||
updater.downloadUpdate.mockReturnValueOnce(
|
||||
new Promise<string[]>((resolve) => {
|
||||
resolveDownload = () => resolve([]);
|
||||
})
|
||||
);
|
||||
service.handleUpdateAvailable({ version: '0.23.0' });
|
||||
|
||||
const firstDownload = service.downloadUpdate();
|
||||
const secondStatus = await service.downloadUpdate();
|
||||
resolveDownload?.();
|
||||
await firstDownload;
|
||||
|
||||
expect(updater.downloadUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(secondStatus.status).toBe(
|
||||
ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloading
|
||||
);
|
||||
});
|
||||
|
||||
it('marks a downloaded update ready to install and installs on request', () => {
|
||||
const { service, updater } = createService();
|
||||
service.handleUpdateAvailable({ version: '0.23.0' });
|
||||
@@ -269,7 +293,9 @@ describe('AppUpdateService', () => {
|
||||
const status: ElectronBridgeAppUpdateStatus = service.getStatus();
|
||||
service.installUpdate();
|
||||
|
||||
expect(status.status).toBe(ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloaded);
|
||||
expect(status.status).toBe(
|
||||
ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloaded
|
||||
);
|
||||
expect(updater.quitAndInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,10 @@ interface AppUpdaterAdapter {
|
||||
downloadUpdate(): Promise<string[]>;
|
||||
quitAndInstall(isSilent?: boolean, isForceRunAfter?: boolean): void;
|
||||
on(event: 'checking-for-update', listener: () => void): this;
|
||||
on(event: 'update-available', listener: (info: AppUpdateInfo) => void): this;
|
||||
on(
|
||||
event: 'update-available',
|
||||
listener: (info: AppUpdateInfo) => void
|
||||
): this;
|
||||
on(
|
||||
event: 'update-not-available',
|
||||
listener: (info: AppUpdateInfo) => void
|
||||
@@ -209,9 +212,9 @@ function toReleaseNotes(
|
||||
function isGitHubRelease(value: unknown): value is GitHubReleaseResponse {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'tag_name' in value &&
|
||||
typeof (value as GitHubReleaseResponse).tag_name === 'string'
|
||||
typeof value === 'object' &&
|
||||
'tag_name' in value &&
|
||||
typeof (value as GitHubReleaseResponse).tag_name === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -335,10 +338,7 @@ export class AppUpdateService {
|
||||
}
|
||||
|
||||
if (
|
||||
this.status.status !==
|
||||
ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Available &&
|
||||
this.status.status !==
|
||||
ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloading
|
||||
this.status.status !== ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Available
|
||||
) {
|
||||
return this.getStatus();
|
||||
}
|
||||
@@ -360,7 +360,8 @@ export class AppUpdateService {
|
||||
installUpdate(): ElectronBridgeAppUpdateStatus {
|
||||
if (
|
||||
this.supportedSelfUpdate &&
|
||||
this.status.status === ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloaded
|
||||
this.status.status ===
|
||||
ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloaded
|
||||
) {
|
||||
this.options.updater.quitAndInstall();
|
||||
}
|
||||
@@ -501,6 +502,7 @@ export class AppUpdateService {
|
||||
const response = await this.releaseFetcher(url, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': `iptvnator/${this.currentVersion}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -579,6 +581,9 @@ export class AppUpdateService {
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send(APP_UPDATE_STATUS_CHANGED, this.getStatus());
|
||||
mainWindow.webContents.send(
|
||||
APP_UPDATE_STATUS_CHANGED,
|
||||
this.getStatus()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,10 @@ interface SettingsSectionScrollDirectiveTestApi {
|
||||
}
|
||||
|
||||
interface SettingsComponentPrivateTestApi {
|
||||
loadAppUpdateStatus(): Promise<void>;
|
||||
matDialog: MatDialog;
|
||||
waitForUiFeedbackFrame(): Promise<void>;
|
||||
waitForAppUpdateStatusRetry(): Promise<void>;
|
||||
}
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
@@ -392,10 +394,34 @@ describe('SettingsComponent', () => {
|
||||
statusHandler(pushedStatus);
|
||||
|
||||
expect(window.electron.getAppUpdateStatus).toHaveBeenCalledTimes(1);
|
||||
expect(window.electron.onAppUpdateStatusChange).toHaveBeenCalledTimes(1);
|
||||
expect(window.electron.onAppUpdateStatusChange).toHaveBeenCalledTimes(
|
||||
1
|
||||
);
|
||||
expect(component.appUpdateStatus()).toEqual(pushedStatus);
|
||||
});
|
||||
|
||||
it('retries the initial app update status load when IPC handlers are still starting', async () => {
|
||||
const retriedStatus: ElectronBridgeAppUpdateStatus = {
|
||||
...DEFAULT_APP_UPDATE_STATUS,
|
||||
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Unsupported,
|
||||
supportedSelfUpdate: false,
|
||||
};
|
||||
await fixture.whenStable();
|
||||
(window.electron.getAppUpdateStatus as jest.Mock)
|
||||
.mockReset()
|
||||
.mockRejectedValueOnce(new Error('No handler registered'))
|
||||
.mockResolvedValueOnce(retriedStatus);
|
||||
jest.spyOn(
|
||||
privateApi(component),
|
||||
'waitForAppUpdateStatusRetry'
|
||||
).mockResolvedValue(undefined);
|
||||
|
||||
await privateApi(component).loadAppUpdateStatus();
|
||||
|
||||
expect(window.electron.getAppUpdateStatus).toHaveBeenCalledTimes(2);
|
||||
expect(component.appUpdateStatus()).toEqual(retriedStatus);
|
||||
});
|
||||
|
||||
it('forwards app update actions to the desktop bridge', async () => {
|
||||
await component.checkForAppUpdate();
|
||||
await component.downloadAppUpdate();
|
||||
|
||||
@@ -85,6 +85,9 @@ import {
|
||||
import { SettingsSnackbarService } from './settings-snackbar.service';
|
||||
import { AppUpdateReleaseNotesDialogComponent } from './app-update-release-notes-dialog.component';
|
||||
|
||||
const APP_UPDATE_STATUS_LOAD_ATTEMPTS = 5;
|
||||
const APP_UPDATE_STATUS_LOAD_RETRY_DELAY_MS = 100;
|
||||
|
||||
@Component({
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
@@ -321,7 +324,31 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appUpdateStatus.set(await window.electron.getAppUpdateStatus());
|
||||
for (
|
||||
let attempt = 1;
|
||||
attempt <= APP_UPDATE_STATUS_LOAD_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
try {
|
||||
this.appUpdateStatus.set(
|
||||
await window.electron.getAppUpdateStatus()
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (attempt === APP_UPDATE_STATUS_LOAD_ATTEMPTS) {
|
||||
console.warn('Failed to load app update status:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.waitForAppUpdateStatusRetry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForAppUpdateStatusRetry(): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, APP_UPDATE_STATUS_LOAD_RETRY_DELAY_MS);
|
||||
});
|
||||
}
|
||||
|
||||
async checkForAppUpdate(): Promise<void> {
|
||||
|
||||
@@ -152,10 +152,7 @@ test('GitHub Releases auto-update metadata is generated and uploaded', () => {
|
||||
);
|
||||
assert.match(buildAndMakeWorkflow, /dist\/executables\/\*\*\/\*\.blockmap/);
|
||||
assert.match(buildAndMakeWorkflow, /Merge macOS updater metadata/);
|
||||
assert.match(
|
||||
buildAndMakeWorkflow,
|
||||
/artifacts\/latest-mac\.yml/
|
||||
);
|
||||
assert.match(buildAndMakeWorkflow, /artifacts\/latest-mac\.yml/);
|
||||
assert.doesNotMatch(
|
||||
releaseFiles,
|
||||
/artifacts\/macos-(?:x64|arm64)-artifacts\/latest-mac\.yml/
|
||||
@@ -168,6 +165,15 @@ test('GitHub Releases auto-update metadata is generated and uploaded', () => {
|
||||
buildAndMakeWorkflow,
|
||||
/artifacts\/windows-artifacts\/latest\.yml/
|
||||
);
|
||||
|
||||
const makeCommands = [
|
||||
...buildAndMakeWorkflow.matchAll(/run: (pnpm run make:app[^\n]*)/g),
|
||||
].map((match) => match[1].trim());
|
||||
assert.ok(makeCommands.length >= 2, 'workflow must package Electron apps');
|
||||
assert.deepEqual(
|
||||
[...new Set(makeCommands)],
|
||||
['pnpm run make:app -- --publish never']
|
||||
);
|
||||
});
|
||||
|
||||
test('generated Electron package metadata mirrors the root package identity', async () => {
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"@iptvnator/shared/interfaces": [
|
||||
"libs/shared/interfaces/src/index.ts"
|
||||
],
|
||||
"@iptvnator/shared/interfaces/ipc-commands": [
|
||||
"libs/shared/interfaces/src/lib/ipc-commands.ts"
|
||||
],
|
||||
"@iptvnator/ui/shared-portals": [
|
||||
"libs/ui/shared-portals/src/index.ts"
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user