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
2 changes: 1 addition & 1 deletion .claude/skills/devtools-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ Edit `app/src/styles/main.scss` (`$accent`) or the maps in `_palette.scss`. Neve

## Verify

Use the `devtools-verify` skill: template check with `ngc`, rebuild `extension/ui`, then the axe and 360px overflow audit on every page you touched, in the popup and at `/__devframes/`.
Add component tests in `app/src/__tests__` as `*.test.ts` (see `app/src/__tests__/forms-panels.test.ts`); `pnpm test:panel` runs them. Then use the `devtools-verify` skill: template check with `ngc`, rebuild `extension/ui`, then the axe and 360px overflow audit on every page you touched, in the popup and at `/__devframes/`.
6 changes: 5 additions & 1 deletion .claude/skills/devtools-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pnpm skills:check
pnpm typecheck
pnpm test
pnpm test:devtools
pnpm test:panel
pnpm test:axe
pnpm build
pnpm extension:build
pnpm devtools:build-pkg
Expand All @@ -26,14 +28,16 @@ CI runs `pnpm exec nx affected -t test build` instead of the plain `pnpm test` a

When the change affects behaviour, an option, a UI label or an agent tool, update the matching page in `apps/docs` in the same change (use the `devtools-docs` skill). When the change touches the docs site (apps/docs) or `README.md`, also run the build checks in the `devtools-docs` skill.

`pnpm typecheck` does not type-check panel templates. Also run:
`pnpm typecheck` runs `ngc` on the panel (`app/tsconfig.json`, with `strictTemplates`) and on the Analog demo, so it catches template errors. To run the panel template check alone:

```sh
NO_COLOR=1 pnpm exec ngc -p app/tsconfig.json --noEmit
```

and treat any `error TS` or `error NG` line as a failure. Strip color codes before grepping the output, or errors slip through.

`pnpm test:axe` needs Chromium (`pnpm exec playwright install chromium` once). It runs axe on every panel tab and hub view against a static report of Angular Travel, in light and dark. It does not replace the browser checks below, which use real data from the demos.

## 2. Run the demos

`pnpm build` is a production build and turns the in-page launcher off. For manual checks rebuild in development mode:
Expand Down
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ See docs/contributing/commit-message-guidelines.md

- [ ] `pnpm commit:check` (commit messages follow the guidelines)
- [ ] `pnpm format:check`
- [ ] `pnpm typecheck` and the `ngc` template check (`pnpm exec ngc -p app/tsconfig.json --noEmit`)
- [ ] `pnpm test` and `pnpm test:devtools`
- [ ] `pnpm typecheck` (includes the `ngc` template checks)
- [ ] `pnpm test`, `pnpm test:devtools` and `pnpm test:panel`
- [ ] `pnpm skills:check` (when `.claude/` changed)
- [ ] Docs in `apps/docs` updated and `pnpm docs:build` passes (when behavior, options, UI labels or agent tools changed), or the `no-docs` label added with the reason below
- [ ] `pnpm extension:build` and `extension/ui` committed (when `app/` changed)
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: Test devtools package
run: pnpm test:devtools

- name: Test devtools panel
run: pnpm test:panel

- name: Build Chrome extension
run: pnpm extension:build

Expand All @@ -65,3 +68,26 @@ jobs:

- name: Smoke-test the CLI
run: node bin.mjs --help

axe:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium

- name: Check the panel with axe
run: pnpm test:axe
65 changes: 65 additions & 0 deletions app/src/__tests__/base-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { detectBaseURL } from '../base-url';

function at(href: string) {
const url = new URL(href);
return {
href: url.href,
origin: url.protocol === 'chrome-extension:' ? `${url.protocol}//${url.host}` : url.origin,
protocol: url.protocol,
pathname: url.pathname,
search: url.search,
};
}

describe('detectBaseURL', () => {
it('tries the embedded server path, then the folder the panel is served from', () => {
expect(detectBaseURL(at('http://localhost:5173/'))).toEqual(['/__ng-devtools/', './']);
expect(detectBaseURL(at('http://127.0.0.1:4782/reports/today/'))).toEqual([
'/__ng-devtools/',
'./',
]);
});

it('lets devframe pick the base when the panel is served under the server path', () => {
expect(detectBaseURL(at('http://localhost:4200/__ng-devtools/'))).toBeUndefined();
expect(detectBaseURL(at('http://localhost:4200/__devframes/ng-devtools/'))).toBeUndefined();
});

it('accepts a same origin baseURL query', () => {
const loc = at('http://localhost:4200/panel/?baseURL=/custom/');
expect(detectBaseURL(loc)).toBe('/custom/');
const absolute = at(
'http://localhost:4200/panel/?baseURL=' + encodeURIComponent('http://localhost:4200/x/'),
);
expect(detectBaseURL(absolute)).toBe('http://localhost:4200/x/');
});

it('ignores a cross origin baseURL query on a web page', () => {
const loc = at(
'http://localhost:4200/panel/?baseURL=' + encodeURIComponent('https://evil.example/'),
);
expect(detectBaseURL(loc)).toEqual(['/__ng-devtools/', './']);
});

it('ignores a malformed baseURL query instead of throwing', () => {
const loc = at('http://localhost:4200/panel/?baseURL=' + encodeURIComponent('http://['));
expect(() => detectBaseURL(loc)).not.toThrow();
expect(detectBaseURL(loc)).toEqual(['/__ng-devtools/', './']);
});

it('accepts an http or https host from the extension panel', () => {
const base = 'chrome-extension://abc/ui/index.html?baseURL=';
const http = at(base + encodeURIComponent('http://localhost:4200/__ng-devtools/'));
expect(detectBaseURL(http)).toBe('http://localhost:4200/__ng-devtools/');
const https = at(base + encodeURIComponent('https://app.example/__ng-devtools/'));
expect(detectBaseURL(https)).toBe('https://app.example/__ng-devtools/');
});

it('rejects other schemes from the extension panel', () => {
const loc = at(
'chrome-extension://abc/ui/index.html?baseURL=' + encodeURIComponent('javascript:alert(1)'),
);
expect(detectBaseURL(loc)).toEqual(['/__ng-devtools/', './']);
});
});
56 changes: 56 additions & 0 deletions app/src/__tests__/dashboard-cards.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import { componentsCard, routesCard, storeCard } from '../pages/dashboard';

describe('dashboard cards', () => {
it('counts source components when the rows carry no kind', () => {
expect(componentsCard([{}, {}, {}])).toEqual({ value: 3, sub: 'discovered in source' });
});

it('splits live components from directives', () => {
expect(
componentsCard([{ kind: 'component' }, { kind: 'component' }, { kind: 'directive' }]),
).toEqual({ value: 2, sub: 'components · 1 directive' });
expect(componentsCard([{ kind: 'component' }])).toEqual({
value: 1,
sub: 'components · 0 directives',
});
});

it('counts source route entries when the rows carry no kind', () => {
expect(routesCard([{ path: 'a' }, { path: 'b' }])).toEqual({
value: 2,
sub: 'route entries in source',
});
});

it('counts distinct navigable paths and redirects', () => {
expect(
routesCard([
{ kind: 'page', fullPath: '/a' },
{ kind: 'page', fullPath: '/a' },
{ kind: 'page', path: 'b' },
{ kind: 'redirect', path: '' },
{ kind: 'redirect', path: 'old' },
]),
).toEqual({ value: 2, sub: 'navigable paths · 2 redirects' });
});

it('counts source declarations when the rows carry no kind', () => {
expect(storeCard([{}, {}])).toEqual({ value: 2, sub: 'declarations in source' });
});

it('lists NgRx declaration kinds, most common first', () => {
expect(
storeCard([
{ kind: 'action' },
{ kind: 'reducer' },
{ kind: 'action' },
{ kind: 'signal-store' },
{ kind: 'custom' },
]),
).toEqual({
value: 5,
sub: '2 actions · 1 reducer · 1 signal store · 1 custom',
});
});
});
9 changes: 9 additions & 0 deletions app/src/__tests__/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, expect, it } from 'vitest';
import { time } from '../format';

describe('time', () => {
it('formats a timestamp as the local time of day', () => {
const at = new Date(2026, 0, 2, 13, 4, 5).getTime();
expect(time(at)).toBe(new Date(at).toLocaleTimeString());
});
});
Loading
Loading