Skip to content

Commit fbae46c

Browse files
authored
fix: resolve the P1 issues (#184)
Fixes the P1 issues from the September sweep, with tests and docs. See the PR for the per-issue list.
1 parent 7c5da93 commit fbae46c

98 files changed

Lines changed: 3156 additions & 346 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import '@angular/compiler';
2+
import { TestBed } from '@angular/core/testing';
3+
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
4+
import type { DevframeRpcClient } from 'devframe/client';
5+
import { afterEach, describe, expect, it } from 'vitest';
6+
import { StoreInspector } from '../pages/store-inspector';
7+
import type { NgrxPage, NgrxState } from '../pages/store-types';
8+
9+
try {
10+
TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
11+
} catch {
12+
// already initialized in this worker
13+
}
14+
15+
afterEach(() => {
16+
TestBed.resetTestingModule();
17+
document.body.innerHTML = '';
18+
});
19+
20+
function page(paused: boolean): NgrxPage {
21+
return {
22+
pageId: 'p1',
23+
url: 'http://localhost/',
24+
title: 'Shop',
25+
stores: [],
26+
classic: { state: { n: 2 }, devtools: true, scope: 'root', ...(paused ? { paused } : {}) },
27+
log: [1, 2].map((seq) => ({
28+
seq,
29+
source: 'store',
30+
storeId: 'store',
31+
type: 'inc',
32+
timestamp: seq,
33+
diff: [],
34+
restorable: true,
35+
})),
36+
reportedAt: 1,
37+
} as NgrxPage;
38+
}
39+
40+
function fakeClient(result: { ok: boolean; paused?: boolean; message: string }) {
41+
const listeners = new Set<(value: unknown) => void>();
42+
let value: NgrxState = { pages: [page(false)] };
43+
const client = {
44+
connectionMeta: {},
45+
scope: () => ({
46+
rpc: {
47+
call: async (name: string) => {
48+
if (name !== 'request-ngrx-action') return [];
49+
if (result.paused) {
50+
value = { pages: [page(true)] };
51+
listeners.forEach((listener) => listener(value));
52+
}
53+
return result;
54+
},
55+
sharedState: async () => ({
56+
value: () => value,
57+
on: (_: string, listener: (value: unknown) => void) => {
58+
listeners.add(listener);
59+
return () => listeners.delete(listener);
60+
},
61+
}),
62+
},
63+
}),
64+
};
65+
return client as unknown as DevframeRpcClient;
66+
}
67+
68+
async function restoreNewest(result: { ok: boolean; paused?: boolean; message: string }) {
69+
const fixture = TestBed.createComponent(StoreInspector);
70+
document.body.appendChild(fixture.nativeElement);
71+
fixture.componentRef.setInput('rpc', fakeClient(result));
72+
await fixture.whenStable();
73+
fixture.detectChanges();
74+
await fixture.componentInstance.restore(2, true);
75+
await fixture.whenStable();
76+
fixture.detectChanges();
77+
return fixture.nativeElement as HTMLElement;
78+
}
79+
80+
describe('StoreInspector restore focus', () => {
81+
it('focuses the state when restoring the newest action does not pause the store', async () => {
82+
const host = await restoreNewest({ ok: true, paused: false, message: 'Jumped.' });
83+
expect(host.querySelector('.paused')).toBeNull();
84+
expect(document.activeElement).toBe(host.querySelector('pre.tree'));
85+
});
86+
87+
it('focuses "Back to latest" when the restore pauses the store', async () => {
88+
const host = await restoreNewest({ ok: true, paused: true, message: 'Jumped. Paused.' });
89+
const latest = host.querySelector('.paused button');
90+
expect(latest).not.toBeNull();
91+
expect(document.activeElement).toBe(latest);
92+
});
93+
94+
it('focuses "Back to latest" after a deferred restore on an already paused page', async () => {
95+
let resolve!: (value: unknown) => void;
96+
const client = {
97+
connectionMeta: {},
98+
scope: () => ({
99+
rpc: {
100+
call: async (name: string) =>
101+
name === 'request-ngrx-action' ? new Promise((done) => (resolve = done)) : [],
102+
sharedState: async () => ({
103+
value: () => ({ pages: [page(true)] }),
104+
on: () => () => undefined,
105+
}),
106+
},
107+
}),
108+
} as unknown as DevframeRpcClient;
109+
const fixture = TestBed.createComponent(StoreInspector);
110+
document.body.appendChild(fixture.nativeElement);
111+
fixture.componentRef.setInput('rpc', client);
112+
await fixture.whenStable();
113+
fixture.detectChanges();
114+
const host = fixture.nativeElement as HTMLElement;
115+
const latest = host.querySelector('.paused button');
116+
expect(latest).not.toBeNull();
117+
118+
const restoring = fixture.componentInstance.restore(1, true);
119+
fixture.detectChanges();
120+
await Promise.resolve();
121+
fixture.detectChanges();
122+
resolve({ ok: true, paused: true, message: 'Jumped. Paused.' });
123+
await restoring;
124+
await fixture.whenStable();
125+
fixture.detectChanges();
126+
expect(document.activeElement).toBe(latest);
127+
});
128+
});

‎app/src/pages/component-tree.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@angular/core';
1515
import type { DevframeRpcClient } from 'devframe/client';
1616
import { hostPageId } from '../page-id';
17+
import { isStaticReport } from '../rpc';
1718

1819
interface SourceComponent {
1920
selector: string;
@@ -428,7 +429,13 @@ function bare(name: string): string {
428429
} @else if (error() && !source().length) {
429430
<div class="state" role="alert">
430431
<p class="state-title">Could not load components</p>
431-
<p class="state-hint">Check that the dev server is running, then try again.</p>
432+
<p class="state-hint">
433+
@if (staticReport()) {
434+
Run <code>ng-devtools build</code> again to rebuild the report.
435+
} @else {
436+
Check that the dev server is running, then try again.
437+
}
438+
</p>
432439
<button type="button" (click)="refresh()">Retry</button>
433440
</div>
434441
} @else if (!source().length) {
@@ -1092,6 +1099,7 @@ function bare(name: string): string {
10921099
})
10931100
export class ComponentTree {
10941101
readonly rpc = input<DevframeRpcClient | null>(null);
1102+
readonly staticReport = computed(() => isStaticReport(this.rpc()));
10951103
readonly focus = input<{ id: string } | null>(null);
10961104
readonly showForm = output<string>();
10971105
readonly focusHandled = output<void>();

‎app/src/pages/dashboard.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type ResolvedNgDevtoolsConfig,
1515
} from '@santoshyadavdev/ng-devtools/config';
1616
import { hostPageId } from '../page-id';
17+
import { isStaticReport } from '../rpc';
1718
import { panelConfig, tabEnabled } from '../devtools-config';
1819
import { TabIcon } from './tab-icon';
1920

@@ -151,7 +152,13 @@ export function storeCard(rows: Row[]): Card {
151152
}
152153
</h2>
153154
@if (metaState() === 'error') {
154-
<p class="hint">Check that the dev server is running, then reload the panel.</p>
155+
<p class="hint">
156+
@if (staticReport()) {
157+
Run <code>ng-devtools build</code> again to rebuild the report.
158+
} @else {
159+
Check that the dev server is running, then reload the panel.
160+
}
161+
</p>
155162
}
156163
</div>
157164
<ul class="chips" [class.pending]="metaState() === 'loading'">
@@ -482,6 +489,7 @@ export function storeCard(rows: Row[]): Card {
482489
export class Dashboard {
483490
rpc = input<DevframeRpcClient | null>(null);
484491
navigate = output<StatTab>();
492+
staticReport = computed(() => isStaticReport(this.rpc()));
485493

486494
meta = signal<BuildMeta | null>(null);
487495
private readonly config = computed(() => panelConfig(this.rpc()));

‎app/src/pages/route-inspector.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, computed, effect, input, linkedSignal, signal, viewChild } from '@angular/core';
22
import type { DevframeRpcClient } from 'devframe/client';
3+
import { isStaticReport } from '../rpc';
34
import { LiveRoute } from './live-route';
45
import { SHARED_STYLES, sourceLocation, type SourceRoute } from './router-types';
56

@@ -49,7 +50,13 @@ import { SHARED_STYLES, sourceLocation, type SourceRoute } from './router-types'
4950
@if (error()) {
5051
<div class="empty" role="alert">
5152
<p class="empty-title">Could not scan the route files.</p>
52-
<p class="muted">Check that the dev server is running, then refresh.</p>
53+
<p class="muted">
54+
@if (staticReport()) {
55+
Run <code>ng-devtools build</code> again to rebuild the report.
56+
} @else {
57+
Check that the dev server is running, then refresh.
58+
}
59+
</p>
5360
</div>
5461
} @else if (loading() && routes().length === 0) {
5562
<p class="muted empty" role="status">Scanning routes…</p>
@@ -211,6 +218,7 @@ import { SHARED_STYLES, sourceLocation, type SourceRoute } from './router-types'
211218
})
212219
export class RouteInspector {
213220
rpc = input<DevframeRpcClient | null>(null);
221+
staticReport = computed(() => isStaticReport(this.rpc()));
214222

215223
routes = signal<SourceRoute[]>([]);
216224
filter = signal('');

‎app/src/pages/route-tree.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ interface MatchResult {
8585
<p class="muted summary">
8686
Generation {{ page().generation }} · {{ rows().length }} route(s). Lazy routes show their
8787
children once loaded.
88+
@if (page().configTruncated; as left) {
89+
{{ left }} route(s) left out: the page lists at most 200 routes per level and 1000 in
90+
total.
91+
}
8892
</p>
8993
}
9094
</div>

‎app/src/pages/router-types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export interface RouterPage {
175175
navigations: NavigationRecord[];
176176
generation?: number;
177177
config?: RouteNode[];
178+
configTruncated?: number;
178179
activeIds?: string[];
179180
setup?: RouterSetup;
180181
outlets?: OutletInfo[];

0 commit comments

Comments
 (0)