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

@@ -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,
});