From d24c67c6e8069678ce5637eb4729d8df4a1a1741 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 1 May 2026 19:34:45 +0200 Subject: [PATCH] test(workspace-shell): repair stale component-spec mocks and cover new overlay child The two component specs in this lib had been carrying 7 long-standing failures rooted in three stale-mock issues, all unrelated to public behaviour but enough to make the suite red on every run. Header spec (5 failures, NG0303 on app-playlist-switcher): - MockPlaylistSwitcherComponent was missing canRefreshActivePlaylist / isRefreshingActivePlaylist inputs and refreshPlaylistRequested output. The real PlaylistSwitcherComponent grew them when refresh was added to the header; the mock never caught up. Re-aligned. Workspace-shell component spec (2 failures): - NG0301 on #dropZone="playlistDropZone" because overrideComponent's set.imports list dropped PlaylistDropZoneDirective and the drop overlay component. Added MockPlaylistDropZoneDirective and MockPlaylistDropOverlayComponent. - After the recent option-D refactor that lifted the import overlay into its own child component, the spec also needed MockWorkspaceShellImportOverlayComponent so the parent template could resolve . - MockWorkspaceShellHeaderComponent gained hasNoPlaylists input and MockWorkspaceShellContextSidebarComponent gained hasPlaylists input to match the real components. - The "renders type-aware xtream import progress copy" test asserted on inline overlay markup that no longer lives in the parent template. Replaced with a structural test that the overlay child renders/unmounts based on facade.showXtreamImportOverlay(). New focused spec for WorkspaceShellImportOverlayComponent (6 tests) covers the moved progress-copy assertion at the right level by mocking the WorkspaceShellXtreamImportService directly: title, progress copy, determinate vs indeterminate progress bar, badge tone, and cancel forwarding. Final state: full feature lib suite is 66/66 (was 53/60 with 7 chronic failures). pnpm nx test web, pnpm nx lint workspace-shell-feature, pnpm nx build web --configuration=production, and the basic + settings web-e2e suites all pass. Co-Authored-By: Claude Opus 4.7 (1M context) Entire-Checkpoint: c2cea9c530e6 --- .../workspace-shell-header.component.spec.ts | 3 + ...ace-shell-import-overlay.component.spec.ts | 146 ++++++++++++++++++ .../workspace-shell.component.spec.ts | 60 +++++-- 3 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.spec.ts diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.spec.ts index b2257bbaa..2c7e1b96e 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.spec.ts @@ -24,9 +24,12 @@ class MockPlaylistSwitcherComponent { readonly showPlaylistInfo = input(false); readonly showAccountInfo = input(false); readonly showAddPlaylist = input(false); + readonly canRefreshActivePlaylist = input(false); + readonly isRefreshingActivePlaylist = input(false); readonly playlistInfoRequested = output(); readonly accountInfoRequested = output(); readonly addPlaylistRequested = output(); + readonly refreshPlaylistRequested = output(); } describe('WorkspaceShellHeaderComponent', () => { diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.spec.ts new file mode 100644 index 000000000..fbf8b6811 --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.spec.ts @@ -0,0 +1,146 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { WorkspaceShellXtreamImportService } from '../../services/workspace-shell-xtream-import.service'; +import { WorkspaceShellImportOverlayComponent } from './workspace-shell-import-overlay.component'; + +class MockWorkspaceShellXtreamImportService { + readonly xtreamImportSourceLabel = signal(''); + readonly xtreamImportPhaseTone = signal<'remote' | 'local' | null>(null); + readonly xtreamImportTitleLabel = signal('Import title'); + readonly xtreamImportPhaseLabel = signal(''); + readonly xtreamImportDetailLabel = signal(''); + readonly xtreamImportProgressLabel = signal(''); + readonly xtreamActiveImportCount = signal(0); + readonly xtreamActiveItemsToImport = signal(0); + readonly canCancelXtreamImport = signal(false); + readonly isCancellingXtreamImport = signal(false); + + cancelXtreamImport = jest.fn(); +} + +describe('WorkspaceShellImportOverlayComponent', () => { + let importService: MockWorkspaceShellXtreamImportService; + + beforeEach(async () => { + importService = new MockWorkspaceShellXtreamImportService(); + + await TestBed.configureTestingModule({ + imports: [ + WorkspaceShellImportOverlayComponent, + NoopAnimationsModule, + ], + providers: [ + { + provide: WorkspaceShellXtreamImportService, + useValue: importService, + }, + { + provide: TranslateService, + useValue: { + instant: (key: string) => key, + get: (key: string) => of(key), + stream: (key: string) => of(key), + onLangChange: of(null), + onTranslationChange: of(null), + onDefaultLangChange: of(null), + currentLang: 'en', + defaultLang: 'en', + }, + }, + ], + }).compileComponents(); + }); + + it('renders the title label', () => { + const fixture = TestBed.createComponent( + WorkspaceShellImportOverlayComponent + ); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + '.workspace-loading-overlay__card h3' + ).textContent + ).toContain('Import title'); + }); + + it('renders the progress copy when the service exposes one', () => { + importService.xtreamImportProgressLabel.set('Movies imported: 20 / 12,323'); + + const fixture = TestBed.createComponent( + WorkspaceShellImportOverlayComponent + ); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + '.workspace-loading-overlay__progress-copy' + )?.textContent + ).toContain('Movies imported: 20 / 12,323'); + }); + + it('renders a determinate progress bar when current and total are non-zero', () => { + importService.xtreamActiveImportCount.set(20); + importService.xtreamActiveItemsToImport.set(100); + + const fixture = TestBed.createComponent( + WorkspaceShellImportOverlayComponent + ); + fixture.detectChanges(); + + const progress = fixture.nativeElement.querySelector( + 'mat-progress-bar' + ); + expect(progress).not.toBeNull(); + expect(progress.getAttribute('mode')).toBe('determinate'); + }); + + it('renders an indeterminate progress bar when totals are unknown', () => { + const fixture = TestBed.createComponent( + WorkspaceShellImportOverlayComponent + ); + fixture.detectChanges(); + + const progress = fixture.nativeElement.querySelector( + 'mat-progress-bar' + ); + expect(progress.getAttribute('mode')).toBe('indeterminate'); + }); + + it('toggles the remote badge modifier when phase tone is remote', () => { + importService.xtreamImportSourceLabel.set('REMOTE'); + importService.xtreamImportPhaseTone.set('remote'); + + const fixture = TestBed.createComponent( + WorkspaceShellImportOverlayComponent + ); + fixture.detectChanges(); + + const badge = fixture.nativeElement.querySelector( + '.workspace-loading-overlay__badge' + ); + expect(badge).not.toBeNull(); + expect( + badge.classList.contains('workspace-loading-overlay__badge--remote') + ).toBe(true); + }); + + it('forwards cancel clicks to the import service', () => { + importService.canCancelXtreamImport.set(true); + + const fixture = TestBed.createComponent( + WorkspaceShellImportOverlayComponent + ); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector( + '.workspace-loading-overlay__action' + ) as HTMLButtonElement; + button.click(); + + expect(importService.cancelXtreamImport).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts index de3de2f9a..ac7d67980 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.spec.ts @@ -1,11 +1,11 @@ import { Component, + Directive, input, output, signal, } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; import { RouterOutlet, provideRouter } from '@angular/router'; import { WorkspacePortalContext, @@ -54,6 +54,7 @@ class MockWorkspaceShellHeaderComponent { readonly canRefreshPlaylist = input(false); readonly isRefreshingPlaylist = input(false); readonly isElectron = input(false); + readonly hasNoPlaylists = input(false); readonly isDownloadsView = input(false); readonly hasActiveDownloads = input(false); readonly headerBulkAction = input(null); @@ -78,6 +79,7 @@ class MockWorkspaceShellContextSidebarComponent { readonly variant = input('none'); readonly context = input(null); readonly section = input(null); + readonly hasPlaylists = input(false); } @Component({ @@ -90,6 +92,31 @@ class MockExternalPlaybackDockComponent { readonly closeClicked = output(); } +@Component({ + selector: 'app-playlist-drop-overlay', + template: '', + standalone: true, +}) +class MockPlaylistDropOverlayComponent { + readonly state = input({ kind: 'idle' }); +} + +@Directive({ + selector: '[appPlaylistDropZone]', + exportAs: 'playlistDropZone', + standalone: true, +}) +class MockPlaylistDropZoneDirective { + readonly overlayState = signal({ kind: 'idle' }); +} + +@Component({ + selector: 'app-workspace-shell-import-overlay', + template: '', + standalone: true, +}) +class MockWorkspaceShellImportOverlayComponent {} + class MockWorkspaceShellFacade { readonly brandLink = signal('/workspace/dashboard'); readonly brandTooltipKey = signal('WORKSPACE.SHELL.RAIL_DASHBOARD'); @@ -114,6 +141,7 @@ class MockWorkspaceShellFacade { readonly headerShortcut = signal(null); readonly canRefreshPlaylist = signal(false); readonly isRefreshingPlaylist = signal(false); + readonly hasNoPlaylists = signal(false); readonly isDownloadsView = signal(false); readonly hasActiveDownloads = signal(false); readonly headerBulkAction = signal(null); @@ -173,11 +201,13 @@ describe('WorkspaceShellComponent', () => { .overrideComponent(WorkspaceShellComponent, { set: { imports: [ - MatProgressBarModule, RouterOutlet, MockExternalPlaybackDockComponent, + MockPlaylistDropOverlayComponent, + MockPlaylistDropZoneDirective, MockWorkspaceShellContextSidebarComponent, MockWorkspaceShellHeaderComponent, + MockWorkspaceShellImportOverlayComponent, MockWorkspaceShellRailComponent, ], providers: [ @@ -210,7 +240,7 @@ describe('WorkspaceShellComponent', () => { ).not.toBeNull(); }); - it('renders type-aware xtream import progress copy in the overlay', async () => { + it('renders the xtream import overlay child only when the facade flag is true', async () => { const facade = new MockWorkspaceShellFacade(); await TestBed.configureTestingModule({ @@ -220,11 +250,13 @@ describe('WorkspaceShellComponent', () => { .overrideComponent(WorkspaceShellComponent, { set: { imports: [ - MatProgressBarModule, RouterOutlet, MockExternalPlaybackDockComponent, + MockPlaylistDropOverlayComponent, + MockPlaylistDropZoneDirective, MockWorkspaceShellContextSidebarComponent, MockWorkspaceShellHeaderComponent, + MockWorkspaceShellImportOverlayComponent, MockWorkspaceShellRailComponent, ], providers: [ @@ -237,18 +269,22 @@ describe('WorkspaceShellComponent', () => { }) .compileComponents(); - facade.showXtreamImportOverlay.set(true); - facade.xtreamActiveImportCount.set(20); - facade.xtreamActiveItemsToImport.set(12323); - facade.xtreamImportProgressLabel.set('Movies imported: 20 / 12,323'); - const fixture = TestBed.createComponent(WorkspaceShellComponent); fixture.detectChanges(); expect( fixture.nativeElement.querySelector( - '.workspace-loading-overlay__progress-copy' - )?.textContent - ).toContain('Movies imported: 20 / 12,323'); + 'app-workspace-shell-import-overlay' + ) + ).toBeNull(); + + facade.showXtreamImportOverlay.set(true); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + 'app-workspace-shell-import-overlay' + ) + ).not.toBeNull(); }); });