diff --git a/app/src/__tests__/signal-inspector.test.ts b/app/src/__tests__/signal-inspector.test.ts index b9ed8b1..cde4401 100644 --- a/app/src/__tests__/signal-inspector.test.ts +++ b/app/src/__tests__/signal-inspector.test.ts @@ -221,6 +221,96 @@ describe('SignalInspector', () => { expect(trigger(fixture, 'signals-page-label')).toBeNull(); expect(notices(fixture)).toContain('Tab A closed, so this shows another page.'); }); + + it('forgets the pick of a page that closed', async () => { + const { fixture, push } = await renderPages({ graph: b, pages: { A: a, B: b } }); + await choose(fixture, 'signals-page-label', 'Tab A'); + await choose(fixture, 'signals-component-label', 'AppA'); + expect(fixture.componentInstance.pickerValue()).toBe('cA'); + + await push({ graph: b, pages: { B: b } }); + await push({ graph: b, pages: { A: a, B: b } }); + await choose(fixture, 'signals-page-label', 'Tab A'); + expect(fixture.componentInstance.pickerValue()).toBe('follow'); + }); + }); + + describe('kind chips', () => { + const listeners = new Set<(value: unknown) => void>(); + const owned = (id: string, kinds: string[]) => ({ + ...graph, + nodes: kinds.map((kind, i) => ({ id: String(i + 1), kind, label: `${kind}${i}`, epoch: 1 })), + component: { ...graph.component, id }, + }); + + async function renderLive(first: unknown) { + listeners.clear(); + const rpc = { + call: () => Promise.resolve([]), + callEvent: () => Promise.resolve(), + sharedState: (name: string) => + Promise.resolve({ + value: () => (name === 'signal-graph' ? { graph: first } : { pages: {} }), + on: (_event: string, listener: (value: unknown) => void) => { + if (name === 'signal-graph') listeners.add(listener); + return () => listeners.delete(listener); + }, + }), + }; + const fixture = TestBed.createComponent(SignalInspector); + fixture.componentRef.setInput('rpc', { + connectionMeta: {}, + scope: () => ({ rpc }), + } as unknown as DevframeRpcClient); + for (let i = 0; i < 3; i++) { + await new Promise((resolve) => setTimeout(resolve)); + await fixture.whenStable(); + } + const push = async (next: unknown) => { + for (const listener of listeners) listener({ graph: next }); + await fixture.whenStable(); + }; + return { fixture, push }; + } + + function pressed(fixture: ComponentFixture) { + return Array.from( + (fixture.nativeElement as HTMLElement).querySelectorAll('.kind-chip[aria-pressed="true"]'), + (el) => el.textContent?.replace(/\s+/g, ''), + ); + } + + it('resets a kind the newly picked component does not have', async () => { + const { fixture, push } = await renderLive(owned('c1', ['signal', 'linkedSignal'])); + fixture.componentInstance.kind.set('linkedSignal'); + await fixture.whenStable(); + + await push(owned('c2', ['signal', 'computed'])); + expect(fixture.componentInstance.kind()).toBeNull(); + expect(pressed(fixture)).toEqual(['All2']); + expect((fixture.nativeElement as HTMLElement).textContent).not.toContain('No signals match.'); + }); + + it('keeps a kind the newly picked component also has', async () => { + const { fixture, push } = await renderLive(owned('c1', ['signal', 'computed'])); + fixture.componentInstance.kind.set('computed'); + await fixture.whenStable(); + + await push(owned('c2', ['computed', 'effect'])); + expect(fixture.componentInstance.kind()).toBe('computed'); + expect(pressed(fixture)).toEqual(['computed1']); + }); + + it('shows an active kind with no matches as a pressed chip with count 0', async () => { + const { fixture, push } = await renderLive(owned('c1', ['signal', 'computed'])); + fixture.componentInstance.kind.set('computed'); + await fixture.whenStable(); + + await push(owned('c1', ['signal'])); + expect(fixture.componentInstance.kind()).toBe('computed'); + expect(pressed(fixture)).toEqual(['computed0']); + expect((fixture.nativeElement as HTMLElement).textContent).toContain('No signals match.'); + }); }); it('offers Clear filters when a filter hides the only resource of a resource-only graph', async () => { diff --git a/app/src/pages/signal-inspector.ts b/app/src/pages/signal-inspector.ts index 595af3a..13bb7c1 100644 --- a/app/src/pages/signal-inspector.ts +++ b/app/src/pages/signal-inspector.ts @@ -10,6 +10,7 @@ import { input, linkedSignal, signal, + untracked, viewChild, } from '@angular/core'; import { DatePipe, JsonPipe, NgTemplateOutlet } from '@angular/common'; @@ -84,6 +85,15 @@ interface LiveNode { } const FOLLOW = 'follow'; + +// Older servers send only the newest graph, without the per-page map. +function graphPagesOf(state: { + graph?: SignalGraph | null; + pages: Record; +}): Record { + if (Object.keys(state.pages).length) return state.pages; + return state.graph ? { [state.graph.pageId ?? '']: state.graph } : {}; +} const ENV = 'env:'; const MAX_HISTORY = 50; @@ -225,7 +235,7 @@ const KIND_COLORS: Record = { left out.

} - @if (kindCounts().length) { + @if (kindChips().length) {
- @for (group of kindCounts(); track group.kind) { + @for (group of kindChips(); track group.kind) {