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
81 changes: 81 additions & 0 deletions app/src/__tests__/forms-panels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,49 @@ describe('FormsLint', () => {
await settle(fixture);
expect(text(fixture)).toContain('No problems found');
});

it('turns a finding path into a Show field button and leaves form-level findings as text', async () => {
const fixture = TestBed.createComponent(FormsLint);
fixture.componentRef.setInput('formId', form.id);
fixture.componentRef.setInput(
'rpc',
fakeClient(() => Promise.resolve(lintFindings)),
);
const shown: string[] = [];
fixture.componentInstance.showField.subscribe((path) => shown.push(path));
await settle(fixture);
const host = fixture.nativeElement as HTMLElement;
const items = host.querySelectorAll('.findings li');
expect(items).toHaveLength(2);
expect(items[1].querySelector('button')).toBeNull();

const link = items[0].querySelector<HTMLButtonElement>('button.summary-link');
expect(link?.textContent?.replace(/\s+/g, ' ').trim()).toBe('Show field address.zip');
link!.click();
expect(shown).toEqual(['address.zip']);
});
});

const lintFindings = [
{
rule: 'missing-label',
severity: 'warning',
form: 'signup@p1',
label: 'Signup.form',
path: 'address.zip',
message: 'The input has no label.',
fix: 'Add a label.',
},
{
rule: 'no-submit',
severity: 'info',
form: 'signup@p1',
label: 'Signup.form',
message: 'The form has no submit button.',
fix: 'Add a submit button.',
},
];

describe('FormsSubmit', () => {
it('shows a failure instead of loading forever when the explain call fails', async () => {
const fixture = TestBed.createComponent(FormsSubmit);
Expand Down Expand Up @@ -426,6 +467,46 @@ describe('FormsInspector error summary', () => {
});
});

describe('FormsInspector lint', () => {
it('opens the field of a finding on the Fields tab with focus on the detail heading', async () => {
const nested: CollectedForm = {
...form,
root: {
...form.root,
children: [
field('email'),
{ ...field('address'), type: 'group', children: [field('address.zip')] },
],
},
};
const fixture = TestBed.createComponent(FormsInspector);
fixture.componentRef.setInput(
'rpc',
fakeClient(
(name) => Promise.resolve(name.includes('lint') ? lintFindings : 'about zip'),
[nested],
),
);
document.body.append(fixture.nativeElement);
await settle(fixture);
const host = fixture.nativeElement as HTMLElement;
const inspector = fixture.componentInstance;
inspector.filter.set('email');
inspector.tab_.set('lint');
await settle(fixture);

expect(host.querySelectorAll('app-forms-lint button.summary-link')).toHaveLength(1);
button(fixture, 'Show field address.zip').click();
await settle(fixture);

expect(inspector.tab_()).toBe('fields');
expect(inspector.filter()).toBe('');
expect(inspector.fieldPath()).toBe('address.zip');
expect(host.querySelector('app-forms-field-detail')).not.toBeNull();
expect(document.activeElement?.id).toBe('forms-field-heading');
});
});

describe('FormsInspector pick', () => {
it('turns into Cancel picking and cancels from the button and Escape', async () => {
const calls: Record<string, unknown>[] = [];
Expand Down
32 changes: 7 additions & 25 deletions app/src/pages/forms-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,12 @@ function countFields(node: FormFieldNode): number {
<app-forms-submit [formId]="form.id" [version]="version()" [rpc]="rpc()" />
}
@case ('lint') {
<app-forms-lint [formId]="form.id" [version]="version()" [rpc]="rpc()" />
<app-forms-lint
[formId]="form.id"
[version]="version()"
[rpc]="rpc()"
(showField)="showField($event)"
/>
}
}
</div>
Expand Down Expand Up @@ -816,29 +821,6 @@ function countFields(node: FormFieldNode): number {
color: var(--text);
overflow-wrap: anywhere;
}
.summary-link {
display: inline;
padding: 0;
border: none;
border-radius: 4px;
background: none;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.summary-link code:first-of-type {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.summary-link:hover code:first-of-type {
color: var(--accent-hover);
}
.summary-link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.error-summary .kind-tag {
margin-left: 6px;
color: var(--text-2);
Expand Down Expand Up @@ -1557,7 +1539,7 @@ export class FormsInspector {
row?.focus();
}

/** Opens a field from the error summary: Fields tab, filters that hide it cleared, detail focused. */
/** Opens a field from the error summary or a lint finding: Fields tab, filters that hide it cleared, detail focused. */
showField(path: string) {
this.tab_.set('fields');
const listed = () => this.rows().some((row) => row.node.path === path);
Expand Down
18 changes: 8 additions & 10 deletions app/src/pages/forms-report.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, effect, input, linkedSignal, signal, untracked } from '@angular/core';
import { Component, effect, input, linkedSignal, output, signal, untracked } from '@angular/core';
import type { DevframeRpcClient } from 'devframe/client';
import { FORMS_STYLES, formsCall, plain, type FormLintFinding } from './forms-types';

Expand Down Expand Up @@ -170,9 +170,12 @@ export class FormsSubmit {
>{{ f.severity }}</span
>
<code class="rule">{{ f.rule }}</code>
@if (f.path) {
@if (f.path; as path) {
<span class="muted path"
>at <code>{{ f.path }}</code></span
>at
<button type="button" class="summary-link" (click)="showField.emit(path)">
<span class="sr-only">Show field </span><code>{{ path }}</code>
</button></span
>
}
</div>
Expand Down Expand Up @@ -260,13 +263,6 @@ export class FormsSubmit {
font-size: 12.5px;
overflow-wrap: anywhere;
}
.path code {
color: #fde68a;

@include m.light {
color: var(--accent);
}
}
.message,
.fix {
margin: 0;
Expand All @@ -291,6 +287,8 @@ export class FormsLint {
formId = input.required<string>();
version = input(0);
rpc = input<DevframeRpcClient | null>(null);
/** Emits a finding's field path so the page can open that field. */
showField = output<string>();

readonly findings = linkedSignal<string, FormLintFinding[] | null>({
source: this.formId,
Expand Down
22 changes: 22 additions & 0 deletions app/src/pages/forms-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,28 @@ export const FORMS_STYLES = `
color: #fff;
}
}
.summary-link {
display: inline;
padding: 0;
border: none;
border-radius: 4px;
background: none;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.summary-link code:first-of-type {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.summary-link:hover code:first-of-type {
color: var(--accent-hover);
}
.summary-link:focus-visible {
@include m.focus-ring;
}
.field-input {
min-width: 0;
height: 34px;
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/content/inspectors/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ What submit does, and why it might do nothing. It also shows what the form sends

Form bugs and model-aware accessibility checks, each with a fix. For generic accessibility checks, run axe on the page.

A finding about one field shows its path, such as `at address.zip`. Click the path to open that field, the same way an **Error summary** entry does: the tab switches to the **Fields** view, opens the field's [details](#field-details) and moves focus to them. Findings about the whole form have no path.

If the devtools server does not answer, the Submit view, the Lint view and the field details say so and offer **Try again**.

### Actions bar
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading