chore: tighten validation and runtime logging

* chore: tighten validation and runtime logging

* fix(i18n): localize new settings labels
This commit is contained in:
4gray authored and GitHub committed 2026-05-15 17:43:42 +02:00
1 parent d24c77a143
commit 2d5c4fa4f9
52 files changed
+767 -214

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
export default {
displayName: 'epg-data-access',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/epg/data-access',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
moduleFileExtensions: ['ts', 'js', 'html'],
};
+8
View File
@@ -6,6 +6,14 @@
"projectType": "library",
"tags": ["scope:shared", "domain:playback", "type:data-access"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/epg/data-access/jest.config.ts",
"tsConfig": "libs/epg/data-access/tsconfig.spec.json"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
@@ -1,119 +1,45 @@
/* import { TestBed } from '@angular/core/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { invoke } from '@tauri-apps/api/core';
import { MockModule, MockProviders } from 'ng-mocks';
import { DataService } from './data.service';
import { firstValueFrom } from 'rxjs';
import { EpgService } from './epg.service';
jest.mock('@tauri-apps/api/core', () => ({
invoke: jest.fn(),
isTauri: () => true,
}));
describe('EpgService', () => {
let service: EpgService;
let snackBar: MatSnackBar;
let store: MockStore;
let translateService: TranslateService;
let dispatchSpy: jest.SpyInstance;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
EpgService,
MockProviders(DataService, TranslateService, MatSnackBar),
provideMockStore(),
{
provide: MatSnackBar,
useValue: {
open: jest.fn(),
},
},
{
provide: TranslateService,
useValue: {
instant: (key: string) => key,
},
},
],
imports: [MockModule(MatSnackBarModule)],
});
service = TestBed.inject(EpgService);
snackBar = TestBed.inject(MatSnackBar);
store = TestBed.inject(MockStore);
translateService = TestBed.inject(TranslateService);
});
jest.spyOn(translateService, 'instant').mockImplementation(
(key) => key
it('returns an empty batch result when the desktop bridge is unavailable', async () => {
const result = await firstValueFrom(
service.getCurrentProgramsForChannels(['channel-1'])
);
jest.spyOn(snackBar, 'open');
dispatchSpy = jest.spyOn(store, 'dispatch');
// Suppress console.error during tests
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {});
expect(result).toEqual(new Map());
});
afterEach(() => {
jest.clearAllMocks();
});
it('should create a service instance', () => {
expect(service).toBeTruthy();
});
describe('fetchEpg', () => {
it('should show success notification on successful fetch', async () => {
(invoke as jest.Mock).mockResolvedValueOnce({});
service.fetchEpg(['http://example.com/epg.xml']);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(snackBar.open).toHaveBeenCalledWith(
'EPG.FETCH_SUCCESS',
null,
expect.any(Object)
);
});
it('should show error notification on fetch failure', async () => {
(invoke as jest.Mock).mockRejectedValueOnce(new Error('Failed'));
service.fetchEpg(['http://example.com/epg.xml']);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(snackBar.open).toHaveBeenCalledWith(
'EPG.ERROR',
'CLOSE',
expect.any(Object)
);
});
});
describe('getChannelPrograms', () => {
it('should update programs and set EPG flag to true when programs exist', (done) => {
const mockPrograms = [
{
start: new Date().toISOString(),
stop: new Date().toISOString(),
title: 'Test',
},
];
(invoke as jest.Mock).mockResolvedValueOnce(mockPrograms);
service.getChannelPrograms('test-channel');
service.currentEpgPrograms$.subscribe((programs) => {
expect(programs).toHaveLength(1);
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({ value: true })
);
done();
});
});
it('should set EPG flag to false when no programs found', (done) => {
(invoke as jest.Mock).mockResolvedValueOnce([]);
service.getChannelPrograms('test-channel');
service.currentEpgPrograms$.subscribe((programs) => {
expect(programs).toHaveLength(0);
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({ value: false })
);
done();
});
});
it('returns null for current program lookup when the desktop bridge is unavailable', async () => {
await expect(
firstValueFrom(service.getCurrentProgramForChannel('channel-1'))
).resolves.toBeNull();
});
});
*/
+8 -2
View File
@@ -3,7 +3,11 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, forkJoin, from, Observable, of } from 'rxjs';
import { catchError, map, tap, timeout } from 'rxjs/operators';
import { EpgChannelMetadata, EpgProgram } from '@iptvnator/shared/interfaces';
import {
createDevLogger,
EpgChannelMetadata,
EpgProgram,
} from '@iptvnator/shared/interfaces';
import { normalizeEpgPrograms } from './epg-program-normalization.util';
interface CachedProgram {
@@ -17,6 +21,8 @@ type EpgChannelMetadataApi = {
) => Promise<Record<string, EpgChannelMetadata | null>>;
};
const debugEpgService = createDevLogger('EpgService');
@Injectable({
providedIn: 'root',
})
@@ -71,7 +77,7 @@ export class EpgService {
*/
getChannelPrograms(channelId: string): void {
if (!this.isDesktop) return;
console.log('Fetching EPG for channel ID:', channelId);
debugEpgService('Fetching EPG for channel ID:', channelId);
from(window.electron.getChannelPrograms(channelId))
.pipe(
+6
View File
@@ -0,0 +1,6 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../../../tsconfig.base.json",
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"isolatedModules": true,
"target": "es2022",
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"],
"moduleResolution": "node10"
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}