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
35 changes: 35 additions & 0 deletions app/src/__tests__/router-panels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,41 @@ describe('RouteCurrent page switch', () => {
});

describe('RouteCurrent', () => {
it('tells screen readers how each active route is nested', async () => {
const fixture = mount(
RouteCurrent,
page({
snapshot: {
...page().snapshot!,
root: {
path: '',
url: '',
outlet: 'primary',
params: {},
data: {},
children: [
{
path: 'users',
url: 'users',
outlet: 'primary',
params: {},
data: {},
children: [
{ path: ':id', url: '7', outlet: 'primary', params: {}, data: {}, children: [] },
],
},
],
},
},
}),
offline,
);
await settle(fixture);
const cells = Array.from(el(fixture).querySelectorAll('td.path'));
const hidden = cells.map((cell) => cell.querySelector('.visually-hidden')?.textContent ?? '');
expect(hidden).toEqual(['', ', level 2, child of (root)', ', level 3, child of /users']);
});

it('reports an unreachable page on Abort instead of #undefined', async () => {
const fixture = mount(
RouteCurrent,
Expand Down
19 changes: 18 additions & 1 deletion app/src/__tests__/store-types.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { pretty, short } from '../pages/store-types';
import { changeCountText, moreChangesText, pretty, short } from '../pages/store-types';

describe('pretty', () => {
it('shows every field of an app object that has its own @type key', () => {
Expand All @@ -20,3 +20,20 @@ describe('pretty', () => {
expect(pretty({ '@type': 'number', value: 'NaN' })).toBe('NaN');
});
});

describe('change counts', () => {
const row = { path: 'a', op: 'change' as const, before: 0, after: 1 };

it('counts the rows a long diff left out', () => {
expect(changeCountText({ diff: [] })).toBe('no change');
expect(changeCountText({ diff: [row] })).toBe('1 change');
expect(changeCountText({ diff: [row, row], diffTruncated: 10 })).toBe('12 changes');
expect(changeCountText({ diff: [row], diffTruncated: 1000 })).toBe('1001+ changes');
});

it('says how many changes are not shown', () => {
expect(moreChangesText(1)).toBe('1 more change not shown');
expect(moreChangesText(10)).toBe('10 more changes not shown');
expect(moreChangesText(1000)).toBe('1000+ more changes not shown');
});
});
33 changes: 27 additions & 6 deletions app/src/pages/analog-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ interface Finding {
severity: 'error' | 'warning' | 'info';
file?: string;
path?: string;
relatedFile?: string;
message: string;
fix: string;
}
Expand Down Expand Up @@ -354,9 +355,9 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
<span class="label">Server calls</span>
<span class="value">{{ allCalls().length }}</span>
</div>
<div class="stat" [attr.data-tone]="findings().length ? 'warn' : 'good'">
<div class="stat" [attr.data-tone]="lintTone() || 'good'">
<span class="label">Issues</span>
<span class="value">{{ findings().length }}</span>
<span class="value">{{ lintCount().errors + lintCount().warnings }}</span>
</div>
@if (page(); as p) {
<div class="stat wide">
Expand Down Expand Up @@ -1289,6 +1290,9 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
.stat[data-tone='good'] .value {
color: var(--ok);
}
.stat[data-tone='bad'] .value {
color: var(--danger-text);
}

.tabs {
display: inline-flex;
Expand Down Expand Up @@ -1348,6 +1352,11 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
background: color-mix(in srgb, var(--warn) 12%, transparent);
color: var(--warn-text);
}
.count[data-tone='bad'],
[role='tab'][aria-selected='true'] .count[data-tone='bad'] {
background: color-mix(in srgb, var(--danger) 12%, transparent);
color: var(--danger-text);
}

.panel {
display: grid;
Expand Down Expand Up @@ -2355,8 +2364,20 @@ export class AnalogInspector {
}
return Array.from(cards.values()).sort((a, b) => order[a.severity] - order[b.severity]);
});
/** Errors and warnings; info findings are advice, so they are not counted. */
readonly lintCount = computed(() => {
const findings = this.findings();
return {
errors: findings.filter((f) => f.severity === 'error').length,
warnings: findings.filter((f) => f.severity === 'warning').length,
};
});
readonly lintTone = computed(() => {
const { errors, warnings } = this.lintCount();
return errors ? 'bad' : warnings ? 'warn' : '';
});
readonly views = computed(() => {
const errors = this.findings().length;
const { errors, warnings } = this.lintCount();
return [
{ id: 'routes' as View, label: 'Routes', count: this.pageCount(), tone: '' },
{ id: 'server' as View, label: 'Server', count: this.allCalls().length, tone: '' },
Expand All @@ -2367,7 +2388,7 @@ export class AnalogInspector {
count: this.project()?.content.length ?? 0,
tone: '',
},
{ id: 'lint' as View, label: 'Lint', count: errors, tone: errors ? 'warn' : '' },
{ id: 'lint' as View, label: 'Lint', count: errors + warnings, tone: this.lintTone() },
];
});

Expand Down Expand Up @@ -2423,7 +2444,7 @@ export class AnalogInspector {
if (!client) return;
const run = ++this.refreshRun;
const [findings, render] = await Promise.all([
call<Finding[]>(client, 'analog-lint'),
call<Finding[]>(client, 'analog-lint', this.hostPageId ?? undefined),
call<{ rows: RenderRow[]; plan: PrerenderPlan }>(client, 'analog-render'),
]);
if (run !== this.refreshRun) return;
Expand Down Expand Up @@ -2519,7 +2540,7 @@ export class AnalogInspector {
const finding = this.findings().find(
(f) => f.rule === 'content-shadows-page' && f.file === file,
);
return finding?.message.match(/(\/\S+\.page\.ts)/)?.[1];
return finding?.relatedFile;
}

contentUrl(file: string): string | undefined {
Expand Down
16 changes: 12 additions & 4 deletions app/src/pages/route-current.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import type { ComponentFocus } from '../types/component-focus.types';
interface RouteRow {
route: ActiveRoute;
depth: number;
label: string;
/** What a screen reader hears for the nesting the indent shows, on child rows. */
nesting?: string;
}

interface OutletRow {
Expand Down Expand Up @@ -95,7 +98,10 @@ interface OutletRow {
@for (row of rows(); track $index) {
<tr>
<td class="path" [style.padding-left.px]="14 + row.depth * 16">
{{ row.depth === 0 && !row.route.path ? '(root)' : '/' + row.route.path }}
{{ row.label }}
@if (row.nesting) {
<span class="visually-hidden">, {{ row.nesting }}</span>
}
@if (row.route.outlet !== 'primary') {
<span class="tag">{{ row.route.outlet }}</span>
}
Expand Down Expand Up @@ -350,9 +356,11 @@ export class RouteCurrent {

readonly rows = computed(() => {
const rows: RouteRow[] = [];
const visit = (route: ActiveRoute, depth: number) => {
rows.push({ route, depth });
for (const child of route.children) visit(child, depth + 1);
const visit = (route: ActiveRoute, depth: number, parent?: string) => {
const label = depth === 0 && !route.path ? '(root)' : '/' + route.path;
const nesting = parent === undefined ? undefined : `level ${depth + 1}, child of ${parent}`;
rows.push({ route, depth, label, nesting });
for (const child of route.children) visit(child, depth + 1, label);
};
const root = this.page().snapshot?.root;
if (root) visit(root, 0);
Expand Down
16 changes: 10 additions & 6 deletions app/src/pages/store-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { actionAllowed, actionBlockedMessage, panelConfig } from '../devtools-co
import { LimitNote } from '../ui/limit-note';
import { Select, type SelectOption } from '../ui/select';
import {
changeCountText,
moreChangesText,
pretty,
sameNgrxLog,
short,
Expand Down Expand Up @@ -387,12 +389,7 @@ const CLASSIC_KINDS = new Set([
@if (entry.origin) {
<span class="tag">{{ entry.origin }}</span>
}
{{
entry.diff.length === 0
? 'no change'
: entry.diff.length +
(entry.diff.length === 1 ? ' change' : ' changes')
}}
{{ changesLabel(entry) }}
·
{{ formatTime(entry.timestamp) }}
@if (entry.durationMs !== undefined) {
Expand Down Expand Up @@ -634,6 +631,9 @@ const CLASSIC_KINDS = new Set([
</li>
}
</ul>
@if (selected.diffTruncated) {
<p class="muted">{{ moreChanges(selected.diffTruncated) }}</p>
}
} @else {
<p class="muted">The state did not change.</p>
}
Expand Down Expand Up @@ -2110,6 +2110,10 @@ export class StoreInspector {
return ORIGIN_TEXT[origin] ?? origin;
}

readonly changesLabel = changeCountText;

readonly moreChanges = moreChangesText;

opLabel(op: string) {
return op === 'add' ? 'added' : op === 'remove' ? 'removed' : 'changed';
}
Expand Down
23 changes: 23 additions & 0 deletions app/src/pages/store-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ export interface NgrxLogEntry {
/** `source: 'signal-store'` only: the event that this state change was correlated with. */
causedByEvent?: { type: string; payload?: unknown };
unrestorable?: 'dropped' | 'not-recorded';
/**
* How many more changes the state went through than `diff` holds (it keeps at most 50).
* The page counts up to {@link DIFF_MORE_CAP}, so at the cap it is a lower bound.
*/
diffTruncated?: number;
}

/** The most changes the page counts past a full diff (`NGRX_DIFF_MORE_CAP` in the package). */
export const DIFF_MORE_CAP = 1000;

/** "12 more changes not shown", or "1000+ more changes not shown" at the cap. */
export function moreChangesText(count: number): string {
const n = count >= DIFF_MORE_CAP ? `${DIFF_MORE_CAP}+` : String(count);
return `${n} more ${count === 1 ? 'change' : 'changes'} not shown`;
}

/** The change count of a log entry, counting the rows left out of a long diff. */
export function changeCountText(entry: Pick<NgrxLogEntry, 'diff' | 'diffTruncated'>): string {
const more = entry.diffTruncated ?? 0;
const total = entry.diff.length + more;
if (total === 0) return 'no change';
const n = more >= DIFF_MORE_CAP ? `${total}+` : String(total);
return `${n} ${total === 1 ? 'change' : 'changes'}`;
}

export interface NgrxPage {
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/agents/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The injector hierarchy the page last reported, with the providers at each level.

### ngrx-store

Each `@ngrx/signals` store on the page: state, computed values, methods, and the component fields that reference it. It also holds the `@ngrx/store` state and the change log, with a state diff per entry. The log records method calls, `patchState` writes, dispatched actions and restores. An `@ngrx/store` action has an `origin`: `dispatch`, `effect` or `reactive`. `classic.paused` is `true` while a restore holds `@ngrx/store` on a past state. An `@ngrx/store` entry with `unrestorable` cannot be restored: `dropped` means Store DevTools no longer holds the action (it was dropped past `maxAge`, or the history was committed, reset or imported), and `not-recorded` means Store DevTools never recorded it. `dropped` counts the older log entries removed at [`limits.changeLog`](../getting-started/configuration.md#limits).
Each `@ngrx/signals` store on the page: state, computed values, methods, and the component fields that reference it. It also holds the `@ngrx/store` state and the change log, with a state diff per entry. A diff holds up to 50 changes, and `diffTruncated` counts the ones left out, up to 1000. The log records method calls, `patchState` writes, dispatched actions and restores. An `@ngrx/store` action has an `origin`: `dispatch`, `effect` or `reactive`. `classic.paused` is `true` while a restore holds `@ngrx/store` on a past state. An `@ngrx/store` entry with `unrestorable` cannot be restored: `dropped` means Store DevTools no longer holds the action (it was dropped past `maxAge`, or the history was committed, reset or imported), and `not-recorded` means Store DevTools never recorded it. `dropped` counts the older log entries removed at [`limits.changeLog`](../getting-started/configuration.md#limits).

### forms

Expand Down
Loading
Loading