test(stalker): move the context-menu spec into a focused spec file

The main layout spec was one effective line under the 1200 max-lines
test budget; the new context-menu test tipped it over and failed CI
lint. Following the playback-session-key precedent, the test moves into
its own template-less spec — which also restores the negative assertion,
since this harness runs without EPG-mapping support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-08 22:16:40 +02:00
1 parent 9028f919fb
commit cde26baffd
2 files changed
+164 -34

No files matched your search

@@ -17,11 +17,7 @@ import {
ResizableDirective,
} from '@iptvnator/portal/shared/util';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import {
EpgItemDescriptionComponent,
EpgListViewComponent,
EpgTimelineComponent,
} from '@iptvnator/ui/epg';
import { EpgListViewComponent, EpgTimelineComponent } from '@iptvnator/ui/epg';
import {
AudioPlayerComponent,
type PlaybackFallbackRequest,
@@ -463,35 +459,6 @@ describe('StalkerLiveStreamLayoutComponent', () => {
window.electron = originalElectron;
});
it('offers programme details from the row context menu when a preview program exists', () => {
fixture.detectChanges();
const program = {
title: 'Current Show',
desc: 'Current description',
channel: 'stalker-10',
start: '2026-04-05 05:30:00',
stop: '2026-04-05 06:00:00',
category: null,
} as never;
component.epgPreviewPrograms.set('10', program);
const rowWithProgram = { id: '10', name: 'Chan 10' } as never;
expect(component.hasChannelContextMenu(rowWithProgram)).toBe(true);
component.contextMenuChannel.set(rowWithProgram);
expect(component.contextMenuProgram()).toBe(program);
component.openProgramDetails();
const dialog = TestBed.inject(MatDialog) as unknown as {
open: jest.Mock;
};
expect(dialog.open).toHaveBeenCalledWith(EpgItemDescriptionComponent, {
data: program,
});
});
it('renders the controlled epg list and removes the load-more button', () => {
fixture.detectChanges();
@@ -0,0 +1,163 @@
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access';
import {
LiveLayoutSidebarStateService,
PORTAL_PLAYER,
} from '@iptvnator/portal/shared/util';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import {
PlaylistsService,
RuntimeCapabilitiesService,
SettingsStore,
} from '@iptvnator/services';
import { EpgItemDescriptionComponent } from '@iptvnator/ui/epg';
import { ElectronStreamHeadersService } from '@iptvnator/ui/playback';
import { StalkerLiveStreamLayoutComponent } from './stalker-live-stream-layout.component';
describe('StalkerLiveStreamLayoutComponent row context menu', () => {
let fixture: ComponentFixture<StalkerLiveStreamLayoutComponent>;
let component: StalkerLiveStreamLayoutComponent;
const playlist = signal({ _id: 'playlist-one', title: 'Portal One' });
const channels = [
{
id: '10',
cmd: 'ffrt4://itv/10',
name: 'Chan 10',
o_name: 'Chan 10',
logo: 'ten.png',
},
];
const store = {
getSelectedCategoryName: signal('All'),
currentPlaylist: playlist,
selectedContentType: signal<'itv' | 'radio'>('itv'),
selectedCategoryId: signal<string | null>('all'),
selectedItvId: signal<string | undefined>(channels[0].id),
selectedItem: signal<(typeof channels)[number] | null>(null),
itvChannels: signal(channels),
radioChannels: signal([]),
searchPhrase: signal(''),
hasMoreChannels: signal(false),
itvFullListActive: signal(false),
itvSelectedCategoryFromCache: signal(false),
itvFullListLoading: signal(false),
itvFullListProgress: signal(null),
itvFullChannelList: signal([]),
isPaginatedContentLoading: signal(false),
selectedItvEpgPrograms: signal([]),
bulkItvEpgByChannel: signal({}),
isLoadingBulkItvEpg: signal(false),
setItvChannels: jest.fn(),
setRadioChannels: jest.fn(),
setPage: jest.fn(),
preloadItvChannels: jest.fn(),
applyMappedItvEpg: jest.fn(),
clearBulkItvEpgCache: jest.fn(),
ensureBulkItvEpg: jest.fn(),
fetchChannelEpg: jest.fn(),
resolveItvPlayback: jest.fn(),
resolveRadioPlayback: jest.fn(),
addToFavorites: jest.fn(),
removeFromFavorites: jest.fn(),
setSelectedItem: jest.fn(),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StalkerLiveStreamLayoutComponent],
providers: [
{ provide: StalkerStore, useValue: store },
{
provide: RuntimeCapabilitiesService,
useValue: {
supportsEpg: false,
isElectron: false,
supportsEpgMapping: false,
},
},
{
provide: PlaylistsService,
useValue: { getPortalFavorites: () => of([]) },
},
{
provide: SettingsStore,
useValue: { openStreamOnDoubleClick: signal(false) },
},
{
provide: PORTAL_PLAYER,
useValue: {
isEmbeddedPlayer: () => true,
openResolvedPlayback: jest.fn(),
},
},
{
provide: ElectronStreamHeadersService,
useValue: { apply: jest.fn(), clear: jest.fn() },
},
{
provide: LiveLayoutSidebarStateService,
useValue: { isCollapsed: signal(false), toggle: jest.fn() },
},
{ provide: EpgRuntimeBridgeService, useValue: {} },
{ provide: MatDialog, useValue: { open: jest.fn() } },
{ provide: MatSnackBar, useValue: { open: jest.fn() } },
{
provide: TranslateService,
useValue: { instant: (key: string) => key },
},
],
})
.overrideComponent(StalkerLiveStreamLayoutComponent, {
set: { template: '' },
})
.compileComponents();
fixture = TestBed.createComponent(StalkerLiveStreamLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => fixture.destroy());
it('offers programme details exactly for rows with a preview program', () => {
const program = {
title: 'Current Show',
desc: 'Current description',
channel: 'stalker-10',
start: '2026-04-05 05:30:00',
stop: '2026-04-05 06:00:00',
category: null,
} as never;
component.epgPreviewPrograms.set('10', program);
const rowWithProgram = { id: '10', name: 'Chan 10' } as never;
const rowWithoutProgram = { id: '11', name: 'Chan 11' } as never;
// supportsEpgMapping is false in this harness, so the programme is
// the only thing that can justify a context menu.
expect(component.hasChannelContextMenu(rowWithProgram)).toBe(true);
expect(component.hasChannelContextMenu(rowWithoutProgram)).toBe(false);
component.contextMenuChannel.set(rowWithProgram);
expect(component.contextMenuProgram()).toBe(program);
// The empty test template renders no menu trigger — stub it so the
// action can close the menu it was invoked from.
const closeMenu = jest.fn();
Object.defineProperty(component, 'contextMenuTrigger', {
value: () => ({ closeMenu }),
});
component.openProgramDetails();
expect(closeMenu).toHaveBeenCalled();
const dialog = TestBed.inject(MatDialog) as unknown as {
open: jest.Mock;
};
expect(dialog.open).toHaveBeenCalledWith(EpgItemDescriptionComponent, {
data: program,
});
});
});