Fix findings view toggle reactivity

This commit is contained in:
master
2026-03-07 17:21:26 +02:00
parent 4b91527297
commit f1ab38aa27
3 changed files with 201 additions and 18 deletions

View File

@@ -0,0 +1,104 @@
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
import { BehaviorSubject, of } from 'rxjs';
import { FindingsContainerComponent } from '../../features/findings/container/findings-container.component';
import { CompareService } from '../../features/compare/services/compare.service';
import { ViewPreferenceService, FindingsViewMode } from '../services/view-preference.service';
import { MockScoringApi, SCORING_API } from '../services/scoring.service';
describe('FindingsContainerComponent', () => {
let fixture: ComponentFixture<FindingsContainerComponent>;
let queryParamMap$: BehaviorSubject<ReturnType<typeof convertToParamMap>>;
let preferenceSignal: ReturnType<typeof signal<FindingsViewMode>>;
beforeEach(async () => {
queryParamMap$ = new BehaviorSubject(convertToParamMap({}));
preferenceSignal = signal<FindingsViewMode>('diff');
await TestBed.configureTestingModule({
imports: [FindingsContainerComponent, NoopAnimationsModule],
providers: [
provideRouter([]),
{
provide: ViewPreferenceService,
useValue: {
viewMode: preferenceSignal.asReadonly(),
getViewMode: () => preferenceSignal(),
setViewMode: (mode: FindingsViewMode) => preferenceSignal.set(mode),
},
},
{
provide: CompareService,
useValue: {
getBaselineRecommendations: () =>
of({
selectedDigest: 'baseline-123',
selectionReason: 'Last Green Build',
alternatives: [],
autoSelectEnabled: true,
}),
computeDelta: () =>
of({
categories: [
{ id: 'added', name: 'Added', icon: 'add', added: 1, removed: 0, changed: 0 },
],
items: [],
}),
},
},
{
provide: SCORING_API,
useClass: MockScoringApi,
},
{
provide: ActivatedRoute,
useValue: {
paramMap: of(convertToParamMap({ scanId: 'test-scan-123' })),
queryParamMap: queryParamMap$,
snapshot: {
paramMap: convertToParamMap({ scanId: 'test-scan-123' }),
queryParamMap: convertToParamMap({}),
},
},
},
],
}).compileComponents();
fixture = TestBed.createComponent(FindingsContainerComponent);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
});
it('switches from diff to detail when the persisted preference changes without a URL override', async () => {
expect(fixture.nativeElement.textContent).toContain('Comparing:');
expect(fixture.nativeElement.textContent).not.toContain('backend-api');
preferenceSignal.set('detail');
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('backend-api');
expect(fixture.nativeElement.textContent).not.toContain('Comparing:');
});
it('keeps the explicit URL override authoritative over the persisted preference', async () => {
queryParamMap$.next(convertToParamMap({ view: 'diff' }));
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
preferenceSignal.set('detail');
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.componentInstance.viewMode()).toBe('diff');
expect(fixture.nativeElement.textContent).toContain('Comparing:');
expect(fixture.nativeElement.textContent).not.toContain('backend-api');
});
});

View File

@@ -197,8 +197,8 @@ export class FindingsContainerComponent implements OnInit {
private readonly viewPref = inject(ViewPreferenceService);
private readonly compareService = inject(CompareService);
// View mode: URL param > user preference > default (diff)
readonly viewMode = signal<FindingsViewMode>('diff');
// View mode: explicit URL override > reactive user preference
readonly viewMode = computed<FindingsViewMode>(() => this.urlViewMode() ?? this.viewPref.viewMode());
// Loading state
readonly loading = signal(false);
@@ -209,6 +209,11 @@ export class FindingsContainerComponent implements OnInit {
// Delta summary for diff view
readonly deltaSummary = signal<{ added: number; removed: number; changed: number } | null>(null);
private readonly urlViewMode = toSignal(
this.route.queryParamMap.pipe(map((params) => this.readViewMode(params.get('view')))),
{ initialValue: this.readViewMode(this.route.snapshot.queryParamMap.get('view')) }
);
// Current scan ID resolved from route param, query param, or deterministic fallback.
private readonly scanId = toSignal(
this.route.paramMap.pipe(
@@ -227,25 +232,9 @@ export class FindingsContainerComponent implements OnInit {
);
ngOnInit(): void {
this.initializeViewMode();
this.loadData();
}
private initializeViewMode(): void {
// Check URL override first
const urlView = this.route.snapshot.queryParamMap.get('view');
if (urlView === 'diff' || urlView === 'detail') {
this.viewMode.set(urlView);
return;
}
// Fall back to user preference
this.viewMode.set(this.viewPref.getViewMode());
// Subscribe to preference changes
// Note: Using effect would be cleaner but keeping it simple here
}
private loadData(): void {
const scanId = this.scanId();
if (!scanId) return;
@@ -278,4 +267,8 @@ export class FindingsContainerComponent implements OnInit {
// Navigate to finding detail or open drawer
console.log('Selected finding:', finding.id);
}
private readViewMode(raw: string | null): FindingsViewMode | null {
return raw === 'diff' || raw === 'detail' ? raw : null;
}
}