chore(deps): upgrade Angular to 22.1 and Nx to 23.2 (#1603)

* chore(deps): upgrade Angular to 22.1 and Nx to 23.2

* fix(deps): complete Angular migrations after rebasing on master

* fix(ci): use the Node pin for Windows runtime refresh

* docs(deps): synchronize the workspace-shell Node requirements
This commit is contained in:
4gray authored and GitHub committed 2026-09-14 19:02:40 +02:00
1 parent ef3f98d026
commit e9eca1c386
200 files changed
+3448 -6823

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
type: internal
area: deps
---
The application uses Angular 22 and Nx 23 with updated build and test tooling.
+2 -2
View File
@@ -61,7 +61,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
- name: Resolve Linux runtime toolchain cache key - name: Resolve Linux runtime toolchain cache key
id: linux-runtime-cache-key id: linux-runtime-cache-key
@@ -379,7 +379,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
- name: Install Linux system dependencies - name: Install Linux system dependencies
+2 -2
View File
@@ -103,7 +103,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
@@ -141,7 +141,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
+2 -2
View File
@@ -59,7 +59,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
- name: Install Dependencies - name: Install Dependencies
@@ -113,7 +113,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '22' node-version-file: '.nvmrc'
cache: 'pnpm' cache: 'pnpm'
- name: Install Dependencies - name: Install Dependencies
@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with: with:
node-version: '22' node-version-file: '.nvmrc'
- name: Refresh pin when it approaches upstream retention - name: Refresh pin when it approaches upstream retention
id: refresh id: refresh
+1
View File
@@ -0,0 +1 @@
22.23.2
+12 -5
View File
@@ -19,10 +19,12 @@ This file provides guidance to coding agents working in this repository.
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy. - See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
- Keep `nx` and every official `@nx/*` package on the same exact version; run - Keep `nx` and every official `@nx/*` package on the same exact version; run
`pnpm run deps:nx:validate` after dependency updates. `pnpm run deps:nx:validate` after dependency updates.
- Vite `7.3.6`, resolved through Angular's build tooling, is patched with - Use the Node version in `.nvmrc` for development and CI. Angular 22 requires
bounded transform prefilters and the upstream precise matchers in Node `^22.22.3 || ^24.15.0` and TypeScript `>=6.0 <6.1` in this workspace.
`patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling - Vite `8.1.5`, resolved through Angular's build tooling, retains upstream
resolves a Vite version containing the fix, and run `pnpm run deps:vite:test` precise matchers and adds bounded raw-code prefilters through
`patches/vite@8.1.5.patch`. Keep the patch until upstream also preserves
comment-bearing asset/worker expressions; run `pnpm run deps:vite:test`
after related dependency updates. after related dependency updates.
- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in - `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in
`patches/app-builder-lib@26.15.7.patch` with the upstream backport `patches/app-builder-lib@26.15.7.patch` with the upstream backport
@@ -30,11 +32,16 @@ This file provides guidance to coding agents working in this repository.
must receive the temporary keychain's own password, not the `.p12` import must receive the temporary keychain's own password, not the `.p12` import
password. macOS runner images since `macos-26-arm64` 20260831 verify that password. macOS runner images since `macos-26-arm64` 20260831 verify that
password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user
name or passphrase you entered is not correct`. Keep the patch until name or passphrase you entered is not correct`. Keep the patch until
electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+), electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+),
and run `pnpm run deps:electron-builder:test` after related dependency and run `pnpm run deps:electron-builder:test` after related dependency
updates — the test fails when the patched version no longer matches the updates — the test fails when the patched version no longer matches the
installed one. installed one.
- `nx-electron@22.0.0` uses a local Nx 23 export-path patch and an explicit
`webpack-node-externals` package extension. Scoped peer allowances for it
and `ngx-indexed-db@22.0.0` live in `pnpm-workspace.yaml`; they are project
compatibility bridges, not upstream support declarations. See
`docs/architecture/nx-workspace-boundaries.md` before removing them.
- A directory holding files consumed by other projects must be an Nx project. - A directory holding files consumed by other projects must be an Nx project.
Nx builds its graph from TypeScript imports only, so a relative SCSS `@use` Nx builds its graph from TypeScript imports only, so a relative SCSS `@use`
across project roots creates no edge and the imported file lands in no task across project roots creates no edge and the imported file lands in no task
+12 -5
View File
@@ -97,10 +97,12 @@ pnpm nx show projects
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy. - See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
- Keep `nx` and every official `@nx/*` package on the same exact version; run - Keep `nx` and every official `@nx/*` package on the same exact version; run
`pnpm run deps:nx:validate` after dependency updates. `pnpm run deps:nx:validate` after dependency updates.
- Vite `7.3.6`, resolved through Angular's build tooling, is patched with - Use the Node version in `.nvmrc` for development and CI. Angular 22 requires
bounded transform prefilters and the upstream precise matchers in Node `^22.22.3 || ^24.15.0` and TypeScript `>=6.0 <6.1` in this workspace.
`patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling - Vite `8.1.5`, resolved through Angular's build tooling, retains upstream
resolves a Vite version containing the fix, and run `pnpm run deps:vite:test` precise matchers and adds bounded raw-code prefilters through
`patches/vite@8.1.5.patch`. Keep the patch until upstream also preserves
comment-bearing asset/worker expressions; run `pnpm run deps:vite:test`
after related dependency updates. after related dependency updates.
- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in - `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in
`patches/app-builder-lib@26.15.7.patch` with the upstream backport `patches/app-builder-lib@26.15.7.patch` with the upstream backport
@@ -108,11 +110,16 @@ pnpm nx show projects
must receive the temporary keychain's own password, not the `.p12` import must receive the temporary keychain's own password, not the `.p12` import
password. macOS runner images since `macos-26-arm64` 20260831 verify that password. macOS runner images since `macos-26-arm64` 20260831 verify that
password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user
name or passphrase you entered is not correct`. Keep the patch until name or passphrase you entered is not correct`. Keep the patch until
electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+), electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+),
and run `pnpm run deps:electron-builder:test` after related dependency and run `pnpm run deps:electron-builder:test` after related dependency
updates — the test fails when the patched version no longer matches the updates — the test fails when the patched version no longer matches the
installed one. installed one.
- `nx-electron@22.0.0` uses a local Nx 23 export-path patch and an explicit
`webpack-node-externals` package extension. Scoped peer allowances for it
and `ngx-indexed-db@22.0.0` live in `pnpm-workspace.yaml`; they are project
compatibility bridges, not upstream support declarations. See
`docs/architecture/nx-workspace-boundaries.md` before removing them.
- A directory holding files consumed by other projects must be an Nx project. - A directory holding files consumed by other projects must be an Nx project.
Nx builds its graph from TypeScript imports only, so a relative SCSS `@use` Nx builds its graph from TypeScript imports only, so a relative SCSS `@use`
across project roots creates no edge and the imported file lands in no task across project roots creates no edge and the imported file lands in no task
+8 -1
View File
@@ -272,11 +272,18 @@ needed.
Requirements: Requirements:
- Node.js 22.13–22.x or 24 and newer with pnpm (via Corepack) - Node.js 22.22.3 or newer within 22.x, or 24.15.0 or newer within 24.x
- pnpm 10.33.0 (via Corepack)
The repository's `.nvmrc` pins the Node.js version used by CI. With nvm,
run `nvm install` and `nvm use` from the repository root to use the same version
for local development.
1. Clone this repository and install project dependencies: 1. Clone this repository and install project dependencies:
``` ```
$ nvm install
$ nvm use
$ corepack enable $ corepack enable
$ pnpm install $ pnpm install
``` ```
+2 -1
View File
@@ -9,7 +9,8 @@
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"ignoreDeprecations": "6.0"
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+2 -1
View File
@@ -3,7 +3,8 @@
"compilerOptions": { "compilerOptions": {
"types": ["node"], "types": ["node"],
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"module": "NodeNext" "module": "NodeNext",
"ignoreDeprecations": "6.0"
}, },
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }
@@ -4,7 +4,7 @@ import {
provideBrowserGlobalErrorListeners, provideBrowserGlobalErrorListeners,
provideZoneChangeDetection, provideZoneChangeDetection,
} from '@angular/core'; } from '@angular/core';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient, withXhr } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
@@ -17,7 +17,7 @@ export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(), provideHttpClient(withXhr()),
importProvidersFrom( importProvidersFrom(
TranslateModule.forRoot({ TranslateModule.forRoot({
defaultLanguage: 'en', defaultLanguage: 'en',
+8 -1
View File
@@ -1,4 +1,9 @@
import { Component, OnInit, inject } from '@angular/core'; import {
Component,
OnInit,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { RemoteControlComponent } from '@iptvnator/ui/remote-control'; import { RemoteControlComponent } from '@iptvnator/ui/remote-control';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -6,6 +11,8 @@ import { TranslateService } from '@ngx-translate/core';
imports: [RemoteControlComponent], imports: [RemoteControlComponent],
selector: 'app-root', selector: 'app-root',
templateUrl: './app.html', templateUrl: './app.html',
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './app.scss', styleUrl: './app.scss',
}) })
export class App implements OnInit { export class App implements OnInit {
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+9 -1
View File
@@ -10,5 +10,13 @@
"src/test-setup.ts", "src/test-setup.ts",
"src/**/*.test.ts", "src/**/*.test.ts",
"src/**/*.spec.ts" "src/**/*.spec.ts"
] ],
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
"nullishCoalescingNotNullable": "suppress",
"optionalChainNotNullable": "suppress"
}
}
}
} }
+2 -1
View File
@@ -10,7 +10,8 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"isolatedModules": true, "isolatedModules": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+4 -1
View File
@@ -9,7 +9,10 @@
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"noUncheckedSideEffectImports": false,
"types": ["*"],
"ignoreDeprecations": "6.0"
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
+19
View File
@@ -18,7 +18,26 @@
"bundle": true, "bundle": true,
"thirdParty": true, "thirdParty": true,
"generatePackageJson": true "generatePackageJson": true
},
"inputs": [
"default",
"^default",
"{workspaceRoot}/pnpm-workspace.yaml",
{
"json": "{workspaceRoot}/package.json",
"fields": [
"packageManager",
"pnpm.onlyBuiltDependencies",
"pnpm.neverBuiltDependencies",
"pnpm.allowBuilds",
"pnpm.supportedArchitectures",
"pnpm.patchedDependencies"
]
},
{
"runtime": "node -e \"try{console.log('pnpm major '+require('child_process').execSync('pnpm --version',{stdio:['ignore','pipe','ignore']}).toString().trim().split('.')[0])}catch{console.log('pnpm major unavailable')}\""
} }
]
}, },
"serve": { "serve": {
"continuous": true, "continuous": true,
+2 -1
View File
@@ -8,7 +8,8 @@
"lib": ["ES2022"], "lib": ["ES2022"],
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true "skipLibCheck": true,
"ignoreDeprecations": "6.0"
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }
+170 -1
View File
@@ -4,6 +4,12 @@ import { expect, test } from './fixtures';
const fixturePath = join(__dirname, 'fixtures/test.m3u'); const fixturePath = join(__dirname, 'fixtures/test.m3u');
test('@web @m3u basic playlist import flow', async ({ page }) => { test('@web @m3u basic playlist import flow', async ({ page }) => {
// The fixture's placeholder media hosts must not delay navigation or make
// persistence coverage depend on external DNS and asset availability.
await page.route(
/^https?:\/\/(?:channel\.icons\.url|example\.channels|xml-url)\//,
(route) => route.abort()
);
await page.goto('/'); await page.goto('/');
// Basic checks // Basic checks
@@ -29,6 +35,15 @@ test('@web @m3u basic playlist import flow', async ({ page }) => {
await expect(page.getByText('4 channels')).toBeVisible(); await expect(page.getByText('4 channels')).toBeVisible();
await expect(page.getByText('1. Channel 1')).toBeVisible(); await expect(page.getByText('1. Channel 1')).toBeVisible();
await expect(page.getByText('4. HappyKids TV')).toBeVisible(); await expect(page.getByText('4. HappyKids TV')).toBeVisible();
const importedPlaylistUrl = page.url();
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page).toHaveURL(importedPlaylistUrl);
await expect(page.getByText('test', { exact: true })).toBeVisible();
await expect(page.getByText('4 channels')).toBeVisible();
await expect(page.getByText('1. Channel 1')).toBeVisible();
await expect(page.getByText('4. HappyKids TV')).toBeVisible();
}); });
test('@web @auto-detect pasted provider message prefills the Xtream form', async ({ test('@web @auto-detect pasted provider message prefills the Xtream form', async ({
@@ -110,7 +125,161 @@ test('@web @auto-detect keeps the pasted message when switching methods', async
); );
}); });
test('@web keyboard shortcuts help opens from question mark', async ({ page }) => { test('@web @m3u reads an existing v1 IndexedDB playlist after application upgrade', async ({
page,
}) => {
const playlistId = 'legacy-indexeddb-playlist';
// Seed before Angular boots so the application must open the old database.
await page.route('**/__indexeddb-seed__', (route) =>
route.fulfill({ contentType: 'text/html', body: '<!doctype html>' })
);
await page.route('https://legacy.example.invalid/**', (route) =>
route.abort()
);
await page.goto('/__indexeddb-seed__');
await page.evaluate(
(id) =>
new Promise<void>((resolve, reject) => {
const request = indexedDB.open('iptvnator', 1);
request.onerror = () => reject(request.error);
request.onupgradeneeded = () => {
const store = request.result.createObjectStore(
'playlists',
{
keyPath: '_id',
autoIncrement: false,
}
);
// Freeze the historical schema independently of app config.
for (const name of [
'_id',
'filename',
'title',
'count',
'playlist',
'importDate',
'lastUsage',
'favorites',
'recentlyViewed',
'autoRefresh',
'url',
'filePath',
]) {
store.createIndex(name, name, { unique: false });
}
};
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(
'playlists',
'readwrite'
);
transaction.onabort = () => {
db.close();
reject(transaction.error);
};
transaction.oncomplete = () => {
db.close();
resolve();
};
transaction.objectStore('playlists').add({
_id: id,
title: 'Legacy playlist',
filename: 'legacy.m3u',
importDate: '2025-01-01T00:00:00.000Z',
lastUsage: '2025-01-01T00:00:00.000Z',
count: 1,
autoRefresh: false,
favorites: [],
recentlyViewed: [],
playlist: {
header: { attrs: {}, raw: '#EXTM3U' },
items: [
{
id: 'legacy-channel',
name: 'Legacy channel',
url: 'https://legacy.example.invalid/live.m3u8',
group: { title: 'News' },
tvg: {
id: '',
name: '',
url: '',
logo: '',
rec: '',
},
http: {
referrer: '',
'user-agent': '',
origin: '',
},
radio: 'false',
},
],
},
});
};
}),
playlistId
);
const playlistPath = `/workspace/playlists/${playlistId}/all`;
await page.goto(playlistPath, { waitUntil: 'domcontentloaded' });
await expect(
page.getByText('Legacy playlist', { exact: true })
).toBeVisible();
await expect(
page.getByText('1. Legacy channel', { exact: true })
).toBeVisible();
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page).toHaveURL(new RegExp(`${playlistPath}$`));
await expect(
page.getByText('Legacy playlist', { exact: true })
).toBeVisible();
await expect(
page.getByText('1. Legacy channel', { exact: true })
).toBeVisible();
const persisted = await page.evaluate(
(id) =>
new Promise((resolve, reject) => {
const request = indexedDB.open('iptvnator');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction('playlists', 'readonly');
const store = transaction.objectStore('playlists');
const record = store.get(id);
transaction.onabort = () => {
db.close();
reject(transaction.error);
};
transaction.oncomplete = () => {
resolve({
version: db.version,
keyPath: store.keyPath,
autoIncrement: store.autoIncrement,
id: record.result?._id,
channel: record.result?.playlist.items[0].name,
});
db.close();
};
};
}),
playlistId
);
expect(persisted).toEqual({
version: 1,
keyPath: '_id',
autoIncrement: false,
id: playlistId,
channel: 'Legacy channel',
});
});
test('@web keyboard shortcuts help opens from question mark', async ({
page,
}) => {
await page.goto('/'); await page.goto('/');
await page.getByRole('button', { name: 'Open keyboard shortcuts' }).focus(); await page.getByRole('button', { name: 'Open keyboard shortcuts' }).focus();
+24
View File
@@ -66,6 +66,18 @@ test.skip(
'DASH ClearKey coverage targets Chromium' 'DASH ClearKey coverage targets Chromium'
); );
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const play = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function () {
// Keep real decoding while avoiding the host's audio-output clock.
// Muting must happen before play, including replacement players.
this.muted = true;
return play.call(this);
};
});
});
async function serveDashFixtures(page: Page): Promise<void> { async function serveDashFixtures(page: Page): Promise<void> {
await page.route(`${FIXTURE_HOST}/**`, async (route) => { await page.route(`${FIXTURE_HOST}/**`, async (route) => {
const url = new URL(route.request().url()); const url = new URL(route.request().url());
@@ -150,7 +162,19 @@ test('@web @m3u @dash ClearKey and clear DASH channels play inline', async ({
await page.getByText('1. ClearKey DASH').click(); await page.getByText('1. ClearKey DASH').click();
await expectVideoPlaying(page); await expectVideoPlaying(page);
const clearKeyVideo = await page
.locator('app-web-player-view video')
.first()
.elementHandle();
if (!clearKeyVideo) {
throw new Error('Missing playing ClearKey video');
}
await page.getByText('2. Clear DASH').click(); await page.getByText('2. Clear DASH').click();
// The new source mounts asynchronously; never accept the old video's time.
await page.waitForFunction(
(previous) => !previous.isConnected,
clearKeyVideo
);
await expectVideoPlaying(page); await expectVideoPlaying(page);
}); });
+2 -1
View File
@@ -9,7 +9,8 @@
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"ignoreDeprecations": "6.0"
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -1
View File
@@ -1,4 +1,4 @@
import { nxPreset } from '@nx/jest/preset.js'; import { nxPreset } from '@nx/jest/preset';
import { createEsmPreset } from 'jest-preset-angular/presets/index.js'; import { createEsmPreset } from 'jest-preset-angular/presets/index.js';
const angularEsmPreset = createEsmPreset({ const angularEsmPreset = createEsmPreset({
@@ -5,6 +5,7 @@ import {
OnInit, OnInit,
signal, signal,
inject, inject,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@@ -101,6 +102,8 @@ import { AppUpdateReleaseNotesDialogComponent } from './settings/app-update-rele
</section> </section>
} }
`, `,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
.app-update-notification { .app-update-notification {
+3
View File
@@ -5,6 +5,7 @@ import {
inject, inject,
OnInit, OnInit,
signal, signal,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Router, RouterOutlet } from '@angular/router'; import { Router, RouterOutlet } from '@angular/router';
@@ -48,6 +49,8 @@ const debugAppComponent = createDevLogger('AppComponent');
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
AppStartupStatusComponent, AppStartupStatusComponent,
AppUpdateNotificationPanelComponent, AppUpdateNotificationPanelComponent,
+2 -1
View File
@@ -2,6 +2,7 @@ import {
HttpClient, HttpClient,
provideHttpClient, provideHttpClient,
withInterceptorsFromDi, withInterceptorsFromDi,
withXhr,
} from '@angular/common/http'; } from '@angular/common/http';
import { import {
FullscreenOverlayContainer, FullscreenOverlayContainer,
@@ -114,7 +115,7 @@ export const appConfig: ApplicationConfig = {
// fullscreen channel panel's sort/context menus and the controls' // fullscreen channel panel's sort/context menus and the controls'
// tooltips render while the player is fullscreen. // tooltips render while the player is fullscreen.
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer }, { provide: OverlayContainer, useClass: FullscreenOverlayContainer },
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withXhr(), withInterceptorsFromDi()),
provideStore({ provideStore({
router: routerReducer, router: routerReducer,
playlistState: playlistReducer, playlistState: playlistReducer,
@@ -6,6 +6,7 @@ import {
SecurityContext, SecurityContext,
signal, signal,
ViewEncapsulation, ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -76,7 +77,9 @@ function decorateReleaseNotesHtml(html: string): string {
</button> </button>
<div class="release-notes-dialog__version"> <div class="release-notes-dialog__version">
<strong>{{ notes()?.releaseName || notes()?.tagName }}</strong> <strong>{{
notes()?.releaseName || notes()?.tagName
}}</strong>
@if (notes()?.publishedAt; as publishedAt) { @if (notes()?.publishedAt; as publishedAt) {
<span>{{ publishedAt | date: 'mediumDate' }}</span> <span>{{ publishedAt | date: 'mediumDate' }}</span>
} }
@@ -120,6 +123,8 @@ function decorateReleaseNotesHtml(html: string): string {
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>
`, `,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
.release-notes-dialog { .release-notes-dialog {
@@ -210,9 +215,8 @@ function decorateReleaseNotesHtml(html: string): string {
], ],
}) })
export class AppUpdateReleaseNotesDialogComponent implements OnInit { export class AppUpdateReleaseNotesDialogComponent implements OnInit {
private readonly data = inject<AppUpdateReleaseNotesDialogData>( private readonly data =
MAT_DIALOG_DATA inject<AppUpdateReleaseNotesDialogData>(MAT_DIALOG_DATA);
);
private readonly dialogRef = inject( private readonly dialogRef = inject(
MatDialogRef<AppUpdateReleaseNotesDialogComponent> MatDialogRef<AppUpdateReleaseNotesDialogComponent>
); );
@@ -271,7 +275,9 @@ export class AppUpdateReleaseNotesDialogComponent implements OnInit {
await window.electron.getAppUpdateReleaseNotes(request) await window.electron.getAppUpdateReleaseNotes(request)
); );
} catch (error) { } catch (error) {
this.error.set(error instanceof Error ? error.message : String(error)); this.error.set(
error instanceof Error ? error.message : String(error)
);
} finally { } finally {
this.loading.set(false); this.loading.set(false);
} }
@@ -4,6 +4,7 @@ import {
input, input,
output, output,
ViewEncapsulation, ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -18,6 +19,8 @@ import {
imports: [MatButtonModule, MatIconModule, TranslateModule], imports: [MatButtonModule, MatIconModule, TranslateModule],
templateUrl: './settings-about-section.component.html', templateUrl: './settings-about-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
':host { display: contents; }', ':host { display: contents; }',
'.version-block .build-commit { opacity: 0.65; font-size: 0.85em; }', '.version-block .build-commit { opacity: 0.65; font-size: 0.85em; }',
@@ -28,7 +31,9 @@ export class SettingsAboutSectionComponent {
readonly version = input<string | undefined>(); readonly version = input<string | undefined>();
readonly buildCommit = input<string | undefined>(); readonly buildCommit = input<string | undefined>();
readonly updateMessage = input<string | undefined>(); readonly updateMessage = input<string | undefined>();
readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(null); readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(
null
);
readonly buildCommitShort = computed(() => { readonly buildCommitShort = computed(() => {
const commit = this.buildCommit()?.trim(); const commit = this.buildCommit()?.trim();
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -14,6 +20,8 @@ import { TranslateModule } from '@ngx-translate/core';
], ],
templateUrl: './settings-backup-section.component.html', templateUrl: './settings-backup-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsBackupSectionComponent { export class SettingsBackupSectionComponent {
@@ -1,5 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, input, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -16,6 +21,8 @@ import { TranslateModule } from '@ngx-translate/core';
], ],
templateUrl: './settings-dashboard-section.component.html', templateUrl: './settings-dashboard-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsDashboardSectionComponent { export class SettingsDashboardSectionComponent {
@@ -1,5 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core'; import {
Component,
computed,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -25,6 +30,8 @@ type SettingsDeleteSummaryItem = {
selector: 'app-settings-delete-all-playlists-dialog', selector: 'app-settings-delete-all-playlists-dialog',
templateUrl: './settings-delete-all-playlists-dialog.component.html', templateUrl: './settings-delete-all-playlists-dialog.component.html',
styleUrls: ['./settings-delete-all-playlists-dialog.component.scss'], styleUrls: ['./settings-delete-all-playlists-dialog.component.scss'],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
CommonModule, CommonModule,
MatButtonModule, MatButtonModule,
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -28,6 +34,8 @@ import { EpgViewModeOption } from './settings.models';
], ],
templateUrl: './settings-epg-section.component.html', templateUrl: './settings-epg-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsEpgSectionComponent { export class SettingsEpgSectionComponent {
@@ -1,5 +1,11 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, input, output, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@@ -27,6 +33,8 @@ import {
], ],
templateUrl: './settings-general-section.component.html', templateUrl: './settings-general-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsGeneralSectionComponent { export class SettingsGeneralSectionComponent {
@@ -1,5 +1,11 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, input, output, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -30,6 +36,8 @@ import { SettingsPlayerOption } from './settings.models';
], ],
templateUrl: './settings-playback-section.component.html', templateUrl: './settings-playback-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsPlaybackSectionComponent { export class SettingsPlaybackSectionComponent {
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -24,6 +30,8 @@ import { QRCodeComponent } from 'angularx-qrcode';
], ],
templateUrl: './settings-remote-control-section.component.html', templateUrl: './settings-remote-control-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsRemoteControlSectionComponent { export class SettingsRemoteControlSectionComponent {
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core'; import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -15,6 +21,8 @@ import { SettingsPlaylistDeleteSummary } from './settings.models';
], ],
templateUrl: './settings-reset-section.component.html', templateUrl: './settings-reset-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'], styles: [':host { display: contents; }'],
}) })
export class SettingsResetSectionComponent { export class SettingsResetSectionComponent {
@@ -4,6 +4,7 @@ import {
input, input,
signal, signal,
ViewEncapsulation, ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -32,6 +33,8 @@ type TmdbKeyTestState = 'idle' | 'testing' | 'success' | 'error';
], ],
templateUrl: './settings-tmdb-section.component.html', templateUrl: './settings-tmdb-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
app-settings-tmdb-section { app-settings-tmdb-section {
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'; import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -38,6 +38,8 @@ export interface SettingsUnsavedChangesDialogData {
} }
`, `,
], ],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
template: ` template: `
<h2 mat-dialog-title> <h2 mat-dialog-title>
{{ 'SETTINGS.UNSAVED_DIALOG_TITLE' | translate }} {{ 'SETTINGS.UNSAVED_DIALOG_TITLE' | translate }}
@@ -472,7 +472,11 @@ describe('SettingsComponent form', () => {
const webFixture = TestBed.createComponent(SettingsComponent); const webFixture = TestBed.createComponent(SettingsComponent);
const webComponent = webFixture.componentInstance; const webComponent = webFixture.componentInstance;
stubSettingsSideEffects(webComponent); stubSettingsSideEffects(webComponent);
const loadSettings = jest.spyOn(webComponent.form, 'loadSettings');
webFixture.detectChanges(); webFixture.detectChanges();
expect(loadSettings).toHaveBeenCalledTimes(1);
// Await hydration before editing; Zone cannot track native awaits.
await loadSettings.mock.results[0].value;
await webFixture.whenStable(); await webFixture.whenStable();
webComponent.settingsForm.patchValue({ theme: Theme.DarkTheme }); webComponent.settingsForm.patchValue({ theme: Theme.DarkTheme });
@@ -78,6 +78,10 @@ describe('SettingsComponent', () => {
const lifecycleFixture = TestBed.createComponent(SettingsComponent); const lifecycleFixture = TestBed.createComponent(SettingsComponent);
const lifecycleComponent = lifecycleFixture.componentInstance; const lifecycleComponent = lifecycleFixture.componentInstance;
stubSettingsSideEffects(lifecycleComponent); stubSettingsSideEffects(lifecycleComponent);
const loadSettings = jest.spyOn(
lifecycleComponent.form,
'loadSettings'
);
const appUpdateInit = jest.spyOn(lifecycleComponent.appUpdate, 'init'); const appUpdateInit = jest.spyOn(lifecycleComponent.appUpdate, 'init');
const appUpdateDispose = jest.spyOn( const appUpdateDispose = jest.spyOn(
lifecycleComponent.appUpdate, lifecycleComponent.appUpdate,
@@ -89,6 +93,9 @@ describe('SettingsComponent', () => {
); );
lifecycleFixture.detectChanges(); lifecycleFixture.detectChanges();
expect(loadSettings).toHaveBeenCalledTimes(1);
// Let ngOnInit resume after its native promise, which Zone cannot track.
await loadSettings.mock.results[0].value;
await lifecycleFixture.whenStable(); await lifecycleFixture.whenStable();
expect(appUpdateInit).toHaveBeenCalledTimes(1); expect(appUpdateInit).toHaveBeenCalledTimes(1);
@@ -197,7 +204,10 @@ describe('SettingsComponent', () => {
it('save-and-leave persists before allowing the navigation', async () => { it('save-and-leave persists before allowing the navigation', async () => {
component.settingsForm.markAsDirty(); component.settingsForm.markAsDirty();
jest.spyOn(component.epg, 'fetchConfiguredEpg').mockImplementation(); jest.spyOn(
component.epg,
'fetchConfiguredEpg'
).mockImplementation();
answerDialogWith('save'); answerDialogWith('save');
await expect( await expect(
@@ -7,6 +7,7 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewEncapsulation, ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'; import { toSignal } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
@@ -93,6 +94,8 @@ export const SETTINGS_DEFAULT_SECTION = 'general';
SettingsResetSectionComponent, SettingsResetSectionComponent,
SettingsTmdbSectionComponent, SettingsTmdbSectionComponent,
], ],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
providers: [ providers: [
SettingsAppUpdateFacade, SettingsAppUpdateFacade,
SettingsBackupFacade, SettingsBackupFacade,
+5 -2
View File
@@ -1,4 +1,7 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone/index.mjs'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless/index.mjs';
import { installDuplicateVideoJsQualityLevelsWarnFilter } from '@iptvnator/shared/testing'; import { installDuplicateVideoJsQualityLevelsWarnFilter } from '@iptvnator/shared/testing';
Object.defineProperty(globalThis, 'jest', { Object.defineProperty(globalThis, 'jest', {
@@ -25,7 +28,7 @@ if (!Element.prototype.animate) {
installDuplicateVideoJsQualityLevelsWarnFilter(); installDuplicateVideoJsQualityLevelsWarnFilter();
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+9 -1
View File
@@ -12,5 +12,13 @@
"src/**/*.test.ts", "src/**/*.test.ts",
"src/**/*.spec.ts", "src/**/*.spec.ts",
"src/**/*.stub.ts" "src/**/*.stub.ts"
] ],
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
"nullishCoalescingNotNullable": "suppress",
"optionalChainNotNullable": "suppress"
}
}
}
} }
+2 -1
View File
@@ -15,7 +15,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"useDefineForClassFields": false "useDefineForClassFields": false,
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+3 -3
View File
@@ -3,9 +3,9 @@
"include": [".astro/types.d.ts", "**/*"], "include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"], "exclude": ["dist"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["./src/*"]
} },
"ignoreDeprecations": "6.0"
} }
} }
+4 -1
View File
@@ -9,7 +9,10 @@
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true,
"noUncheckedSideEffectImports": false,
"types": ["*"],
"ignoreDeprecations": "6.0"
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
+2 -1
View File
@@ -6,7 +6,8 @@
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node10", "moduleResolution": "node10",
"types": ["jest", "node"], "types": ["jest", "node"],
"strict": true "strict": true,
"ignoreDeprecations": "6.0"
}, },
"include": [ "include": [
"jest.config.ts", "jest.config.ts",
+1 -1
View File
@@ -8,7 +8,7 @@ RUN apk add --no-cache python3 make g++ git
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY .npmrc ./ COPY .npmrc ./
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY patches ./patches COPY patches ./patches
# Node 25 unbundled Corepack, so `corepack enable` cannot be the way pnpm gets # Node 25 unbundled Corepack, so `corepack enable` cannot be the way pnpm gets
+61 -14
View File
@@ -56,23 +56,70 @@ Omit `pnpm nx migrate --run-migrations` when the first command reports that no
migrations exist. Major Nx updates always use this manual workflow and the migrations exist. Major Nx updates always use this manual workflow and the
resulting PR runs the full CI pipeline. resulting PR runs the full CI pipeline.
## Angular 22 Toolchain Compatibility
Angular framework/Material 22.1 and CLI/build 22.1 use Nx 23.2 and TypeScript
6.0. Framework and CLI patch numbers may differ; keep framework packages aligned
in both dependency sections and all official Nx packages on one exact version.
Use `.nvmrc` for development/CI; supported LTS Node ranges are
`^22.22.3 || ^24.15.0`. Docker uses the supported Node 24 line.
The Angular 22 migration explicitly preserves the old default change detection
with `ChangeDetectionStrategy.Eager`. Existing OnPush components and
`provideZoneChangeDetection` remain in their existing modes.
Existing eager components under Angular ESLint carry a line-scoped explanation
for the newly recommended OnPush rule; new components keep that rule enabled.
Adopting OnPush in those components requires a separate behavior review.
TypeScript 6 migration settings preserve compiler defaults and per-project output roots;
`ignoreDeprecations: "6.0"` temporarily supports legacy Node test configs and
`baseUrl`, rather than changing the whole workspace to browser resolution.
Two third-party packages need scoped compatibility bridges in
`pnpm-workspace.yaml`:
- `nx-electron@22.0.0` still declares Nx 22 peers. Its existing package-layout
patch also changes the removed `@nx/js/src/utils/buildable-libs-utils` import
to `@nx/js/internal`. A package extension supplies its undeclared runtime
dependency `webpack-node-externals`. The packaging suite loads every executor
and checks the single application manifest. Remove these bridges only after
the upstream package declares and implements the current Nx APIs.
- `ngx-indexed-db@22.0.0` declares Angular `<22`, but its used API remains
compatible. Its allowance is scoped to the exact Angular version and must be
revalidated on upgrades. `apps/web-e2e/src/basic.e2e.ts` opens a historical
`iptvnator` v1 database, retains the `_id` key path, and checks reload
persistence. The shared interfaces package declares the same library version.
Remove the allowance when upstream includes the installed Angular version.
Jest uses root Babel 7 while Angular build retains its own Babel 8. The website
uses Astro 7 with the root TypeScript 6; no Astro-specific compiler package
extension is needed. Keep the website build in toolchain validation.
The website's alias uses a relative `./src/*` target without the deprecated
`baseUrl` option.
Electron's webpack build resolves `ts-loader` 9.6.2 through the existing
`nx-electron` version range. Keep at least 9.5.7: earlier versions clear
`rootDir` in transpile-only mode and fail with TypeScript 6 (TS5011/TS6059).
Angular unit-test setup explicitly uses `setupZonelessTestEnv` and separately
imports `zone.js/testing` for existing `fakeAsync` helpers. This preserves the
Angular 21 scheduler used by jest-preset-angular 15: its `setupZoneTestEnv` loaded
Zone but did not provide zone-based change detection on Angular 20+. Preset 17
does provide it, which disables automatic fixture rendering and changes
`whenStable()` behavior. Production applications continue to use their existing
`provideZoneChangeDetection`; browser and Electron E2E validate that runtime.
## Vite Dev-Server Patch ## Vite Dev-Server Patch
Angular's development builder currently resolves Vite `7.3.6`. That release's Angular's development builder resolves Vite `8.1.5`. It contains upstream precise
asset and worker transform prefilters can catastrophically backtrack on a large asset/worker URL matchers, but also uses them as raw-code prefilters. Those
generated chunk containing unrelated `new URL...` expressions while searching prefilters reject valid expressions containing comments before the handlers can
for a valid `new URL(..., import.meta.url)` construct. The Electron development strip the comments and apply the precise matchers.
server can then fail a lazy chunk request with `Maximum call stack size
exceeded`, even though static builds succeed because they do not pass emitted
chunks through Vite's request-time plugin container.
`patches/vite@7.3.6.patch` backports Vite's upstream precise-matcher fix from `patches/vite@8.1.5.patch` adds bounded asset/worker prefilters and uses the asset
[vitejs/vite#21800](https://github.com/vitejs/vite/pull/21800). Bounded prefilter for bundled URL rewriting too. The precise handler matchers remain
prefilters keep the request-time scan linear while allowing comment-bearing unchanged. This preserves the earlier protection against expensive scanning of
asset and worker expressions to reach the precise matcher after Vite strips large false-positive chunks and retains comment-bearing expressions. Remove the
comments. Keep the patch while the supported Angular toolchain resolves Vite patch only when the resolved upstream Vite passes the same behavior checks:
`7.3.6`; remove it only after the resolved Vite contains the upstream fix. Run
the regression check after any related manifest or lockfile update:
```bash ```bash
pnpm run deps:vite:test pnpm run deps:vite:test
+7 -6
View File
@@ -325,6 +325,7 @@ handlers in `apps/electron-backend/src/app/events/window.events.ts`):
`leave-html-full-screen` would un-hide the controls over a window that `leave-html-full-screen` would un-hide the controls over a window that
is still fullscreen. A fullscreen launch (below) or F11 followed by the is still fullscreen. A fullscreen launch (below) or F11 followed by the
player's `F` → `Esc` makes that path routine on Windows/Linux. player's `F` → `Esc` makes that path routine on Windows/Linux.
3. `WINDOW:TOGGLE_FULLSCREEN` toggles OS-level fullscreen (`setFullScreen`) 3. `WINDOW:TOGGLE_FULLSCREEN` toggles OS-level fullscreen (`setFullScreen`)
and, like the maximize toggle, reports the requested state and leaves and, like the maximize toggle, reports the requested state and leaves
the `WINDOW:STATE_CHANGED` push authoritative. Because the transition is the `WINDOW:STATE_CHANGED` push authoritative. Because the transition is
@@ -439,15 +440,15 @@ Toolchain notes for the Electron 43 baseline:
implicit `node-gyp rebuild` bypasses those binaries and makes installation implicit `node-gyp rebuild` bypasses those binaries and makes installation
depend on the host compiler toolchain. The old `node-abi` override belonged depend on the host compiler toolchain. The old `node-abi` override belonged
to v12's removed `prebuild-install` path and is no longer required. to v12's removed `prebuild-install` path and is no longer required.
2. Local development supports **Node 22.13–22.x or Node >= 24**, declared in 2. Local development supports **Node 22.22.3–22.x or 24.15.0–24.x**, declared in
`engines` as `^22.13.0 || >=24.0.0`. `engines` as `^22.22.3 || ^24.15.0`. Use `.nvmrc` (currently 22.23.2) for
The direct `@faker-js/faker` dependency and current lint tooling require development and CI. Angular 22 sets this supported LTS range and requires
that floor. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4, TypeScript `>=6.0 <6.1`. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
which requires Node 22.12 or newer, and the root `postinstall` runs which requires Node 22.12 or newer, and the root `postinstall` runs
`install-app-deps` on every `pnpm install`. The 26.15.7 minimum also `install-app-deps` on every `pnpm install`. The 26.15.7 minimum also
carries the v26 backport that fully extracts the Snap template's `.tar.7z` carries the v26 backport that fully extracts the Snap template's `.tar.7z`
payload; 26.15.0–26.15.6 can payload; 26.15.0–26.15.6 can
produce a Snap that is missing `desktop-init.sh`. CI already runs Node 22. produce a Snap that is missing `desktop-init.sh`.
3. Dependabot keeps Electron, native database, packaging, EPG parser, and 3. Dependabot keeps Electron, native database, packaging, EPG parser, and
version-locked Shaka/mpegts updates out of the shared npm minor/patch group. version-locked Shaka/mpegts updates out of the shared npm minor/patch group.
Those dependencies require standalone PRs so their dedicated package, Those dependencies require standalone PRs so their dedicated package,
@@ -457,7 +458,7 @@ Toolchain notes for the Electron 43 baseline:
Electron command instead of during package `postinstall`, so `electron` Electron command instead of during package `postinstall`, so `electron`
must not remain in pnpm's `onlyBuiltDependencies` allowlist. The first local must not remain in pnpm's `onlyBuiltDependencies` allowlist. The first local
`pnpm run serve:backend` can include a one-time download; use `pnpm exec `pnpm run serve:backend` can include a one-time download; use `pnpm exec
electron --version` to prewarm it before an offline run. electron --version` to prewarm it before an offline run.
Known caveats: Known caveats:
+1 -1
View File
@@ -1,4 +1,4 @@
import { nxPreset } from '@nx/jest/preset.js'; import { nxPreset } from '@nx/jest/preset';
import { createEsmPreset } from 'jest-preset-angular/presets/index.js'; import { createEsmPreset } from 'jest-preset-angular/presets/index.js';
const angularEsmPreset = createEsmPreset({ const angularEsmPreset = createEsmPreset({
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -9,7 +9,8 @@
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"files": [], "files": [],
"include": [], "include": [],
@@ -6,6 +6,7 @@ import {
signal, signal,
ViewEncapsulation, ViewEncapsulation,
viewChild, viewChild,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { import {
@@ -70,6 +71,7 @@ const METHOD_BY_CANDIDATE_KIND: Record<
selector: 'app-add-playlist', selector: 'app-add-playlist',
templateUrl: './add-playlist-dialog.component.html', templateUrl: './add-playlist-dialog.component.html',
styleUrl: './add-playlist-dialog.component.scss', styleUrl: './add-playlist-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class AddPlaylistDialogComponent { export class AddPlaylistDialogComponent {
@@ -98,8 +100,9 @@ export class AddPlaylistDialogComponent {
* the prefill is applied by an effect that re-runs once the child's * the prefill is applied by an effect that re-runs once the child's
* viewChild signal resolves, then clears this. * viewChild signal resolves, then clears this.
*/ */
private readonly pendingPrefill = private readonly pendingPrefill = signal<ProviderImportCandidate | null>(
signal<ProviderImportCandidate | null>(null); null
);
/** /**
* Survives the auto-detect surface being destroyed by a method switch, so * Survives the auto-detect surface being destroyed by a method switch, so
@@ -1,4 +1,11 @@
import { Component, computed, input, OnInit, output } from '@angular/core'; import {
Component,
computed,
input,
OnInit,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -139,6 +146,7 @@ function maskUrlQueryPasswords(url: string): string {
selector: 'app-auto-import', selector: 'app-auto-import',
templateUrl: './auto-import.component.html', templateUrl: './auto-import.component.html',
styleUrl: './auto-import.component.scss', styleUrl: './auto-import.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
MatButtonModule, MatButtonModule,
MatFormFieldModule, MatFormFieldModule,
@@ -228,10 +236,7 @@ export class AutoImportComponent implements OnInit {
); );
break; break;
case 'xtream': case 'xtream':
push( push('HOME.XTREAM_PLAYLIST.SERVER_URL', candidate.serverUrl);
'HOME.XTREAM_PLAYLIST.SERVER_URL',
candidate.serverUrl
);
push('HOME.XTREAM_PLAYLIST.USERNAME', candidate.username); push('HOME.XTREAM_PLAYLIST.USERNAME', candidate.username);
push( push(
'HOME.XTREAM_PLAYLIST.PASSWORD', 'HOME.XTREAM_PLAYLIST.PASSWORD',
@@ -239,14 +244,8 @@ export class AutoImportComponent implements OnInit {
); );
break; break;
case 'stalker': case 'stalker':
push( push('HOME.STALKER_PORTAL.SERVER_URL', candidate.portalUrl);
'HOME.STALKER_PORTAL.SERVER_URL', push('HOME.STALKER_PORTAL.MAC_ADDRESS', candidate.macAddress);
candidate.portalUrl
);
push(
'HOME.STALKER_PORTAL.MAC_ADDRESS',
candidate.macAddress
);
push( push(
'HOME.STALKER_PORTAL.SERIAL_NUMBER', 'HOME.STALKER_PORTAL.SERIAL_NUMBER',
candidate.serialNumber candidate.serialNumber
@@ -1,4 +1,11 @@
import { Component, EventEmitter, Output, inject, signal } from '@angular/core'; import {
Component,
EventEmitter,
Output,
inject,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistFileImportService } from '@iptvnator/playlist/shared/util'; import { PlaylistFileImportService } from '@iptvnator/playlist/shared/util';
@@ -11,6 +18,7 @@ const KB = 1024;
imports: [DragDropFileUploadDirective, MatIconModule, TranslatePipe], imports: [DragDropFileUploadDirective, MatIconModule, TranslatePipe],
selector: 'app-file-upload', selector: 'app-file-upload',
templateUrl: './file-upload.component.html', templateUrl: './file-upload.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrls: ['./file-upload.component.scss'], styleUrls: ['./file-upload.component.scss'],
}) })
export class FileUploadComponent { export class FileUploadComponent {
@@ -1,4 +1,10 @@
import { Component, inject, output, signal } from '@angular/core'; import {
Component,
inject,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
@@ -59,6 +65,7 @@ interface StalkerSettledIdentity {
], ],
selector: 'app-stalker-portal-import', selector: 'app-stalker-portal-import',
templateUrl: './stalker-portal-import.component.html', templateUrl: './stalker-portal-import.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
:host { :host {
@@ -1,4 +1,9 @@
import { Component, EventEmitter, Output } from '@angular/core'; import {
Component,
EventEmitter,
Output,
ChangeDetectionStrategy,
} from '@angular/core';
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
@@ -12,6 +17,7 @@ import { TranslatePipe } from '@ngx-translate/core';
selector: 'app-text-import', selector: 'app-text-import',
templateUrl: './text-import.component.html', templateUrl: './text-import.component.html',
styleUrls: ['./text-import.component.scss'], styleUrls: ['./text-import.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [MatInputModule, ReactiveFormsModule, TranslatePipe], imports: [MatInputModule, ReactiveFormsModule, TranslatePipe],
}) })
export class TextImportComponent { export class TextImportComponent {
@@ -1,4 +1,9 @@
import { Component, inject, OnInit } from '@angular/core'; import {
Component,
inject,
OnInit,
ChangeDetectionStrategy,
} from '@angular/core';
import { import {
FormBuilder, FormBuilder,
FormControl, FormControl,
@@ -14,6 +19,7 @@ import { RuntimeCapabilitiesService } from '@iptvnator/services';
@Component({ @Component({
selector: 'app-url-upload', selector: 'app-url-upload',
templateUrl: './url-upload.component.html', templateUrl: './url-upload.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
@@ -1,4 +1,10 @@
import { Component, EventEmitter, Output, inject } from '@angular/core'; import {
Component,
EventEmitter,
Output,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { import {
AbstractControl, AbstractControl,
FormControl, FormControl,
@@ -47,6 +53,7 @@ function xtreamServerUrlValidator(
], ],
selector: 'app-xtream-code-import', selector: 'app-xtream-code-import',
templateUrl: './xtream-code-import.component.html', templateUrl: './xtream-code-import.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
:host { :host {
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -7,6 +7,7 @@ import {
output, output,
signal, signal,
untracked, untracked,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
@@ -59,6 +60,7 @@ import { M3uVodMetadataService } from './m3u-vod-metadata.service';
TranslatePipe, TranslatePipe,
], ],
templateUrl: './m3u-vod-detail.component.html', templateUrl: './m3u-vod-detail.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrls: ['./m3u-vod-detail.component.scss'], styleUrls: ['./m3u-vod-detail.component.scss'],
}) })
export class M3uVodDetailComponent { export class M3uVodDetailComponent {
@@ -105,7 +105,9 @@
@if (activeChannel.radio === 'true') { @if (activeChannel.radio === 'true') {
<app-audio-player <app-audio-player
[url]="activeChannel.url" [url]="activeChannel.url"
[icon]="activeChannel?.tvg?.logo!" [icon]="
$safeNavigationMigration(activeChannel?.tvg?.logo)!
"
[channelName]="displayChannelName()" [channelName]="displayChannelName()"
[volume]="volume()" [volume]="volume()"
(volumeChange)="onInlineVolumeChange($event)" (volumeChange)="onInlineVolumeChange($event)"
@@ -14,6 +14,7 @@ import {
signal, signal,
untracked, untracked,
viewChild, viewChild,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -226,6 +227,7 @@ function isInsideScrollableRegion(
{ provide: EPG_GUIDE_SOURCE, useExisting: M3uEpgGuideSourceService }, { provide: EPG_GUIDE_SOURCE, useExisting: M3uEpgGuideSourceService },
], ],
templateUrl: './video-player.component.html', templateUrl: './video-player.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './video-player.component.scss', styleUrl: './video-player.component.scss',
}) })
export class VideoPlayerComponent export class VideoPlayerComponent
@@ -1,14 +1,16 @@
import { Component, input, output } from '@angular/core'; import {
Component,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
import type { PlaylistType } from '../../add-playlist-menu/playlist-type'; import type { PlaylistType } from '../../add-playlist-menu/playlist-type';
export type EmptyStateType = export type EmptyStateType =
| 'welcome-dashboard' 'welcome-dashboard' | 'welcome-sources' | 'no-results' | 'no-data';
| 'welcome-sources'
| 'no-results'
| 'no-data';
interface FeatureCard { interface FeatureCard {
icon: string; icon: string;
@@ -100,6 +102,7 @@ const SOURCE_CARDS: readonly SourceCard[] = [
'./empty-state.responsive.scss', './empty-state.responsive.scss',
'./empty-state.themes.scss', './empty-state.themes.scss',
], ],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [MatButtonModule, MatIcon, TranslatePipe], imports: [MatButtonModule, MatIcon, TranslatePipe],
}) })
export class EmptyStateComponent { export class EmptyStateComponent {
@@ -171,15 +171,9 @@ describe('PlaylistInfoComponent', () => {
currentLang: 'en', currentLang: 'en',
get: jest.fn((key: string) => of(key)), get: jest.fn((key: string) => of(key)),
instant: jest.fn((key: string) => key), instant: jest.fn((key: string) => key),
onDefaultLangChange: of({ onDefaultLangChange: new Subject(),
lang: 'en', onLangChange: new Subject(),
translations: {}, onTranslationChange: new Subject(),
}),
onLangChange: of({ lang: 'en', translations: {} }),
onTranslationChange: of({
lang: 'en',
translations: {},
}),
}, },
}, },
{ {
@@ -1,6 +1,12 @@
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'; import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, DestroyRef, inject, signal } from '@angular/core'; import {
Component,
DestroyRef,
inject,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { import {
FormControl, FormControl,
ReactiveFormsModule, ReactiveFormsModule,
@@ -159,6 +165,7 @@ type DesktopFileSaveBridge = Pick<
`, `,
], ],
providers: [DatePipe], providers: [DatePipe],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
ClipboardModule, ClipboardModule,
MatButton, MatButton,
@@ -12,6 +12,7 @@ import {
inject, inject,
input, input,
output, output,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'; import { toSignal } from '@angular/core/rxjs-interop';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
@@ -33,6 +34,7 @@ import { PlaylistMeta } from '@iptvnator/shared/interfaces';
selector: 'app-playlist-item', selector: 'app-playlist-item',
templateUrl: './playlist-item.component.html', templateUrl: './playlist-item.component.html',
styleUrls: ['./playlist-item.component.scss'], styleUrls: ['./playlist-item.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
SourceHealthIndicatorComponent, SourceHealthIndicatorComponent,
DatePipe, DatePipe,
@@ -1,4 +1,11 @@
import { Component, DestroyRef, computed, effect, inject } from '@angular/core'; import {
Component,
DestroyRef,
computed,
effect,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
@@ -107,7 +114,9 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
@if (entry.status === 'ready') { @if (entry.status === 'ready') {
<span>{{ <span>{{
'SOURCE_HEALTH.REASON.' + 'SOURCE_HEALTH.REASON.' +
entry.health?.reason | translate $safeNavigationMigration(
entry.health?.reason
) | translate
}}</span> }}</span>
} @else { } @else {
<span>{{ <span>{{
@@ -117,8 +126,9 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
} }
@if (entry.health?.checkedAt) { @if (entry.health?.checkedAt) {
<time>{{ <time>{{
$safeNavigationMigration(
entry.health?.checkedAt entry.health?.checkedAt
| date: 'shortTime' ) | date: 'shortTime'
}}</time> }}</time>
} }
@if (entry.warning) { @if (entry.warning) {
@@ -170,6 +180,7 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
</button> </button>
} }
</mat-dialog-actions>`, </mat-dialog-actions>`,
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
:host { :host {
@@ -6,6 +6,7 @@ import {
inject, inject,
input, input,
untracked, untracked,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -28,6 +29,7 @@ import { PlaylistMeta, sourceHealthType } from '@iptvnator/shared/interfaces';
role="img" role="img"
></span> ></span>
}`, }`,
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
:host { :host {
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -1,4 +1,10 @@
import { Component, input, output, signal } from '@angular/core'; import {
Component,
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { NgComponentOutlet } from '@angular/common'; import { NgComponentOutlet } from '@angular/common';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
@@ -21,6 +27,7 @@ import { CategoryContentViewComponent } from './category-content-view.component'
@Component({ @Component({
selector: 'app-grid-list', selector: 'app-grid-list',
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.Eager,
template: '', template: '',
}) })
class MockGridListComponent { class MockGridListComponent {
@@ -37,6 +44,7 @@ class MockGridListComponent {
@Component({ @Component({
selector: 'app-playlist-error-view', selector: 'app-playlist-error-view',
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.Eager,
template: '', template: '',
}) })
class MockPlaylistErrorViewComponent { class MockPlaylistErrorViewComponent {
@@ -48,6 +56,7 @@ class MockPlaylistErrorViewComponent {
@Component({ @Component({
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.Eager,
template: '', template: '',
}) })
class MockDetailComponent { class MockDetailComponent {
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -312,7 +312,10 @@
{{ {{
episodeTitle( episodeTitle(
episode.item, episode.item,
episode.episodeMetadata?.title $safeNavigationMigration(
episode.episodeMetadata
?.title
)
) )
}} }}
</strong> </strong>
@@ -360,7 +363,10 @@
' ' + ' ' +
episodeTitle( episodeTitle(
episode.item, episode.item,
episode.episodeMetadata?.title $safeNavigationMigration(
episode.episodeMetadata
?.title
)
) )
" "
(click)=" (click)="
@@ -390,7 +396,10 @@
': ' + ': ' +
episodeTitle( episodeTitle(
episode.item, episode.item,
episode.episodeMetadata?.title $safeNavigationMigration(
episode.episodeMetadata
?.title
)
) )
" "
> >
@@ -418,9 +427,11 @@
' ' + ' ' +
episodeTitle( episodeTitle(
episode.item, episode.item,
$safeNavigationMigration(
episode.episodeMetadata episode.episodeMetadata
?.title ?.title
) )
)
" "
(click)=" (click)="
runFileAction( runFileAction(
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -1,4 +1,10 @@
import { Component, inject, input, output } from '@angular/core'; import {
Component,
inject,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
@@ -50,6 +56,7 @@ export interface FavoriteLayoutItem {
'./favorites-layout.component.scss', './favorites-layout.component.scss',
'../../styles/portal-sidebar.scss', '../../styles/portal-sidebar.scss',
], ],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ imports: [
CategoryViewComponent, CategoryViewComponent,
ContentCardComponent, ContentCardComponent,
@@ -1,4 +1,9 @@
import { Component, inject, input } from '@angular/core'; import {
Component,
inject,
input,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -14,6 +19,7 @@ import { PlaylistActions } from '@iptvnator/m3u-state';
selector: 'app-playlist-error-view', selector: 'app-playlist-error-view',
templateUrl: './playlist-error-view.component.html', templateUrl: './playlist-error-view.component.html',
styleUrls: ['./playlist-error-view.component.scss'], styleUrls: ['./playlist-error-view.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule], imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
}) })
export class PlaylistErrorViewComponent { export class PlaylistErrorViewComponent {
@@ -6,6 +6,7 @@ import {
model, model,
Output, Output,
ViewChild, ViewChild,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -38,6 +39,7 @@ export interface SearchFilter {
TranslatePipe, TranslatePipe,
], ],
templateUrl: './search-form.component.html', templateUrl: './search-form.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
.search-container { .search-container {
@@ -1,4 +1,4 @@
import { Component, signal } from '@angular/core'; import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { InfiniteScrollDirective } from './infinite-scroll.directive'; import { InfiniteScrollDirective } from './infinite-scroll.directive';
@@ -13,6 +13,7 @@ import { InfiniteScrollDirective } from './infinite-scroll.directive';
[infiniteResetKey]="resetKey()" [infiniteResetKey]="resetKey()"
(infiniteLoadMore)="onLoadMore()" (infiniteLoadMore)="onLoadMore()"
></div>`, ></div>`,
changeDetection: ChangeDetectionStrategy.Eager,
imports: [InfiniteScrollDirective], imports: [InfiniteScrollDirective],
}) })
class TestHostComponent { class TestHostComponent {
@@ -1,4 +1,9 @@
import { Component, inject, input } from '@angular/core'; import {
Component,
inject,
input,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
@@ -8,12 +13,16 @@ import {
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
} from '@angular/router'; } from '@angular/router';
import { PortalRailLink, PortalRailSection } from '@iptvnator/portal/shared/util'; import {
PortalRailLink,
PortalRailSection,
} from '@iptvnator/portal/shared/util';
@Component({ @Component({
selector: 'app-portal-rail-links', selector: 'app-portal-rail-links',
imports: [MatIcon, MatListModule, MatTooltip, RouterLink, RouterLinkActive], imports: [MatIcon, MatListModule, MatTooltip, RouterLink, RouterLinkActive],
templateUrl: './portal-rail-links.component.html', templateUrl: './portal-rail-links.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './portal-rail-links.component.scss', styleUrl: './portal-rail-links.component.scss',
}) })
export class PortalRailLinksComponent { export class PortalRailLinksComponent {
@@ -27,7 +36,9 @@ export class PortalRailLinksComponent {
readonly variant = input<'list' | 'rail'>('list'); readonly variant = input<'list' | 'rail'>('list');
resolveIcon(link: PortalRailLink): string { resolveIcon(link: PortalRailLink): string {
const normalizedPath = link.path.map((segment) => String(segment)).join('/'); const normalizedPath = link.path
.map((segment) => String(segment))
.join('/');
if (normalizedPath.includes('/workspace/sources')) { if (normalizedPath.includes('/workspace/sources')) {
return 'playlist_play'; return 'playlist_play';
} }
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; // Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({ setupZonelessTestEnv({
errorOnUnknownElements: true, errorOnUnknownElements: true,
errorOnUnknownProperties: true, errorOnUnknownProperties: true,
}); });
@@ -10,7 +10,8 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false, "emitDecoratorMetadata": false,
"module": "preserve" "module": "preserve",
"ignoreDeprecations": "6.0"
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
@@ -1,5 +1,11 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component, input, output, signal } from '@angular/core'; import {
Component,
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
@@ -33,7 +39,11 @@ function deferred<T>() {
return { promise, reject, resolve }; return { promise, reject, resolve };
} }
@Component({ selector: 'app-vod-details', template: '' }) @Component({
selector: 'app-vod-details',
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class StubVodDetailsComponent { class StubVodDetailsComponent {
readonly playbackSessionKey = input.required<string>(); readonly playbackSessionKey = input.required<string>();
readonly item = input.required<unknown>(); readonly item = input.required<unknown>();
@@ -53,7 +63,11 @@ class StubVodDetailsComponent {
readonly inlineExternalFallbackRequested = output<unknown>(); readonly inlineExternalFallbackRequested = output<unknown>();
} }
@Component({ selector: 'app-stalker-series-view', template: '' }) @Component({
selector: 'app-stalker-series-view',
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class StubStalkerSeriesViewComponent { class StubStalkerSeriesViewComponent {
readonly vodWithSeries = input<unknown>(null); readonly vodWithSeries = input<unknown>(null);
readonly providerOnly = input(false); readonly providerOnly = input(false);
@@ -6,6 +6,7 @@ import {
inject, inject,
input, input,
signal, signal,
ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
@@ -55,6 +56,7 @@ import { createPlaybackSessionKey } from '@iptvnator/playback/util';
selector: 'app-stalker-catalog-detail', selector: 'app-stalker-catalog-detail',
imports: [StalkerSeriesViewComponent, VodDetailsComponent], imports: [StalkerSeriesViewComponent, VodDetailsComponent],
templateUrl: './stalker-catalog-detail.component.html', templateUrl: './stalker-catalog-detail.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [ styles: [
` `
:host { :host {
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff. Show more