fix: the after-1.0 findings for the hub, agent tools, panel and weekl… · pangular-inspector/devtools@b10969d · GitHub
Skip to content

Commit b10969d

Browse files
authored
fix: the after-1.0 findings for the hub, agent tools, panel and weekly e2e (#437)
1 parent f4de03c commit b10969d

74 files changed

Lines changed: 2482 additions & 378 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.

‎.github/workflows/e2e.yml‎

Lines changed: 1 addition & 5 deletions
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
2+
import type { DevframeRpcClient } from 'devframe/client';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
4+
import { ComponentTree } from '../pages/component-tree';
5+
6+
const LONG = 'CustomerOrderHistoryTableWithInlineFiltersComponent';
7+
8+
const roots = [
9+
{
10+
id: 'app',
11+
name: 'App',
12+
tag: 'app-root',
13+
children: [{ id: 'long', name: LONG, tag: 'app-customer-order-history-table', children: [] }],
14+
},
15+
];
16+
17+
function client(): DevframeRpcClient {
18+
const state: Record<string, unknown> = {
19+
'component-tree': {
20+
pages: {
21+
a: { pageId: 'a', title: 'a', url: '/', roots, count: 2, detail: null, reportedAt: 1 },
22+
},
23+
},
24+
};
25+
const rpc = {
26+
call: () => Promise.resolve([]),
27+
callEvent: () => Promise.resolve(),
28+
sharedState: (name: string) =>
29+
Promise.resolve({ value: () => state[name] ?? null, on: () => () => {} }),
30+
};
31+
return { connectionMeta: {}, scope: () => ({ rpc }) } as unknown as DevframeRpcClient;
32+
}
33+
34+
async function settle(fixture: ComponentFixture<unknown>) {
35+
for (let i = 0; i < 3; i++) {
36+
await new Promise((resolve) => setTimeout(resolve));
37+
await fixture.whenStable();
38+
}
39+
}
40+
41+
async function setup() {
42+
const fixture = TestBed.createComponent(ComponentTree);
43+
fixture.componentRef.setInput('rpc', client());
44+
document.body.appendChild(fixture.nativeElement);
45+
await settle(fixture);
46+
return { fixture, root: fixture.nativeElement as HTMLElement };
47+
}
48+
49+
/** jsdom has no layout, so names that are cut off report a wider content than their box. */
50+
function cutOff(names: string[]) {
51+
vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockImplementation(function (
52+
this: HTMLElement,
53+
) {
54+
return names.includes(this.textContent?.trim() ?? '') ? 400 : 0;
55+
});
56+
vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(120);
57+
}
58+
59+
afterEach(() => {
60+
vi.restoreAllMocks();
61+
document.body.innerHTML = '';
62+
TestBed.resetTestingModule();
63+
});
64+
65+
describe('component tree at narrow widths', () => {
66+
it('keeps a name and a hover title on every toolbar action', async () => {
67+
const { fixture, root } = await setup();
68+
const actions = Array.from(
69+
root.querySelectorAll<HTMLButtonElement>('.toolbar .tree-action, .toolbar .pick'),
70+
);
71+
expect(actions.map((b) => b.querySelector('.label')?.textContent?.trim())).toEqual([
72+
'Expand all',
73+
'Collapse all',
74+
'Pick component on page',
75+
]);
76+
for (const action of actions) {
77+
expect(action.title).toBe(action.textContent?.trim());
78+
}
79+
fixture.destroy();
80+
});
81+
82+
it('gives each name and tag its full text as a hover title', async () => {
83+
const { fixture, root } = await setup();
84+
const row = root.querySelector<HTMLElement>('.row[data-id="long"]')!;
85+
expect(row.querySelector<HTMLElement>('.name')!.title).toBe(LONG);
86+
expect(row.querySelector<HTMLElement>('.tag')!.title).toBe(
87+
'<app-customer-order-history-table>',
88+
);
89+
fixture.destroy();
90+
});
91+
92+
it('shows the full text of a cut off row while it has focus', async () => {
93+
cutOff([LONG]);
94+
const { fixture, root } = await setup();
95+
const row = root.querySelector<HTMLElement>('.row[data-id="long"]')!;
96+
row.focus();
97+
await settle(fixture);
98+
const tip = root.querySelector<HTMLElement>('.name-tip');
99+
expect(tip?.textContent?.trim()).toBe(`${LONG} <app-customer-order-history-table>`);
100+
expect(tip?.getAttribute('aria-hidden')).toBe('true');
101+
102+
row.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
103+
await settle(fixture);
104+
expect(root.querySelector('.name-tip')).toBeNull();
105+
106+
row.blur();
107+
row.focus();
108+
await settle(fixture);
109+
expect(root.querySelector('.name-tip')).not.toBeNull();
110+
row.blur();
111+
await settle(fixture);
112+
expect(root.querySelector('.name-tip')).toBeNull();
113+
fixture.destroy();
114+
});
115+
116+
it('shows no tip for a row that fits', async () => {
117+
cutOff([LONG]);
118+
const { fixture, root } = await setup();
119+
root.querySelector<HTMLElement>('.row[data-id="app"]')!.focus();
120+
await settle(fixture);
121+
expect(root.querySelector('.name-tip')).toBeNull();
122+
fixture.destroy();
123+
});
124+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
2+
import type { DevframeRpcClient } from 'devframe/client';
3+
import { afterEach, describe, expect, it } from 'vitest';
4+
import { Dashboard } from '../pages/dashboard';
5+
6+
/** A trusted client whose `__connection.json` config has no redaction, like a current server's. */
7+
function fakeClient(backend: string, calls: string[]): DevframeRpcClient {
8+
const rpc = {
9+
call: (name: string) => {
10+
calls.push(name);
11+
if (name === 'redaction-config') {
12+
return Promise.resolve({ secretNames: ['passport'], unmask: ['pin'] });
13+
}
14+
return new Promise(() => {});
15+
},
16+
callEvent: () => Promise.resolve(),
17+
sharedState: () => Promise.resolve({ value: () => null, on: () => () => {} }),
18+
};
19+
return {
20+
status: 'connected',
21+
connectionMeta: { backend, configs: { pangular: {} } },
22+
scope: () => ({ rpc }),
23+
events: { on: () => () => {} },
24+
} as unknown as DevframeRpcClient;
25+
}
26+
27+
async function settle(fixture: ComponentFixture<unknown>) {
28+
for (let i = 0; i < 3; i++) {
29+
await new Promise((resolve) => setTimeout(resolve));
30+
await fixture.whenStable();
31+
}
32+
}
33+
34+
function configText(fixture: ComponentFixture<unknown>): string {
35+
return (fixture.nativeElement as HTMLElement).querySelector('section.config')?.textContent ?? '';
36+
}
37+
38+
afterEach(() => {
39+
document.body.innerHTML = '';
40+
});
41+
42+
describe('Dashboard redaction rows', () => {
43+
it('lists the secret names a trusted panel reads from the server', async () => {
44+
const calls: string[] = [];
45+
const fixture = TestBed.createComponent(Dashboard);
46+
fixture.componentRef.setInput('rpc', fakeClient('websocket', calls));
47+
await settle(fixture);
48+
expect(calls).toContain('redaction-config');
49+
const text = configText(fixture);
50+
expect(text).toContain('Extra secret names');
51+
expect(text).toContain('passport');
52+
expect(text).toContain('Unmasked names');
53+
expect(text).toContain('pin');
54+
});
55+
56+
it('does not ask a static report, which carries no secret names', async () => {
57+
const calls: string[] = [];
58+
const fixture = TestBed.createComponent(Dashboard);
59+
fixture.componentRef.setInput('rpc', fakeClient('static', calls));
60+
await settle(fixture);
61+
expect(calls).not.toContain('redaction-config');
62+
expect(configText(fixture)).not.toContain('Extra secret names');
63+
});
64+
});

‎app/src/__tests__/shell-deep-link.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ describe('deep links', () => {
9191
booted.fixture.destroy();
9292
});
9393

94+
it('switches tabs when only the hash changes after load', async () => {
95+
const { fixture, app } = await boot('#tab=signals');
96+
expect(app.tab()).toBe('signals');
97+
location.hash = '#tab=forms';
98+
await settle(fixture);
99+
expect(app.tab()).toBe('forms');
100+
location.hash = '#tab=components&id=9&page=p3';
101+
await settle(fixture);
102+
expect(app.tab()).toBe('components');
103+
expect(app.componentFocus()).toEqual({ id: '9', pageId: 'p3' });
104+
location.hash = '#tab=settings';
105+
await settle(fixture);
106+
expect(app.tab()).toBe('settings');
107+
location.hash = '#tab=nope';
108+
await settle(fixture);
109+
expect(app.tab()).toBe('settings');
110+
fixture.destroy();
111+
});
112+
94113
it('follows a link the server sends to open panels', async () => {
95114
const { fixture, app } = await boot();
96115
server.handlers.get('open-in-panel')!({ tab: 'components', id: '7', page: 'p2' });

‎app/src/app.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ function readView(): View | null {
201201
'[style.--accent]': 'viewAccent()?.accent ?? null',
202202
'[style.--accent-text]': 'viewAccent()?.text ?? null',
203203
'(window:message)': 'inspectFromPanel($event)',
204+
'(window:hashchange)': 'followHash()',
204205
'(document:keydown)': 'onGlobalKey($event)',
205206
},
206207
imports: [
@@ -1631,6 +1632,20 @@ export class App implements OnInit, OnDestroy {
16311632
return true;
16321633
}
16331634

1635+
/**
1636+
* Follows a `#tab=` link that changes after load (a link inside the panel, or
1637+
* a script setting `location.hash`), the same way as one on load.
1638+
*/
1639+
followHash() {
1640+
const link = parsePanelLink(location.hash);
1641+
if (!link) return;
1642+
if (link.tab === 'settings') {
1643+
if (this.tab() !== 'settings') this.toggleSettings();
1644+
return;
1645+
}
1646+
this.openLink(link);
1647+
}
1648+
16341649
/** Listens for the commands and links the server sends to every open panel. */
16351650
private listenForServer(client: DevframeRpcClient) {
16361651
const rpc = client.scope('pangular').rpc as unknown as {

‎app/src/devtools-config.ts‎

Lines changed: 22 additions & 0 deletions

0 commit comments

Comments
 (0)