mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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>
194 lines
6.5 KiB
TypeScript
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');
|
|
});
|
|
});
|