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 <app-workspace-shell-import-overlay />.
- 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) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
This commit is contained in:
4grayandClaude Opus 4.7 committed 2026-05-01 19:34:45 +02:00
1 parent 63f5173139
commit d24c67c6e8
3 files changed
+197 -12

No files matched your search

@@ -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<void>();
readonly accountInfoRequested = output<void>();
readonly addPlaylistRequested = output<void>();
readonly refreshPlaylistRequested = output<void>();
}
describe('WorkspaceShellHeaderComponent', () => {
@@ -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);
});
});
@@ -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<WorkspaceHeaderBulkAction | null>(null);
@@ -78,6 +79,7 @@ class MockWorkspaceShellContextSidebarComponent {
readonly variant = input<WorkspaceShellContextPanel>('none');
readonly context = input<WorkspacePortalContext | null>(null);
readonly section = input<string | null>(null);
readonly hasPlaylists = input(false);
}
@Component({
@@ -90,6 +92,31 @@ class MockExternalPlaybackDockComponent {
readonly closeClicked = output<void>();
}
@Component({
selector: 'app-playlist-drop-overlay',
template: '',
standalone: true,
})
class MockPlaylistDropOverlayComponent {
readonly state = input<unknown>({ 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<WorkspaceHeaderBulkAction | null>(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();
});
});