Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
622b036
fix(pipes): list the single page and watch missing templates in pipe …
erkamyaman Nov 8, 2026
450d07d
fix(pipes): keep the selected page when opening a pipe from Components
erkamyaman Nov 8, 2026
a0538c0
fix(router): fill query placeholders of a static redirect in the preview
erkamyaman Nov 8, 2026
d5b1d64
chore: format generated docs pages with the Prettier API
erkamyaman Nov 8, 2026
8ca5463
docs: name the package root entry point and fix the form and provider…
erkamyaman Nov 8, 2026
acbd031
docs: say how inspect-signals finds a directive host and that try-web…
erkamyaman Nov 8, 2026
783f6a8
fix(store): keep the store filter and refusals in ngrx-explain-change
erkamyaman Nov 8, 2026
449edb8
fix(components): announce a repeated keyboard pick message again
erkamyaman Nov 8, 2026
31a628b
fix(signals): keep a focus page pending and drop a stale focused signal
erkamyaman Nov 8, 2026
0fe8296
fix(store): write signal store diff paths the way the state tree does
erkamyaman Nov 8, 2026
e5b169a
perf(store): walk only the open nodes of the state tree
erkamyaman Nov 8, 2026
6cc554b
fix(forms): read a control in a disabled fieldset as disabled
erkamyaman Nov 8, 2026
c67b9c5
fix(components): drop a component detail selector that is not text
erkamyaman Nov 8, 2026
8e0da3b
docs: split the component-tree resource and say when the selector is …
erkamyaman Nov 8, 2026
278200c
fix(components): report change detection errors from load-defer-block
erkamyaman Nov 8, 2026
1ec5e7b
test(analog): give the seeded payload test the setup timeout
erkamyaman Nov 8, 2026
32a9619
fix(analog): keep the Nitro task confirm panel open when a run fails
erkamyaman Nov 8, 2026
47f437d
fix(popup): apply a counts launcher change made in a popped-out panel
erkamyaman Nov 8, 2026
72cc529
docs: note the extension frame URL limit and Ionic pages without Capa…
erkamyaman Nov 8, 2026
9a30f1d
fix(forms): count submit buttons in a disabled fieldset as disabled
erkamyaman Nov 8, 2026
48ad7a6
docs: say where the panel and the popup each keep the counts launcher…
erkamyaman Nov 8, 2026
1e14b3d
refactor(ui): use the shared roving list in the Pipes and Capacitor l…
erkamyaman Nov 8, 2026
e2f35f1
refactor: move message redaction into redaction.ts
erkamyaman Nov 8, 2026
224a691
ci: fail the docs check when code changes without docs
erkamyaman Nov 8, 2026
cd07d16
feat(ui): link the connection error states to the troubleshooting docs
erkamyaman Nov 8, 2026
2e1a409
ci: run the Vite plugin tests on Vite 6 and Vite 7
erkamyaman Nov 8, 2026
adfbf73
docs: generate an Angular debug API version table from debug-api.ts
erkamyaman Nov 8, 2026
90d6028
docs: list Ionic and the Firefox build as available platforms
erkamyaman Nov 8, 2026
7c64aa6
refactor: keep the changed files under their line limits
erkamyaman Nov 8, 2026
65882d6
build(extension): rebuild the extension UI
erkamyaman Nov 8, 2026
7326cad
Merge remote-tracking branch 'origin/main' into fix/pre-release-cleanup
erkamyaman Nov 8, 2026
38fbae3
Merge remote-tracking branch 'origin/main' into fix/pre-release-cleanup
erkamyaman Nov 8, 2026
dbc7b0d
fix(components): mask secrets in the load-defer-block render error
erkamyaman Nov 8, 2026
6dad036
fix(ui): move the roving Tab stop only to a row that becomes selected
erkamyaman Nov 8, 2026
25bb2bd
fix(router): copy repeated and missing redirect query params as Angul…
erkamyaman Nov 8, 2026
34f5224
docs: correct the form and provider recipes and the autocomplete rule
erkamyaman Nov 8, 2026
a750234
docs: shorten the settings, inspect-signals and Pipes recording text
erkamyaman Nov 8, 2026
c35ec74
build(extension): rebuild the extension UI
erkamyaman Nov 8, 2026
c3c7cea
docs: narrow the component-not-found case in the injection recipe
erkamyaman Nov 8, 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
28 changes: 28 additions & 0 deletions .github/workflows/compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,31 @@ jobs:

- name: Test the page side
run: pnpm exec vitest run --root packages/devtools --config vitest.compat.config.ts

vite:
name: Vite ${{ matrix.vite }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# The `vite` peer range is `>=6`; the workspace installs the newest major.
vite: ['6', '7']
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: ./.github/actions/setup

- name: Install Vite ${{ matrix.vite }}
env:
VITE_MAJOR: ${{ matrix.vite }}
# Only this job's copy of the workspace changes; the committed lockfile stays as it is.
run: |
printf "\noverrides:\n vite: '^%s.0.0'\n" "$VITE_MAJOR" >> pnpm-workspace.yaml
pnpm install --no-frozen-lockfile
echo "- vite $(node -p "require('vite/package.json').version")" | tee -a "$GITHUB_STEP_SUMMARY"

- name: Test the Vite plugin
run: pnpm exec vitest run --root packages/devtools src/__tests__/vite- src/__tests__/lan-address.test.ts
3 changes: 2 additions & 1 deletion .github/workflows/docs-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
elif [ "$NO_DOCS" = "true" ]; then
echo "Code changed without docs, marked no-docs."
else
echo "::warning title=Docs not updated::This pull request changes code but no page in apps/docs/src/content. Update the docs in this pull request, or add the no-docs label and say why in the description."
echo "::error title=Docs not updated::This pull request changes code but no page in apps/docs/src/content. Update the docs in this pull request, or add the no-docs label and say why in the description."
printf '%s\n' "$code"
exit 1
fi
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ By taking part you agree to the [Code of Conduct](./CODE_OF_CONDUCT.md). Report
- **A new tab, RPC function or agent tool:** follow the steps in [Development setup](./apps/docs/src/content/contributing/development.md). Describe what an agent tool returns and when it is empty, and add tests.
- **UI changes:** follow the [UI guidelines](docs/contributing/ui-guidelines.md). Use the theme variables, the SCSS mixins and the shared dropdown.
- **Docs changes:** follow the [writing guide](./apps/docs/src/content/contributing/writing-docs.md). Run the docs site with `pnpm docs:dev`.
- **Keep the docs in step with the code:** when a pull request changes behaviour, an option, a UI label or an agent tool, update the matching page in `apps/docs` in the same pull request. If no docs change is needed, add the `no-docs` label and say why in the description. The Docs check workflow warns when code changes without docs.
- **Keep the docs in step with the code:** when a pull request changes behaviour, an option, a UI label or an agent tool, update the matching page in `apps/docs` in the same pull request. If no docs change is needed, add the `no-docs` label and say why in the description. The Docs check workflow fails when code changes without docs.

## Open an issue

Expand Down
32 changes: 32 additions & 0 deletions app/src/__tests__/analog-views.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,38 @@ describe('Analog Nitro tasks', () => {
expect((fixture.nativeElement as HTMLElement).textContent).toContain('[redacted]');
});

it('keeps the confirm panel and payload open when a run fails, and closes it after a run that works', async () => {
let ok = false;
const { rpcClient } = client({
'analog-runtime-config': () => ({ env: [], runtimeConfig: [], options: [] }),
'analog-nitro': () => ({
plugins: [],
tasks: [{ name: 'db:migrate', file: '/src/server/tasks/db/migrate.ts' }],
storage: [],
cachedRules: [],
cacheDirs: [],
entries: [],
notes: [],
}),
'analog-run-nitro-task': () =>
ok
? { ok: true, name: 'db:migrate', status: 200, ms: 1, output: '' }
: { ok: false, name: 'db:migrate', error: 'Task failed.' },
});
const fixture = TestBed.createComponent(AnalogRuntime);
fixture.componentRef.setInput('rpc', rpcClient);
await settle(fixture);
const runtime = fixture.componentInstance;
runtime.askTask('db:migrate');
runtime.taskPayload.set('{"dry":true}');
await runtime.runTask('db:migrate');
expect(runtime.taskConfirm()).toBe('db:migrate');
expect(runtime.taskPayload()).toBe('{"dry":true}');
ok = true;
await runtime.runTask('db:migrate');
expect(runtime.taskConfirm()).toBeNull();
});

it('starts a task once, even when Run task is used again while it runs', async () => {
let answer!: (value: unknown) => void;
const { rpcClient, calls } = client({
Expand Down
16 changes: 16 additions & 0 deletions app/src/__tests__/app-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,22 @@ describe('App scroll memory while content loads', () => {
});
});

describe('App connection error', () => {
it('links to the troubleshooting docs when it cannot reach the server', async () => {
const fixture = TestBed.createComponent(App);
document.body.appendChild(host(fixture));
await settle(fixture);
fixture.componentInstance.connectionFailed.set(true);
await settle(fixture);
const link = host(fixture).querySelector<HTMLAnchorElement>('.connection-error a')!;
expect(link.getAttribute('href')).toBe(
'https://pangular-inspector.dev/getting-started/troubleshooting/#the-panel-says-disconnected',
);
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
fixture.destroy();
});
});

describe('App counts launcher setting', () => {
it('sends the stored counts launcher to the popup once the panel connects', async () => {
localStorage.setItem('pangular-pref:pill', 'true');
Expand Down
7 changes: 7 additions & 0 deletions app/src/__tests__/component-pick-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ describe('keyboard pick', () => {
expect(fixture.componentInstance.announcement()).toBe(
'App <app-root>. Press Enter to pick it.',
);
// The same component again changes the live text, so a screen reader repeats it.
press('ArrowDown');
await settle(fixture);
expect(fixture.componentInstance.announcement()).toBe(
'App <app-root>. Press Enter to pick it.\u00a0',
);
expect(fixture.componentInstance.pickMessage()).toBe('App <app-root>. Press Enter to pick it.');
expect(press('Enter').defaultPrevented).toBe(true);
expect(calls.at(-1)).toEqual({
name: 'request-component-pick-move',
Expand Down
28 changes: 28 additions & 0 deletions app/src/__tests__/page-conventions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,31 @@ describe('pages use the shared state component', () => {
for (const { file, tag } of states) expect(tag, file).toMatch(/\(clear\)=/);
});
});

describe('pages with a keyboard list use the shared roving list', () => {
it('has no hand-written arrow key list handler left in a page', async () => {
// Trees, charts and the graph move in two dimensions and keep their own keys.
const handWritten = [...(await pageSources())]
.filter(([, source]) => /\['ArrowDown',\s*'ArrowUp',\s*'Home',\s*'End'\]/.test(source))
.map(([file]) => file);
expect(handWritten).toEqual([]);
});

it('puts appRovingList on the lists that moved to it', async () => {
const sources = await pageSources();
const lists = {
'capacitor-inspector.ts': '[data-cap-call]',
'pipes-inspector.ts': '[data-pipe-row]',
'network-inspector.ts': '',
'query-inspector.ts': '',
'signal-inspector.ts': '',
'store-inspector.ts': '',
'timeline.ts': '',
};
for (const [file, selector] of Object.entries(lists)) {
const source = sources.get(file) ?? '';
expect(source, file).toContain('appRovingList="');
if (selector) expect(source, file).toContain(`appRovingList="${selector}"`);
}
});
});
26 changes: 26 additions & 0 deletions app/src/__tests__/pipes-record-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,32 @@ describe('pipes live data per page', () => {
expect(fixture.componentInstance.liveFor('price')?.instanceCount).toBe(2);
});

it('shows the page another tab asked for over the page the panel belongs to', async () => {
scopeToPage('A');
const fixture = TestBed.createComponent(PipesInspector);
fixture.componentRef.setInput('pageId', 'B');
fixture.componentRef.setInput(
'rpc',
withPages({
pipes: [live(5)],
async: [],
reportedAt: 0,
instrumented: [],
pages: [
{ pageId: 'A', pipes: [live(3)], async: [] },
{ pageId: 'B', pipes: [live(2)], async: [] },
],
}),
);
for (let i = 0; i < 4; i++) {
await new Promise((resolve) => setTimeout(resolve));
await fixture.whenStable();
}
expect(fixture.componentInstance.liveFor('price')?.instanceCount).toBe(2);
fixture.componentRef.setInput('pageId', null);
expect(fixture.componentInstance.liveFor('price')?.instanceCount).toBe(3);
});

it('lists the template usages of a custom pipe', async () => {
const fixture = await render(
withPages({ pipes: [], async: [], reportedAt: 0, instrumented: [] }),
Expand Down
11 changes: 10 additions & 1 deletion app/src/__tests__/popout-window.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { popoutOpener, returnFromPopout } from '../shell-prefs';
import { popoutOpener, postToOpener, returnFromPopout } from '../shell-prefs';

/** A stand-in for a window, with the fields the helpers read. */
function fakeWindow(name: string, opener: unknown, referrer = '') {
Expand Down Expand Up @@ -42,4 +42,13 @@ describe('the pop-out window', () => {
);
expect(returnFromPopout(fakeWindow('pangular-inspector-k3x9', opener))).toBe(false);
});

it('sends a setting to the app page only from the window the popup opened', () => {
const opener = { postMessage: vi.fn(), location: { origin: 'http://localhost:4200' } };
const message = { type: 'pangular:popup-pill', pill: true };
expect(postToOpener(message, fakeWindow('pangular-inspector-k3x9', opener))).toBe(true);
expect(opener.postMessage).toHaveBeenCalledWith(message, 'http://localhost:4200');
expect(postToOpener(message, fakeWindow('other', opener))).toBe(false);
expect(opener.postMessage).toHaveBeenCalledTimes(1);
});
});
25 changes: 25 additions & 0 deletions app/src/__tests__/roving-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ describe('RovingList', () => {
expect(tabStops(fixture)).toEqual(['c']);
});

it('moves the Tab stop to a row that becomes selected without focus', async () => {
const fixture = mount();
await settle(fixture);
expect(tabStops(fixture)).toEqual(['a']);
// A click in Safari selects a row without focusing it.
fixture.componentInstance.selected.set('c');
await settle(fixture);
expect(tabStops(fixture)).toEqual(['c']);
});

it('keeps the Tab stop when a row that stays selected changes another class', async () => {
const fixture = mount();
const outside = document.createElement('button');
document.body.appendChild(outside);
fixture.componentInstance.selected.set('c');
await settle(fixture);
rows(fixture)[0].focus();
outside.focus();
await settle(fixture);
expect(tabStops(fixture)).toEqual(['a']);
rows(fixture)[2].classList.add('hovered');
await settle(fixture);
expect(tabStops(fixture)).toEqual(['a']);
});

it('moves focus with the arrow keys, Home and End, and stops at the ends', async () => {
const fixture = mount();
await settle(fixture);
Expand Down
36 changes: 34 additions & 2 deletions app/src/__tests__/signal-focus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { DevframeRpcClient } from 'devframe/client';
import { afterEach, describe, expect, it } from 'vitest';
import { SignalInspector } from '../pages/signal-inspector';

const graphOf = (componentId: string) => ({
pageId: 'p1',
const graphOf = (componentId: string, pageId = 'p1') => ({
pageId,
component: { id: componentId, name: 'Cart', tag: 'app-cart', path: 'app-cart' },
nodes: [
{
Expand Down Expand Up @@ -78,4 +78,36 @@ describe('SignalInspector focus', () => {
await settle(fixture);
expect(fixture.componentInstance.selectedId()).toBe('1');
});

it('forgets the focused signal when another component is picked', async () => {
const fixture = TestBed.createComponent(SignalInspector);
const { client, push } = fakeClient();
fixture.componentRef.setInput('rpc', client);
document.body.append(fixture.nativeElement);
await settle(fixture);

fixture.componentRef.setInput('focus', { componentId: 'c1', pageId: 'p1', key: 'count' });
await settle(fixture);
fixture.componentInstance.pickComponent('c2');
push({ graph: graphOf('c1'), pages: { p1: graphOf('c1') } });
await settle(fixture);
expect(fixture.componentInstance.selectedId()).toBeNull();
});

it('shows the page a focus names once that page reports its graph', async () => {
const fixture = TestBed.createComponent(SignalInspector);
const { client, push } = fakeClient();
fixture.componentRef.setInput('rpc', client);
document.body.append(fixture.nativeElement);
await settle(fixture);

fixture.componentRef.setInput('focus', { componentId: 'c5', pageId: 'p2' });
await settle(fixture);
expect(fixture.componentInstance.shownPageId()).toBe('p1');

// p1 reports last, but the focus asked for p2.
push({ graph: graphOf('c0'), pages: { p1: graphOf('c0'), p2: graphOf('c5', 'p2') } });
await settle(fixture);
expect(fixture.componentInstance.shownPageId()).toBe('p2');
});
});
8 changes: 8 additions & 0 deletions app/src/__tests__/store-state-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ describe('stateRows', () => {
expect(childPath('cart', 'items')).toBe('cart.items');
expect(childPath('cart.items', 0)).toBe('cart.items[0]');
expect(childPath('user', 'first name')).toBe('user["first name"]');
// The collector writes a top-level key that needs quotes the same way.
expect(childPath('', 'display name')).toBe('"display name"');
});

it('marks a changed key that needs quotes', () => {
const rows = stateRows(state, { changed: ['user["first name"]'] }).rows;
expect(rows.find((r) => r.path === 'user["first name"]')!.changed).toBe(true);
expect(rows.find((r) => r.path === 'user')!.holdsChange).toBe(true);
});

it('opens the first level, nodes holding a change, and what the user opened', () => {
Expand Down
Loading
Loading