Files
iptvnator/apps/web/src/app/settings/settings-tmdb-section.component.spec.ts
T
4grayandClaude Fable 5 f7bb3a13db feat(playlist): open recognized M3U movies in the VOD detail view (#1420)
M3U entries recognized as movie files now open in the portals' two-state VOD
detail view, fed by TMDB metadata instead of the empty EPG zone. Watch-first:
activation still plays immediately, with plot, cast, rating and artwork below
the player; Escape reveals the Browse hero.

Recognition is a synchronous URL-shape heuristic (movie container extension or
an Xtream-style /movie/ path; radio, DASH, /series/ paths and episode-marker
names keep today's live layout), gated on TMDB enrichment plus the new
default-on Settings.m3uVodDetails toggle. Works in Electron and the PWA.

Review follow-ups included: the playback payload no longer carries TMDB fields
(its identity is the player's source-application key), the persisted volume
reaches the player and survives Browse → Play, the enrichment guard keys on
the full lookup identity, and the saved engine mounts first time instead of
briefly falling back to Video.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:18:06 +02:00

194 lines
6.5 KiB
TypeScript

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { TmdbApiService, TmdbCacheService } from '@iptvnator/services';
import { SettingsTmdbSectionComponent } from './settings-tmdb-section.component';
const CACHE_SIZE_LABEL = '{{entries}} entries · {{size}}';
const CACHE_ERROR_LABEL = 'Could not read the cache';
describe('SettingsTmdbSectionComponent', () => {
let fixture: ComponentFixture<SettingsTmdbSectionComponent>;
let getStats: jest.Mock;
let clear: jest.Mock;
const createForm = () =>
new FormGroup({
m3uVodDetails: new FormControl(true),
tmdb: new FormGroup({
enabled: new FormControl(true),
apiKey: new FormControl(''),
}),
});
const queryByTestId = (testId: string): HTMLElement | null =>
fixture.nativeElement.querySelector(`[data-test-id="${testId}"]`);
const clearButton = () =>
queryByTestId('tmdb-clear-cache') as HTMLButtonElement;
/** Constructor kicks off the stats read; let it settle and render. */
const settle = async () => {
await fixture.whenStable();
fixture.detectChanges();
};
/**
* The section component only exists while its settings page is open, so
* "reopening the section" means recreating the component.
*/
const reopenSection = async () => {
fixture.destroy();
fixture = TestBed.createComponent(SettingsTmdbSectionComponent);
fixture.componentRef.setInput('form', createForm());
fixture.detectChanges();
await settle();
};
beforeEach(async () => {
getStats = jest.fn().mockResolvedValue({ entries: 42, bytes: 2048 });
clear = jest.fn().mockResolvedValue(42);
await TestBed.configureTestingModule({
imports: [
SettingsTmdbSectionComponent,
NoopAnimationsModule,
ReactiveFormsModule,
TranslateModule.forRoot(),
],
providers: [
{
provide: TmdbCacheService,
useValue: { getStats, clear },
},
{
provide: TmdbApiService,
useValue: { validateApiKey: jest.fn() },
},
],
}).compileComponents();
const translate = TestBed.inject(TranslateService);
translate.setTranslation(
'en',
{
SETTINGS: {
TMDB_CACHE_SIZE: CACHE_SIZE_LABEL,
TMDB_CACHE_ERROR: CACHE_ERROR_LABEL,
},
},
true
);
translate.use('en');
fixture = TestBed.createComponent(SettingsTmdbSectionComponent);
fixture.componentRef.setInput('form', createForm());
fixture.detectChanges();
});
it('offers the M3U movie-recognition toggle while TMDB is enabled', async () => {
await settle();
const checkbox = queryByTestId('tmdb-m3u-vod-details');
expect(checkbox).not.toBeNull();
(checkbox?.querySelector('input') as HTMLInputElement).click();
fixture.detectChanges();
expect(fixture.componentInstance.m3uVodDetailsControl?.value).toBe(
false
);
});
it('hides the M3U toggle when TMDB is disabled', async () => {
await settle();
fixture.componentInstance.form().get('tmdb.enabled')?.setValue(false);
fixture.detectChanges();
expect(queryByTestId('tmdb-m3u-vod-details')).toBeNull();
});
it('hides the M3U toggle row when the host form lacks the control', async () => {
fixture.destroy();
fixture = TestBed.createComponent(SettingsTmdbSectionComponent);
fixture.componentRef.setInput(
'form',
new FormGroup({
tmdb: new FormGroup({
enabled: new FormControl(true),
apiKey: new FormControl(''),
}),
})
);
fixture.detectChanges();
await settle();
expect(queryByTestId('tmdb-m3u-vod-details')).toBeNull();
});
it('sizes the cache as soon as the section page opens', async () => {
await settle();
expect(getStats).toHaveBeenCalledTimes(1);
expect(queryByTestId('tmdb-cache-size')?.textContent).toContain('42');
});
it('clears the cache and re-reads the size', async () => {
await settle();
getStats.mockResolvedValue({ entries: 0, bytes: 0 });
clearButton().click();
await fixture.whenStable();
fixture.detectChanges();
expect(clear).toHaveBeenCalledTimes(1);
expect(getStats).toHaveBeenCalledTimes(2);
// Nothing left to clear — the button stops offering it
expect(clearButton().disabled).toBe(true);
});
it('surfaces a failed clear instead of claiming an empty cache', async () => {
await settle();
clear.mockResolvedValue(null);
clearButton().click();
await fixture.whenStable();
fixture.detectChanges();
expect(queryByTestId('tmdb-cache-size')?.textContent).toContain(
CACHE_ERROR_LABEL
);
// Still actionable: the rows are there, the user can retry
expect(clearButton().disabled).toBe(false);
});
it('surfaces a failed size read the same way', async () => {
getStats.mockReset();
getStats.mockResolvedValue(null);
await reopenSection();
expect(queryByTestId('tmdb-cache-size')?.textContent).toContain(
CACHE_ERROR_LABEL
);
});
it('retries the size read next time the section is opened', async () => {
getStats.mockReset();
getStats.mockResolvedValueOnce(null);
getStats.mockResolvedValue({ entries: 42, bytes: 2048 });
await reopenSection();
expect(queryByTestId('tmdb-cache-size')?.textContent).toContain(
CACHE_ERROR_LABEL
);
// Otherwise a single transient failure sticks until the destructive
// Clear button shifts it — reopening the page must retry instead
await reopenSection();
expect(getStats).toHaveBeenCalledTimes(2);
expect(queryByTestId('tmdb-cache-size')?.textContent).toContain('42');
});
});