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(); }); });