Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions app/src/__tests__/store-inspector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import '@angular/compiler';
import { TestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import type { DevframeRpcClient } from 'devframe/client';
import { afterEach, describe, expect, it } from 'vitest';
import { StoreInspector } from '../pages/store-inspector';
import type { NgrxPage, NgrxState } from '../pages/store-types';

try {
TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
} catch {
// already initialized in this worker
}

afterEach(() => {
TestBed.resetTestingModule();
document.body.innerHTML = '';
});

function page(paused: boolean): NgrxPage {
return {
pageId: 'p1',
url: 'http://localhost/',
title: 'Shop',
stores: [],
classic: { state: { n: 2 }, devtools: true, scope: 'root', ...(paused ? { paused } : {}) },
log: [1, 2].map((seq) => ({
seq,
source: 'store',
storeId: 'store',
type: 'inc',
timestamp: seq,
diff: [],
restorable: true,
})),
reportedAt: 1,
} as NgrxPage;
}

function fakeClient(result: { ok: boolean; paused?: boolean; message: string }) {
const listeners = new Set<(value: unknown) => void>();
let value: NgrxState = { pages: [page(false)] };
const client = {
connectionMeta: {},
scope: () => ({
rpc: {
call: async (name: string) => {
if (name !== 'request-ngrx-action') return [];
if (result.paused) {
value = { pages: [page(true)] };
listeners.forEach((listener) => listener(value));
}
return result;
},
sharedState: async () => ({
value: () => value,
on: (_: string, listener: (value: unknown) => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
}),
},
}),
};
return client as unknown as DevframeRpcClient;
}

async function restoreNewest(result: { ok: boolean; paused?: boolean; message: string }) {
const fixture = TestBed.createComponent(StoreInspector);
document.body.appendChild(fixture.nativeElement);
fixture.componentRef.setInput('rpc', fakeClient(result));
await fixture.whenStable();
fixture.detectChanges();
await fixture.componentInstance.restore(2, true);
await fixture.whenStable();
fixture.detectChanges();
return fixture.nativeElement as HTMLElement;
}

describe('StoreInspector restore focus', () => {
it('focuses the state when restoring the newest action does not pause the store', async () => {
const host = await restoreNewest({ ok: true, paused: false, message: 'Jumped.' });
expect(host.querySelector('.paused')).toBeNull();
expect(document.activeElement).toBe(host.querySelector('pre.tree'));
});

it('focuses "Back to latest" when the restore pauses the store', async () => {
const host = await restoreNewest({ ok: true, paused: true, message: 'Jumped. Paused.' });
const latest = host.querySelector('.paused button');
expect(latest).not.toBeNull();
expect(document.activeElement).toBe(latest);
});

it('focuses "Back to latest" after a deferred restore on an already paused page', async () => {
let resolve!: (value: unknown) => void;
const client = {
connectionMeta: {},
scope: () => ({
rpc: {
call: async (name: string) =>
name === 'request-ngrx-action' ? new Promise((done) => (resolve = done)) : [],
sharedState: async () => ({
value: () => ({ pages: [page(true)] }),
on: () => () => undefined,
}),
},
}),
} as unknown as DevframeRpcClient;
const fixture = TestBed.createComponent(StoreInspector);
document.body.appendChild(fixture.nativeElement);
fixture.componentRef.setInput('rpc', client);
await fixture.whenStable();
fixture.detectChanges();
const host = fixture.nativeElement as HTMLElement;
const latest = host.querySelector('.paused button');
expect(latest).not.toBeNull();

const restoring = fixture.componentInstance.restore(1, true);
fixture.detectChanges();
await Promise.resolve();
fixture.detectChanges();
resolve({ ok: true, paused: true, message: 'Jumped. Paused.' });
await restoring;
await fixture.whenStable();
fixture.detectChanges();
expect(document.activeElement).toBe(latest);
});
});
10 changes: 9 additions & 1 deletion app/src/pages/component-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@angular/core';
import type { DevframeRpcClient } from 'devframe/client';
import { hostPageId } from '../page-id';
import { isStaticReport } from '../rpc';

interface SourceComponent {
selector: string;
Expand Down Expand Up @@ -428,7 +429,13 @@ function bare(name: string): string {
} @else if (error() && !source().length) {
<div class="state" role="alert">
<p class="state-title">Could not load components</p>
<p class="state-hint">Check that the dev server is running, then try again.</p>
<p class="state-hint">
@if (staticReport()) {
Run <code>ng-devtools build</code> again to rebuild the report.
} @else {
Check that the dev server is running, then try again.
}
</p>
<button type="button" (click)="refresh()">Retry</button>
</div>
} @else if (!source().length) {
Expand Down Expand Up @@ -1092,6 +1099,7 @@ function bare(name: string): string {
})
export class ComponentTree {
readonly rpc = input<DevframeRpcClient | null>(null);
readonly staticReport = computed(() => isStaticReport(this.rpc()));
readonly focus = input<{ id: string } | null>(null);
readonly showForm = output<string>();
readonly focusHandled = output<void>();
Expand Down
10 changes: 9 additions & 1 deletion app/src/pages/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type ResolvedNgDevtoolsConfig,
} from '@santoshyadavdev/ng-devtools/config';
import { hostPageId } from '../page-id';
import { isStaticReport } from '../rpc';
import { panelConfig, tabEnabled } from '../devtools-config';
import { TabIcon } from './tab-icon';

Expand Down Expand Up @@ -151,7 +152,13 @@ export function storeCard(rows: Row[]): Card {
}
</h2>
@if (metaState() === 'error') {
<p class="hint">Check that the dev server is running, then reload the panel.</p>
<p class="hint">
@if (staticReport()) {
Run <code>ng-devtools build</code> again to rebuild the report.
} @else {
Check that the dev server is running, then reload the panel.
}
</p>
}
</div>
<ul class="chips" [class.pending]="metaState() === 'loading'">
Expand Down Expand Up @@ -482,6 +489,7 @@ export function storeCard(rows: Row[]): Card {
export class Dashboard {
rpc = input<DevframeRpcClient | null>(null);
navigate = output<StatTab>();
staticReport = computed(() => isStaticReport(this.rpc()));

meta = signal<BuildMeta | null>(null);
private readonly config = computed(() => panelConfig(this.rpc()));
Expand Down
10 changes: 9 additions & 1 deletion app/src/pages/route-inspector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, computed, effect, input, linkedSignal, signal, viewChild } from '@angular/core';
import type { DevframeRpcClient } from 'devframe/client';
import { isStaticReport } from '../rpc';
import { LiveRoute } from './live-route';
import { SHARED_STYLES, sourceLocation, type SourceRoute } from './router-types';

Expand Down Expand Up @@ -49,7 +50,13 @@ import { SHARED_STYLES, sourceLocation, type SourceRoute } from './router-types'
@if (error()) {
<div class="empty" role="alert">
<p class="empty-title">Could not scan the route files.</p>
<p class="muted">Check that the dev server is running, then refresh.</p>
<p class="muted">
@if (staticReport()) {
Run <code>ng-devtools build</code> again to rebuild the report.
} @else {
Check that the dev server is running, then refresh.
}
</p>
</div>
} @else if (loading() && routes().length === 0) {
<p class="muted empty" role="status">Scanning routes…</p>
Expand Down Expand Up @@ -211,6 +218,7 @@ import { SHARED_STYLES, sourceLocation, type SourceRoute } from './router-types'
})
export class RouteInspector {
rpc = input<DevframeRpcClient | null>(null);
staticReport = computed(() => isStaticReport(this.rpc()));

routes = signal<SourceRoute[]>([]);
filter = signal('');
Expand Down
4 changes: 4 additions & 0 deletions app/src/pages/route-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ interface MatchResult {
<p class="muted summary">
Generation {{ page().generation }} · {{ rows().length }} route(s). Lazy routes show their
children once loaded.
@if (page().configTruncated; as left) {
{{ left }} route(s) left out: the page lists at most 200 routes per level and 1000 in
total.
}
</p>
}
</div>
Expand Down
1 change: 1 addition & 0 deletions app/src/pages/router-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export interface RouterPage {
navigations: NavigationRecord[];
generation?: number;
config?: RouteNode[];
configTruncated?: number;
activeIds?: string[];
setup?: RouterSetup;
outlets?: OutletInfo[];
Expand Down
Loading
Loading