@lariocpt/flawless-react 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/flawless-a11y/SKILL.md +113 -0
- package/.agents/skills/flawless-api/SKILL.md +159 -0
- package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
- package/.agents/skills/flawless-e2e/SKILL.md +155 -0
- package/.agents/skills/flawless-expo/SKILL.md +283 -0
- package/.agents/skills/flawless-findings/SKILL.md +116 -0
- package/.agents/skills/flawless-load/SKILL.md +112 -0
- package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
- package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
- package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
- package/.agents/skills/flawless-react/SKILL.md +403 -0
- package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
- package/.agents/skills/flawless-regen/SKILL.md +135 -0
- package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
- package/LICENSE +21 -0
- package/README.md +608 -2
- package/bin/flawless-react +4 -0
- package/package.json +67 -4
- package/scripts/postinstall.d.mts +46 -0
- package/scripts/postinstall.mjs +127 -0
- package/src/analyse/authShims.ts +220 -0
- package/src/analyse/bootability.ts +257 -0
- package/src/analyse/childBridge.ts +302 -0
- package/src/analyse/childrenIntoSlot.ts +66 -0
- package/src/analyse/compoundFamily.ts +100 -0
- package/src/analyse/dataExports.ts +200 -0
- package/src/analyse/extractChildAttrs.ts +112 -0
- package/src/analyse/extractDataDeps.ts +857 -0
- package/src/analyse/extractDisclosures.ts +186 -0
- package/src/analyse/extractFetchCalls.ts +81 -0
- package/src/analyse/extractFlows.ts +239 -0
- package/src/analyse/extractGuards.ts +270 -0
- package/src/analyse/extractHooks.ts +237 -0
- package/src/analyse/extractInteractions.ts +1421 -0
- package/src/analyse/extractLogging.ts +45 -0
- package/src/analyse/extractNativeA11y.ts +138 -0
- package/src/analyse/extractPredicates.ts +784 -0
- package/src/analyse/extractPropUsage.ts +162 -0
- package/src/analyse/extractProps.ts +1212 -0
- package/src/analyse/extractRenderMode.ts +165 -0
- package/src/analyse/extractRootTag.ts +117 -0
- package/src/analyse/extractState.ts +361 -0
- package/src/analyse/extractStaticText.ts +212 -0
- package/src/analyse/extractVariants.ts +186 -0
- package/src/analyse/harvestApiEndpoints.ts +284 -0
- package/src/analyse/hostProps.ts +185 -0
- package/src/analyse/hostTag.ts +72 -0
- package/src/analyse/htmlContentModel.ts +94 -0
- package/src/analyse/index.ts +744 -0
- package/src/analyse/mountPathCalls.ts +442 -0
- package/src/analyse/nativeContentModel.ts +129 -0
- package/src/analyse/nativeDialect.ts +83 -0
- package/src/analyse/nativeProps.ts +339 -0
- package/src/analyse/ownReturns.ts +31 -0
- package/src/analyse/parseFile.ts +919 -0
- package/src/analyse/pos.ts +8 -0
- package/src/analyse/providerWraps.ts +1977 -0
- package/src/analyse/pureModule.ts +88 -0
- package/src/analyse/reexportTable.ts +148 -0
- package/src/analyse/refs.ts +195 -0
- package/src/analyse/renderPath.ts +441 -0
- package/src/analyse/resolveDepReturns.ts +370 -0
- package/src/analyse/resolveIndexKeys.ts +98 -0
- package/src/analyse/resolveModule.ts +431 -0
- package/src/analyse/resolveTagRootTags.ts +103 -0
- package/src/analyse/serverActionReturns.ts +213 -0
- package/src/analyse/sourceCache.ts +263 -0
- package/src/analyse/spreadSiblings.ts +98 -0
- package/src/analyse/walkChildren.ts +98 -0
- package/src/analyse/zodSchema.ts +424 -0
- package/src/bin.ts +23 -0
- package/src/changed.ts +221 -0
- package/src/cli.ts +560 -0
- package/src/cliFlags.ts +281 -0
- package/src/core.ts +1229 -0
- package/src/docLine.ts +122 -0
- package/src/emitPaths.ts +51 -0
- package/src/expo/detect.ts +184 -0
- package/src/expo/refusal.ts +62 -0
- package/src/expo/routes.ts +145 -0
- package/src/index.ts +95 -0
- package/src/mcp/analysisTools.ts +720 -0
- package/src/mcp/infoTools.ts +135 -0
- package/src/mcp/protocol.ts +130 -0
- package/src/mcp/run.ts +269 -0
- package/src/mcp/runTools.ts +362 -0
- package/src/mcp/server.ts +279 -0
- package/src/mcp/spec.ts +86 -0
- package/src/mcp/support.ts +268 -0
- package/src/mcp/tools.ts +75 -0
- package/src/nextjs/apiRoutes.ts +116 -0
- package/src/nextjs/detect.ts +397 -0
- package/src/nextjs/emit.ts +106 -0
- package/src/nextjs/liveSpec.ts +79 -0
- package/src/nextjs/mswHandlers.ts +55 -0
- package/src/nextjs/providerWrapper.ts +603 -0
- package/src/nextjs/routes.ts +86 -0
- package/src/nextjs/swagger.ts +32 -0
- package/src/packageJson.ts +414 -0
- package/src/platform.ts +18 -0
- package/src/prompt.ts +132 -0
- package/src/reactCompiler.ts +165 -0
- package/src/reporters/findings.ts +156 -0
- package/src/reporters/report.ts +463 -0
- package/src/runner/runTests.ts +402 -0
- package/src/scaffold/adoptStyle.ts +181 -0
- package/src/scaffold/index.ts +253 -0
- package/src/scaffold/openapi.ts +183 -0
- package/src/scaffold/templates.ts +2379 -0
- package/src/scaffold-tests/index.ts +463 -0
- package/src/scaffold-tests/mockValues.ts +208 -0
- package/src/scaffold-tests/mocks.ts +794 -0
- package/src/scaffold-tests/nativeIndex.ts +52 -0
- package/src/scaffold-tests/nativeTemplates.ts +1528 -0
- package/src/scaffold-tests/postDispatch.ts +152 -0
- package/src/scaffold-tests/registry.ts +443 -0
- package/src/scaffold-tests/selectors.ts +493 -0
- package/src/scaffold-tests/selectorsNative.ts +273 -0
- package/src/scaffold-tests/stamp.ts +79 -0
- package/src/scaffold-tests/templates.ts +3578 -0
- package/src/skills/cli.ts +248 -0
- package/src/skills/compat.ts +166 -0
- package/src/skills/data.ts +192 -0
- package/src/skills/md.d.ts +7 -0
- package/src/skills/menu.ts +55 -0
- package/src/skills/spec.ts +227 -0
- package/src/suiteScripts.ts +110 -0
- package/src/tasks/a11yNative.ts +281 -0
- package/src/tasks/a11yScan.ts +67 -0
- package/src/tasks/biomeRunner.ts +301 -0
- package/src/tasks/biomeSweep.ts +166 -0
- package/src/tasks/e2eExpo.ts +436 -0
- package/src/tasks/e2eRoutes.ts +756 -0
- package/src/tasks/eslintReactRules.ts +67 -0
- package/src/tasks/index.ts +24 -0
- package/src/tasks/loadExpo.ts +273 -0
- package/src/tasks/loadPipes.ts +750 -0
- package/src/tasks/perfHarness.ts +145 -0
- package/src/tasks/staticPerfAnalysis.ts +510 -0
- package/src/tasks/types.ts +1 -0
- package/src/tasks/unitAndBrowser.ts +462 -0
- package/src/types.ts +1419 -0
- package/src/version.ts +12 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { PropsShape, PropValue } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A curated, closed table of host-element props — deliberately NOT a model of
|
|
5
|
+
* the DOM. `ComponentProps<"div">` is the dominant props shape in shadcn/base-ui
|
|
6
|
+
* codebases, and without this every such component synthesises zero props and
|
|
7
|
+
* gets an assertion-free smoke test.
|
|
8
|
+
*
|
|
9
|
+
* Three rules are baked in, each because breaking them produces *false test
|
|
10
|
+
* failures* rather than missing coverage:
|
|
11
|
+
*
|
|
12
|
+
* 1. Never synthesise `value`/`checked` — a value without an onChange makes
|
|
13
|
+
* React warn about a controlled input and turns `userEvent.type` into a
|
|
14
|
+
* silent no-op. Always `defaultValue`.
|
|
15
|
+
* 2. Never synthesise `placeholder`/`name`/`required` — an empty-string default
|
|
16
|
+
* is useless to the scaffolder and `getByPlaceholderText("")` throws.
|
|
17
|
+
* 3. `type` is always a valid literal, never `""` — `<button type="">` is
|
|
18
|
+
* invalid HTML.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface HostElementProps {
|
|
22
|
+
shape: PropsShape;
|
|
23
|
+
variantOptions?: Record<string, PropValue[]>;
|
|
24
|
+
acceptsChildren: boolean;
|
|
25
|
+
/** Implicit ARIA role, only where unambiguous. */
|
|
26
|
+
role?: string;
|
|
27
|
+
/** Handler props the scaffolder stubs with vi.fn(). */
|
|
28
|
+
handlers: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Present on every HTML element — safe for any tag, known or not. */
|
|
32
|
+
const BASE: PropsShape = { className: "", id: "" };
|
|
33
|
+
|
|
34
|
+
export const VOID_ELEMENTS: ReadonlySet<string> = new Set([
|
|
35
|
+
"area",
|
|
36
|
+
"base",
|
|
37
|
+
"br",
|
|
38
|
+
"col",
|
|
39
|
+
"embed",
|
|
40
|
+
"hr",
|
|
41
|
+
"img",
|
|
42
|
+
"input",
|
|
43
|
+
"link",
|
|
44
|
+
"meta",
|
|
45
|
+
"param",
|
|
46
|
+
"source",
|
|
47
|
+
"track",
|
|
48
|
+
"wbr",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const CONTAINER_TAGS = [
|
|
52
|
+
"div",
|
|
53
|
+
"span",
|
|
54
|
+
"p",
|
|
55
|
+
"section",
|
|
56
|
+
"article",
|
|
57
|
+
"header",
|
|
58
|
+
"footer",
|
|
59
|
+
"main",
|
|
60
|
+
"aside",
|
|
61
|
+
"figure",
|
|
62
|
+
"blockquote",
|
|
63
|
+
"table",
|
|
64
|
+
"thead",
|
|
65
|
+
"tbody",
|
|
66
|
+
"tr",
|
|
67
|
+
"td",
|
|
68
|
+
"th",
|
|
69
|
+
"dl",
|
|
70
|
+
"dt",
|
|
71
|
+
"dd",
|
|
72
|
+
"pre",
|
|
73
|
+
"code",
|
|
74
|
+
"em",
|
|
75
|
+
"strong",
|
|
76
|
+
"small",
|
|
77
|
+
"fieldset",
|
|
78
|
+
"legend",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function container(role?: string): HostElementProps {
|
|
82
|
+
return { shape: { ...BASE }, acceptsChildren: true, handlers: [], ...(role ? { role } : {}) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const TABLE: Record<string, HostElementProps> = {};
|
|
86
|
+
for (const tag of CONTAINER_TAGS) TABLE[tag] = container();
|
|
87
|
+
|
|
88
|
+
TABLE.nav = container("navigation");
|
|
89
|
+
TABLE.ul = container("list");
|
|
90
|
+
TABLE.ol = container("list");
|
|
91
|
+
TABLE.li = container("listitem");
|
|
92
|
+
for (const h of ["h1", "h2", "h3", "h4", "h5", "h6"]) TABLE[h] = container("heading");
|
|
93
|
+
|
|
94
|
+
TABLE.label = {
|
|
95
|
+
shape: { ...BASE, htmlFor: "" },
|
|
96
|
+
acceptsChildren: true,
|
|
97
|
+
handlers: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
TABLE.button = {
|
|
101
|
+
shape: { ...BASE, type: "button", disabled: false },
|
|
102
|
+
variantOptions: { type: ["button", "submit", "reset"] },
|
|
103
|
+
acceptsChildren: true,
|
|
104
|
+
role: "button",
|
|
105
|
+
handlers: ["onClick"],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
TABLE.a = {
|
|
109
|
+
shape: { ...BASE, href: "/" },
|
|
110
|
+
acceptsChildren: true,
|
|
111
|
+
role: "link",
|
|
112
|
+
handlers: ["onClick"],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
TABLE.input = {
|
|
116
|
+
shape: { ...BASE, type: "text", defaultValue: "", disabled: false },
|
|
117
|
+
variantOptions: { type: ["text", "email", "password", "number"] },
|
|
118
|
+
acceptsChildren: false,
|
|
119
|
+
role: "textbox",
|
|
120
|
+
handlers: ["onChange"],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
TABLE.textarea = {
|
|
124
|
+
shape: { ...BASE, defaultValue: "", disabled: false, rows: 3 },
|
|
125
|
+
acceptsChildren: false,
|
|
126
|
+
role: "textbox",
|
|
127
|
+
handlers: ["onChange"],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
TABLE.select = {
|
|
131
|
+
shape: { ...BASE, defaultValue: "", disabled: false },
|
|
132
|
+
acceptsChildren: true,
|
|
133
|
+
role: "combobox",
|
|
134
|
+
handlers: ["onChange"],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
TABLE.form = {
|
|
138
|
+
shape: { ...BASE },
|
|
139
|
+
acceptsChildren: true,
|
|
140
|
+
role: "form",
|
|
141
|
+
handlers: ["onSubmit"],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
TABLE.img = {
|
|
145
|
+
shape: { ...BASE, src: "/flawless.png", alt: "placeholder" },
|
|
146
|
+
acceptsChildren: false,
|
|
147
|
+
role: "img",
|
|
148
|
+
handlers: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const tag of ["svg", "path", "circle", "rect", "g"]) {
|
|
152
|
+
TABLE[tag] = {
|
|
153
|
+
shape: { ...BASE },
|
|
154
|
+
acceptsChildren: !VOID_ELEMENTS.has(tag),
|
|
155
|
+
handlers: [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const HOST_ELEMENT_PROPS: Readonly<Record<string, HostElementProps>> = TABLE;
|
|
160
|
+
|
|
161
|
+
/** Tag → implicit ARIA role, shared with the selector emitters so they cannot drift. */
|
|
162
|
+
export const TAG_TO_ROLE: Readonly<Record<string, string>> = Object.fromEntries(
|
|
163
|
+
Object.entries(TABLE)
|
|
164
|
+
.filter(([, v]) => v.role !== undefined)
|
|
165
|
+
.map(([k, v]) => [k, v.role as string]),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
export function isHostTagRecognised(tag: string): boolean {
|
|
169
|
+
return tag in TABLE;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Never undefined. An unknown tag still gets `className`/`id` (present on every
|
|
174
|
+
* HTML element) and a children flag from the closed void-element list — both
|
|
175
|
+
* are facts about HTML, not guesses about this component.
|
|
176
|
+
*/
|
|
177
|
+
export function hostPropsFor(tag: string): HostElementProps {
|
|
178
|
+
const known = TABLE[tag];
|
|
179
|
+
if (known) return { ...known, shape: { ...known.shape } };
|
|
180
|
+
return {
|
|
181
|
+
shape: { ...BASE },
|
|
182
|
+
acceptsChildren: !VOID_ELEMENTS.has(tag),
|
|
183
|
+
handlers: [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { detectHostDialect, type HostDialect } from "./nativeDialect.ts";
|
|
2
|
+
import { nativeElementFor } from "./nativeProps.ts";
|
|
3
|
+
import type { ImportBinding } from "./parseFile.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether a JSX tag names a host element, and which one.
|
|
7
|
+
*
|
|
8
|
+
* ONE predicate, replacing the `/^[a-z][a-z0-9-]*$/` test that was written out
|
|
9
|
+
* five times — extractRootTag, extractRenderMode, extractFlows, extractChildAttrs
|
|
10
|
+
* and selectors.ts. Five copies of a rule is the defect class this project keeps
|
|
11
|
+
* finding in its own docs, and it was about to become ten.
|
|
12
|
+
*
|
|
13
|
+
* The rule is EVIDENCE-BASED in both directions:
|
|
14
|
+
*
|
|
15
|
+
* - lowercase -> a DOM intrinsic. Byte-identical to the old test.
|
|
16
|
+
* - Capitalised -> a host element IF AND ONLY IF this file imports that
|
|
17
|
+
* name from a specifier in the closed native table.
|
|
18
|
+
* - anything else -> undefined, meaning "this is a component" — exactly
|
|
19
|
+
* the state every uppercase tag is in today.
|
|
20
|
+
*
|
|
21
|
+
* Never a guess from the name. `<IconButton>` is not a button, `<TicketRow>` is
|
|
22
|
+
* not a row, and shadcn's `<ButtonPrimitive>` must keep answering undefined on
|
|
23
|
+
* the web path or the DOM regression this predicate was extracted from returns.
|
|
24
|
+
*/
|
|
25
|
+
export interface ResolvedHostTag {
|
|
26
|
+
/**
|
|
27
|
+
* The canonical element name: the MODULE-SIDE export, never the local alias.
|
|
28
|
+
*
|
|
29
|
+
* `import { View as Box }` gives tag "Box" and name "View". This is the same
|
|
30
|
+
* refusal extractRootTag already makes for `<UI.Badge/>` — "Badge" and
|
|
31
|
+
* "UI.Badge" are both lies about the emitted element — applied in the other
|
|
32
|
+
* direction, and it is what makes rootTag comparable across files.
|
|
33
|
+
*/
|
|
34
|
+
name: string;
|
|
35
|
+
dialect: HostDialect;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type HostTagResolver = (tag: string) => ResolvedHostTag | undefined;
|
|
39
|
+
|
|
40
|
+
const LOWERCASE_INTRINSIC = /^[a-z][a-z0-9-]*$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Today's rule, exactly: lowercase intrinsics only.
|
|
44
|
+
*
|
|
45
|
+
* The default everywhere, so every existing call site behaves byte-identically
|
|
46
|
+
* until it is handed a native resolver on purpose.
|
|
47
|
+
*/
|
|
48
|
+
export const domOnlyResolver: HostTagResolver = (tag) =>
|
|
49
|
+
LOWERCASE_INTRINSIC.test(tag) ? { name: tag, dialect: "dom" } : undefined;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A resolver for one file, built from that file's own imports.
|
|
53
|
+
*
|
|
54
|
+
* Returns `domOnlyResolver` unchanged for a DOM file — not a wrapper around it,
|
|
55
|
+
* the same function object — so the web path cannot pick up so much as an extra
|
|
56
|
+
* closure.
|
|
57
|
+
*/
|
|
58
|
+
export function makeHostTagResolver(imports: Map<string, ImportBinding>): HostTagResolver {
|
|
59
|
+
if (detectHostDialect(imports) === "dom") return domOnlyResolver;
|
|
60
|
+
return (tag) => {
|
|
61
|
+
// A lowercase tag in a React Native file is still a DOM intrinsic as far as
|
|
62
|
+
// this predicate is concerned. It cannot render on a device, but saying so
|
|
63
|
+
// is extractRenderMode's job, not this one's.
|
|
64
|
+
if (LOWERCASE_INTRINSIC.test(tag)) return { name: tag, dialect: "dom" };
|
|
65
|
+
if (!/^[A-Z]/.test(tag)) return undefined;
|
|
66
|
+
const binding = imports.get(tag);
|
|
67
|
+
if (!binding || binding.isTypeOnly || binding.kind === "namespace") return undefined;
|
|
68
|
+
const exported = binding.importedName ?? tag;
|
|
69
|
+
if (nativeElementFor(binding.specifier, exported) === undefined) return undefined;
|
|
70
|
+
return { name: exported, dialect: "native" };
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts about HTML's content model that a generated harness has to respect.
|
|
3
|
+
*
|
|
4
|
+
* This is not inference about the analysed app — it is the specification. A
|
|
5
|
+
* `<tbody>` is only valid inside a `<table>`, so mounting `TableBody` straight
|
|
6
|
+
* into the host's `#root` div made React 19 log
|
|
7
|
+
*
|
|
8
|
+
* In HTML, <tbody> cannot be a child of <div>. This will cause a hydration error.
|
|
9
|
+
*
|
|
10
|
+
* on seven of the dogfood app's table primitives. The spec then failed them for
|
|
11
|
+
* a warning the harness itself provoked. Wrapping the mount in the required
|
|
12
|
+
* ancestors keeps those components covered instead of excluding them, and it is
|
|
13
|
+
* a lookup rather than a guess.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Ancestors a tag must be nested in, outermost first. */
|
|
17
|
+
const REQUIRED_ANCESTORS: Record<string, string[]> = {
|
|
18
|
+
thead: ["table"],
|
|
19
|
+
tbody: ["table"],
|
|
20
|
+
tfoot: ["table"],
|
|
21
|
+
caption: ["table"],
|
|
22
|
+
colgroup: ["table"],
|
|
23
|
+
col: ["table", "colgroup"],
|
|
24
|
+
tr: ["table", "tbody"],
|
|
25
|
+
td: ["table", "tbody", "tr"],
|
|
26
|
+
th: ["table", "tbody", "tr"],
|
|
27
|
+
option: ["select"],
|
|
28
|
+
optgroup: ["select"],
|
|
29
|
+
li: ["ul"],
|
|
30
|
+
dt: ["dl"],
|
|
31
|
+
dd: ["dl"],
|
|
32
|
+
figcaption: ["figure"],
|
|
33
|
+
legend: ["fieldset"],
|
|
34
|
+
source: ["video"],
|
|
35
|
+
track: ["video"],
|
|
36
|
+
area: ["map"],
|
|
37
|
+
param: ["object"],
|
|
38
|
+
summary: ["details"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The chain a component's root tag must be mounted inside, or [] when it can
|
|
43
|
+
* stand alone. Unknown tags (every component name, every custom element) return
|
|
44
|
+
* [] — the harness only wraps where HTML demands it.
|
|
45
|
+
*/
|
|
46
|
+
export function requiredAncestors(rootTag: string | undefined): string[] {
|
|
47
|
+
if (!rootTag) return [];
|
|
48
|
+
return REQUIRED_ANCESTORS[rootTag] ?? [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Elements a user can type into. Anything else rejects .fill()/.type() outright. */
|
|
52
|
+
const FILLABLE_TAGS = new Set(["input", "textarea", "select"]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether a change/input event target is something a test can type into.
|
|
56
|
+
*
|
|
57
|
+
* React's onChange bubbles, so it is idiomatic to put it on the `<form>` and
|
|
58
|
+
* read `event.target`. Driving that literally emitted
|
|
59
|
+
* `page.locator("form").fill("x")`, which Playwright rejects with "Element is
|
|
60
|
+
* not an <input>, <textarea>, <select> or [contenteditable]" — the generator
|
|
61
|
+
* describing event delegation as if it were a text box.
|
|
62
|
+
*/
|
|
63
|
+
export function isFillableTag(tag: string): boolean {
|
|
64
|
+
return FILLABLE_TAGS.has(tag);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Input types that hold no user-suppliable text (or none a blind fill can supply). */
|
|
68
|
+
const UNFILLABLE_INPUT_TYPES = new Set([
|
|
69
|
+
"hidden",
|
|
70
|
+
"checkbox",
|
|
71
|
+
"radio",
|
|
72
|
+
"file",
|
|
73
|
+
"image",
|
|
74
|
+
"submit",
|
|
75
|
+
"reset",
|
|
76
|
+
"button",
|
|
77
|
+
"range",
|
|
78
|
+
"color",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export type FillKind = "text" | "toggle" | "select" | "skip";
|
|
82
|
+
|
|
83
|
+
/** How (or whether) a form field can be driven, from its tag and type alone. */
|
|
84
|
+
export function fillKind(tag: string, type: string | undefined): FillKind {
|
|
85
|
+
if (tag === "select") return "select";
|
|
86
|
+
if (tag === "textarea") return "text";
|
|
87
|
+
if (tag !== "input") return "skip";
|
|
88
|
+
if (!type) return "text";
|
|
89
|
+
if (type === "checkbox" || type === "radio") return "toggle";
|
|
90
|
+
// A hidden input carries a value the component chose; typing into it asserts
|
|
91
|
+
// nothing and, when several share a name, cannot even be addressed uniquely.
|
|
92
|
+
if (UNFILLABLE_INPUT_TYPES.has(type)) return "skip";
|
|
93
|
+
return "text";
|
|
94
|
+
}
|