Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8d2ff3f
fix: resolve the P1 issues
erkamyaman Sep 30, 2026
1d65c5b
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
7c510e7
fix(store): keep focus when restoring or leaving a past state
erkamyaman Sep 30, 2026
084fb20
fix(popup): fit the floating panel to narrow windows
erkamyaman Sep 30, 2026
de3f8da
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
99d5a72
fix: resolve the P2 issues
erkamyaman Sep 30, 2026
0db30a9
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
71fc1ab
fix: resolve the P3 issues
erkamyaman Sep 30, 2026
a8ddb63
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
62a1174
fix: address review comments on the P1 fixes
erkamyaman Sep 30, 2026
521460a
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
5f6f47b
Merge fix/p1-issues into fix/p2-issues
erkamyaman Sep 30, 2026
fc069c2
Merge fix/p2-issues into fix/p3-issues
erkamyaman Sep 30, 2026
c12901b
fix(analog): use the Vite root for routes and build meta in Nx worksp…
erkamyaman Sep 30, 2026
134aba0
fix: address the QA findings on the P2 fixes
erkamyaman Sep 30, 2026
2550ade
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
9c7d13b
Merge fix/p1-issues into fix/p2-issues
erkamyaman Sep 30, 2026
f6f853d
fix: address the QA findings on the P3 fixes
erkamyaman Sep 30, 2026
a9cb1c6
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
ff7adb3
Merge fix/p2-issues into fix/p3-issues
erkamyaman Sep 30, 2026
e603e19
fix: address the second round of review comments on the P1 fixes
erkamyaman Sep 30, 2026
ba794cd
chore(extension): rebuild the extension UI
erkamyaman Sep 30, 2026
72dff88
Merge fix/p1-issues into fix/p2-issues
erkamyaman Sep 30, 2026
4336c8a
Merge fix/p2-issues into fix/p3-issues
erkamyaman Sep 30, 2026
00b8e76
Merge main into fix/p3-issues
erkamyaman Sep 30, 2026
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
99 changes: 99 additions & 0 deletions app/src/__tests__/config-pending.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { TestBed, type ComponentFixture } from '@angular/core/testing';
import type { DevframeRpcClient } from 'devframe/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Dashboard } from '../pages/dashboard';

const connection = vi.hoisted(() => ({
resolve: (_client: unknown) => {},
}));

vi.mock('devframe/client', () => ({
connectDevframe: () =>
new Promise((resolve) => {
connection.resolve = resolve;
}),
}));

const { App } = await import('../app');

function fakeClient(configs: object = {}): DevframeRpcClient {
const rpc = {
call: () => new Promise(() => {}),
callEvent: () => Promise.resolve(),
sharedState: () => Promise.resolve({ value: () => null, on: () => () => {} }),
};
return {
connectionMeta: { configs },
scope: () => ({ rpc }),
events: { on: () => () => {} },
} as unknown as DevframeRpcClient;
}

const signalsOff = { 'ng-devtools': { inspectors: { signals: false } } };

async function settle(fixture: ComponentFixture<unknown>) {
for (let i = 0; i < 3; i++) {
await new Promise((resolve) => setTimeout(resolve));
await fixture.whenStable();
}
}

function host(fixture: ComponentFixture<unknown>): HTMLElement {
return fixture.nativeElement as HTMLElement;
}

afterEach(() => {
document.body.innerHTML = '';
location.hash = '';
});

describe('Dashboard before the connection is up', () => {
it('shows a loading configuration block and no stat cards', async () => {
const fixture = TestBed.createComponent(Dashboard);
await settle(fixture);
const config = host(fixture).querySelector('section.config')!;
expect(config.getAttribute('aria-busy')).toBe('true');
expect(config.textContent).toContain('Loading');
expect(config.textContent).not.toContain('Defaults');
expect(host(fixture).querySelectorAll('button.stat')).toHaveLength(0);
});

it('shows the stat cards and configuration once connected', async () => {
const fixture = TestBed.createComponent(Dashboard);
fixture.componentRef.setInput('rpc', fakeClient(signalsOff));
await settle(fixture);
const config = host(fixture).querySelector('section.config')!;
expect(config.getAttribute('aria-busy')).not.toBe('true');
expect(config.textContent).toContain('Inspectors off');
const labels = [...host(fixture).querySelectorAll('button.stat .label')].map((el) =>
el.textContent?.trim(),
);
expect(labels).toContain('Components');
expect(labels).not.toContain('Signals');
});
});

describe('App tabs before the connection is up', () => {
function tabNames(fixture: ComponentFixture<unknown>) {
return [...host(fixture).querySelectorAll('nav button')].map((b) => b.textContent?.trim());
}

it('lists only the Dashboard until the config arrives, then the enabled inspectors', async () => {
const fixture = TestBed.createComponent(App);
await settle(fixture);
expect(tabNames(fixture)).toEqual(['Dashboard']);

connection.resolve(fakeClient(signalsOff));
await settle(fixture);
const names = tabNames(fixture);
expect(names).toContain('Components');
expect(names).not.toContain('Signals');
});

it('keeps a tab restored from the address visible while connecting', async () => {
location.hash = '#tab=routes';
const fixture = TestBed.createComponent(App);
await settle(fixture);
expect(tabNames(fixture)).toEqual(['Dashboard', 'Routes']);
});
});
10 changes: 9 additions & 1 deletion app/src/__tests__/dashboard-cards.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { describe, expect, it } from 'vitest';
import { componentsCard, routesCard, storeCard } from '../pages/dashboard';
import { componentsCard, routesCard, storeCard, zoneLabel } from '../pages/dashboard';

describe('dashboard cards', () => {
it('names the change detection mode the page reported', () => {
expect(zoneLabel('zoneless')).toBe('Zoneless');
expect(zoneLabel('zone')).toBe('zone.js');
expect(zoneLabel('zone-unused')).toBe('Zoneless, zone.js loaded');
expect(zoneLabel(null)).toBeNull();
expect(zoneLabel('toString')).toBeNull();
});

it('counts source components when the rows carry no kind', () => {
expect(componentsCard([{}, {}, {}])).toEqual({ value: 3, sub: 'discovered in source' });
});
Expand Down
93 changes: 93 additions & 0 deletions app/src/__tests__/forms-panels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ describe('FormsFieldDetail', () => {
button(fixture, 'Close details for email').click();
expect(closed).toBe(1);
});

it('says why a redacted field has no Set editor and links to unmasking', async () => {
const fixture = detail(() => Promise.resolve('about pin'));
fixture.componentRef.setInput('node', field('pin', { redacted: 'input-type' }));
await settle(fixture);
const host = fixture.nativeElement as HTMLElement;
expect(host.querySelector('#field-value')).toBeNull();
expect(text(fixture)).toContain('this field is redacted (password input)');
const link = host.querySelector<HTMLAnchorElement>('a')!;
expect(link.textContent).toContain('How to unmask it');
expect(link.href).toContain('security.md#opt-fields-in-or-out');
});
});

describe('FormsInspector fields', () => {
Expand Down Expand Up @@ -189,3 +201,84 @@ describe('FormsInspector fields', () => {
expect(document.activeElement).toBe(row);
});
});

describe('FormsInspector pick', () => {
it('turns into Cancel picking and cancels from the button and Escape', async () => {
const calls: Record<string, unknown>[] = [];
const pending: ((value: unknown) => void)[] = [];
const fixture = TestBed.createComponent(FormsInspector);
fixture.componentRef.setInput(
'rpc',
fakeClient(
(_name, arg) => {
calls.push(arg);
if (arg['action'] === 'pick') return new Promise((resolve) => pending.push(resolve));
return Promise.resolve({ ok: true, message: 'Picking cancelled.' });
},
[form],
),
);
document.body.append(fixture.nativeElement);
await settle(fixture);
const host = fixture.nativeElement as HTMLElement;

button(fixture, 'Pick field on page').click();
await settle(fixture);
const cancel = button(fixture, 'Cancel picking');
expect(cancel.getAttribute('aria-pressed')).toBe('true');
cancel.click();
await settle(fixture);
expect(calls.at(-1)).toEqual({ action: 'cancel-pick', formId: form.id });
pending.shift()!({ ok: false, error: 'Picking cancelled.' });
await settle(fixture);
expect(button(fixture, 'Pick field on page').getAttribute('aria-pressed')).toBe('false');
expect(text(fixture)).toContain('Picking cancelled.');

button(fixture, 'Pick field on page').click();
await settle(fixture);
calls.length = 0;
host.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await settle(fixture);
expect(calls).toEqual([{ action: 'cancel-pick', formId: form.id }]);
});

it('ignores the answer of a superseded pick', async () => {
const pending: ((value: unknown) => void)[] = [];
const fixture = TestBed.createComponent(FormsInspector);
fixture.componentRef.setInput(
'rpc',
fakeClient(() => new Promise((resolve) => pending.push(resolve)), [form]),
);
await settle(fixture);
const inspector = fixture.componentInstance;
void inspector.pick();
void inspector.pick();
await settle(fixture);
pending[1]({ ok: true, formId: form.id, path: 'email' });
await settle(fixture);
pending[0]({ ok: false, error: 'Picking cancelled.' });
await settle(fixture);
expect(inspector.message()).toBe('Picked email.');
expect(inspector.picking()).toBeNull();
});
});

describe('FormsInspector redaction', () => {
it('labels redacted fields in plain words', async () => {
const secret: CollectedForm = {
...form,
root: {
...form.root,
children: [field('pinCode', { redacted: 'key', value: '[redacted]' })],
},
};
const fixture = TestBed.createComponent(FormsInspector);
fixture.componentRef.setInput(
'rpc',
fakeClient(() => Promise.resolve(''), [secret]),
);
await settle(fixture);
expect(text(fixture)).toContain('redacted: name looks secret');
expect(text(fixture)).not.toContain('redacted (key)');
});
});
36 changes: 36 additions & 0 deletions app/src/__tests__/panel-highlight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { DevframeRpcClient } from 'devframe/client';
import { describe, expect, it, vi } from 'vitest';
import { clearHighlightsOnHide } from '../rpc';

function fakeClient() {
const callEvent = vi.fn(() => Promise.resolve());
const client = { scope: () => ({ rpc: { callEvent } }) } as unknown as DevframeRpcClient;
return { client, callEvent };
}

describe('panel highlights', () => {
it('clears the page and form highlights when the panel page hides', () => {
const { client, callEvent } = fakeClient();
const stop = clearHighlightsOnHide(() => client);
expect(callEvent).not.toHaveBeenCalled();

dispatchEvent(new Event('pagehide'));
expect(callEvent.mock.calls).toEqual([
['request-page-highlight', null],
['request-form-highlight', null],
]);

callEvent.mockClear();
stop();
expect(callEvent).toHaveBeenCalledTimes(2);
callEvent.mockClear();
dispatchEvent(new Event('pagehide'));
expect(callEvent).not.toHaveBeenCalled();
});

it('does nothing before the panel connects', () => {
const stop = clearHighlightsOnHide(() => null);
expect(() => dispatchEvent(new Event('pagehide'))).not.toThrow();
stop();
});
});
Loading
Loading