@lariocpt/flawless-react 0.0.0-stage → 0.3.1
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 +611 -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,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-a11y
|
|
3
|
+
description: Run and act on flawless-react's a11y task — biome's a11y rule group over a web tree, or the tool's own React Native analyser (six `a11y/*` rules with a denominator) under `--expo`, both writing one aggregate `reports/a11y.json`. Use when triaging accessibility findings, when the native run reports zero and you need to know whether it looked, when biome is not found, when a `biome-ignore` comment is flagged as unused, or when deciding what an icon-only control needs. For the suppression rules across tasks see flawless-findings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flawless a11y
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bunx @lariocpt/flawless-react "src/**/*.tsx" --tasks a11y
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
One task, two engines, one aggregate report (`flawless/reports/a11y.json`). Which
|
|
13
|
+
engine runs is decided by `--expo`, and the two are not interchangeable: biome's
|
|
14
|
+
rules are HTML/ARIA, the native rules are `accessibilityLabel`/`accessibilityRole`.
|
|
15
|
+
|
|
16
|
+
## Web: a biome sweep
|
|
17
|
+
|
|
18
|
+
The task runs biome with a **scratch config** — `recommended: false` plus only the
|
|
19
|
+
`a11y` group — so your project's `biome.json` is bypassed and a finding here is not
|
|
20
|
+
an invitation to reconfigure your lint. Findings print as
|
|
21
|
+
`file:line:col lint/a11y/<rule> message`.
|
|
22
|
+
|
|
23
|
+
Severities are mixed and mean what they say: `useAltText`, `useButtonType`,
|
|
24
|
+
`useHtmlLang`, `useAnchorContent`, `useHeadingContent`, `noRedundantRoles` are
|
|
25
|
+
errors; `noAutofocus`, `noLabelWithoutControl`, `noNoninteractiveTabindex`,
|
|
26
|
+
`noPositiveTabindex`, `noRedundantAlt`, `useFocusableInteractive` are warnings.
|
|
27
|
+
Errors are defects; warnings on a component that composes its label elsewhere (a
|
|
28
|
+
generic `Field` wrapper) are usually correct code.
|
|
29
|
+
|
|
30
|
+
**biome is a prerequisite on this path only.** The resolver looks in the target
|
|
31
|
+
package's `node_modules`, every ancestor (so a hoisted workspace root counts) and
|
|
32
|
+
`PATH`; when it finds nothing the task fails naming all of them. From the compiled
|
|
33
|
+
binary the message is explicit: install `@biomejs/biome` in the project you are
|
|
34
|
+
scanning, or put biome on `PATH`.
|
|
35
|
+
|
|
36
|
+
**And it has to be biome 2.** The sweep writes a biome 2 linter config and parses
|
|
37
|
+
its JSON reporter, so a candidate whose `--version` names another major is walked
|
|
38
|
+
past in favour of one that can read it — which is how a repo that hoists a 1.x
|
|
39
|
+
still gets a sweep from a 2 on `PATH` — and if none can, the task fails naming the
|
|
40
|
+
version and path it refused. A 1.x biome does answer, and what it prints is a
|
|
41
|
+
different document (`location.path` is an object, `message` an array of markup
|
|
42
|
+
runs), so its output cannot be read as findings. A source or npm install answers
|
|
43
|
+
with its own pinned 2 first and never sees the repo's biome: only the compiled
|
|
44
|
+
binary resolves into the scanned project.
|
|
45
|
+
|
|
46
|
+
## Native: the analyser, and its denominator
|
|
47
|
+
|
|
48
|
+
Biome's a11y rules are all HTML/ARIA. Over 31 React Native files carrying 82
|
|
49
|
+
unlabelled `<Pressable>`s they returned **zero**, while the same config finds 3/3 on
|
|
50
|
+
this repo's own DOM offender fixture — so the native path is the tool's own engine,
|
|
51
|
+
six rules:
|
|
52
|
+
|
|
53
|
+
| rule | severity | what it catches |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `a11y/unnamed-control` | error | a pressable whose children are provably textless with no `accessibilityLabel` |
|
|
56
|
+
| `a11y/missing-role` | error | a pressable with no `accessibilityRole` — RN has no implicit roles |
|
|
57
|
+
| `a11y/textinput-unlabelled` | error | a `TextInput` with no label; a `placeholder` alone does not count |
|
|
58
|
+
| `a11y/icon-unhidden` | warn | an icon from a known vector family exposed to the a11y tree with nothing to announce |
|
|
59
|
+
| `a11y/label-text-mismatch` | error | an `accessibilityLabel` that does not contain the visible text |
|
|
60
|
+
| `a11y/invalid-role` | error | an `accessibilityRole` value RN does not know |
|
|
61
|
+
|
|
62
|
+
Two things to read off every native run:
|
|
63
|
+
|
|
64
|
+
- **The summary always carries a denominator:** `N press target(s), N text input(s),
|
|
65
|
+
N icon(s) examined, 6 native rules`. **A native zero without that denominator is a
|
|
66
|
+
bug, not a clean bill.**
|
|
67
|
+
- **Files in DOM dialect are not swept.** A component whose host dialect is not
|
|
68
|
+
native (a `react-native-web` surface, a web-only screen inside an Expo tree) is
|
|
69
|
+
counted, not judged: the summary appends `N dom-dialect file(s) NOT swept — run
|
|
70
|
+
without --expo for those`, and the report carries them under
|
|
71
|
+
`domDialectFilesNotSwept`.
|
|
72
|
+
|
|
73
|
+
The engine is restrained on purpose, and that restraint is why its errors are
|
|
74
|
+
trustworthy: `unnamed-control` fires only on the **provably** nameless case —
|
|
75
|
+
children that are absent or all textless. A pressable rendering `{building.name}`
|
|
76
|
+
announces at runtime, so label-absence there is not a finding. The same for
|
|
77
|
+
`dynamicLabel` / `dynamicRole`: a label built by code is not a finding.
|
|
78
|
+
|
|
79
|
+
One freebie worth knowing: RN's own `<Button>` is exempt from `missing-role`,
|
|
80
|
+
because it has exactly one implicit role and the analyser injects it.
|
|
81
|
+
|
|
82
|
+
## What neither engine sees
|
|
83
|
+
|
|
84
|
+
Static a11y is name-and-role analysis. It does not check contrast, focus order,
|
|
85
|
+
reading order, motion, or whether a `role` is semantically right for the widget; the
|
|
86
|
+
web engine cannot see a label supplied by a wrapper it does not analyse, and the
|
|
87
|
+
native engine cannot see what a screen reader assembles at run time. Zero findings
|
|
88
|
+
means "no provably silent control", not "accessible".
|
|
89
|
+
|
|
90
|
+
## Suppressing
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// biome-ignore lint/a11y/noLabelWithoutControl: generic wrapper — every call site associates the control
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Biome attaches the comment to the **next line's** diagnostic; for a multi-line JSX
|
|
97
|
+
element whose finding anchors on an attribute, put the comment inside the tag
|
|
98
|
+
directly above that attribute. Native findings have **no suppression mechanism** —
|
|
99
|
+
deliberately: each message names the one prop that retires it
|
|
100
|
+
(`accessibilityLabel`, `accessibilityRole`, `aria-hidden`), so either add it or the
|
|
101
|
+
finding is wrong and belongs in an issue.
|
|
102
|
+
|
|
103
|
+
The cross-config trap that makes a biome suppression report as `unused`, and the fix
|
|
104
|
+
for it, is in flawless-findings.
|
|
105
|
+
|
|
106
|
+
## Never
|
|
107
|
+
|
|
108
|
+
- Never read a native zero as a pass without its denominator.
|
|
109
|
+
- Never enable one of these rules in a project's biome config purely to silence the
|
|
110
|
+
project's `biome ci` about the suppression — enable it because the suppressed site
|
|
111
|
+
was the only finding, which is usually true, and say so in the commit.
|
|
112
|
+
- Never add `accessibilityRole` or `aria-*` to satisfy a sweep where the message's
|
|
113
|
+
actual ask (`accessibilityLabel`, `aria-hidden`) is the fix.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-api
|
|
3
|
+
description: Use flawless-react programmatically — runFlawless/runCore options and results, exit-code and error contracts, the analyse()/ComponentTree surface for custom reporting, the tasks registry, and the --ci JSON contract. Use when scripting suite generation (CI, sandboxes, agents), building custom reports on the analysis, or embedding the tool in another workflow.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flawless programmatic API
|
|
7
|
+
|
|
8
|
+
> **`expo` is a `CoreOptions` field beside `nextjs`**, and the two are mutually
|
|
9
|
+
> exclusive — `runCore` throws a `UsageError` for the pair, because it is a public API a
|
|
10
|
+
> programmatic caller reaches without `parseArgs`. `RunContext` carries
|
|
11
|
+
> `expo?: ExpoContext`; ask `platformOf(ctx)` rather than testing `ctx.expo` yourself, so
|
|
12
|
+
> widening the rule later happens in one place. An unported task returns a **failed
|
|
13
|
+
> `TaskResult`**, not a throw, so a refusal is readable from `results[]` and the `--ci`
|
|
14
|
+
> blob.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { runFlawless } from "flawless-react";
|
|
18
|
+
|
|
19
|
+
const { exitCode, outDir, results, trees } = await runFlawless({
|
|
20
|
+
entry: "src/components/Widget.tsx", // path or glob; optional in Next.js mode
|
|
21
|
+
nextjs: ".", // enables Next.js mode, "." = cwd
|
|
22
|
+
tasks: ["unit-browser", "perf-static"],
|
|
23
|
+
colocate: true,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`runFlawless` is the friendly wrapper (`entry`/`tasks`/`out`); `runCore` from
|
|
28
|
+
`core.ts` is the same engine with CLI-shaped names (`pattern`/`taskIds`/
|
|
29
|
+
`outDir`) plus `changed` and `heaps`. Everything else — `runTests`, `force`,
|
|
30
|
+
`report`, `log`, `quiet` — is on **both**.
|
|
31
|
+
|
|
32
|
+
Two output flags, and they answer different questions:
|
|
33
|
+
|
|
34
|
+
- **`log`** — may this run write to **stdout**? Off by default programmatically.
|
|
35
|
+
It gates the progress narration *and* the per-file findings listing. `--json`
|
|
36
|
+
turns it off because stdout must parse whole.
|
|
37
|
+
- **`quiet`** — should `runCore`'s own diagnostics on **stderr** be suppressed?
|
|
38
|
+
Off by default, so the warnings **do** speak, even when `log` is off. Pass
|
|
39
|
+
`quiet: true` only if you route them yourself.
|
|
40
|
+
|
|
41
|
+
One flag for both is how `--ci` became the mode that lost its warnings.
|
|
42
|
+
`quiet` is deliberately narrow: it silences `runCore`'s `warn()` lines, not
|
|
43
|
+
stderr as a whole — the suite runner's output is routed by a different flag,
|
|
44
|
+
and task-level `console.warn`s have no gate. Redirect fd 2 if you need silence.
|
|
45
|
+
|
|
46
|
+
## Contracts
|
|
47
|
+
|
|
48
|
+
- **Never calls `process.exit`.** Bad input throws `UsageError` (exported from
|
|
49
|
+
the package and from `types.ts`); everything else is a plain error.
|
|
50
|
+
- **CLI exit codes:** 0 clean · 1 a task failed, a `--run-tests`/`--changed`
|
|
51
|
+
suite run failed (suite outcomes fold into the exit code), or a non-task
|
|
52
|
+
failure such as a broken Next.js emit · 2 usage error. The code alone does not
|
|
53
|
+
say which — `results`, `testRun` and `errors` do.
|
|
54
|
+
- **`--ci`** = `--no-interactive --json`: stdout is exactly one JSON blob
|
|
55
|
+
`{ exitCode, outDir, results, keptScripts, testRun, errors? }` and nothing
|
|
56
|
+
else. Progress goes away entirely; parse the whole stdout, not a line of it.
|
|
57
|
+
**stderr still carries diagnostics** — skipped files, an unparseable spec,
|
|
58
|
+
script collisions, a failed Next.js emit. Capture it: those lines are the only
|
|
59
|
+
account of work the run quietly dropped.
|
|
60
|
+
- **Never read exit 0 as "the suites passed."** Read `testRun`. The invariant:
|
|
61
|
+
`mode: "none"` **if and only if** zero suites executed, so `green` never
|
|
62
|
+
appears on a phase that ran nothing.
|
|
63
|
+
- `{ mode: "generated" | "affected", green }` → suites really RAN; `green` is
|
|
64
|
+
what folded into `exitCode`;
|
|
65
|
+
- `{ mode: "none", reason }` → **nothing ran**, and `reason` says which of the
|
|
66
|
+
three causes it was: no selected task generates a suite `--run-tests`
|
|
67
|
+
executes (only `perf-harness`, `unit-browser` and `e2e` do — `load` emits
|
|
68
|
+
pipes you run with `bun run flawless:load`), nothing was affected, or the
|
|
69
|
+
selected tasks plan no run for the work that was;
|
|
70
|
+
- absent → the run never REACHED the test-run phase. That is NOT "no run was
|
|
71
|
+
asked for": a request blocked by a failed task is also absent, exits 1, and
|
|
72
|
+
is explained by `results`. Read absent as "look elsewhere" (FR-046).
|
|
73
|
+
- **`errors`** carries failures belonging to no task, so `results` cannot show
|
|
74
|
+
them — a Next.js emit that threw sets `exitCode: 1` with every task `ok: true`.
|
|
75
|
+
Omitted on a clean run, so branch on its presence.
|
|
76
|
+
- `results` is `TaskResult[]`: `{ id, ok, outputs, findings, summary, error?,
|
|
77
|
+
status? }` — `ok: false` with `error` set is a real task failure.
|
|
78
|
+
`status` ("generated" | "refreshed" | "kept", perf-harness only for now)
|
|
79
|
+
says where the run sat in the suite lifecycle: a re-run over an existing
|
|
80
|
+
suite is ok:true + "refreshed", and a `--changed` run keeps the harness
|
|
81
|
+
(ok:true, "kept", outputs empty) rather than regenerating it.
|
|
82
|
+
- `reproCommand` semantics: REPORT.md's repro line is rebuilt from options,
|
|
83
|
+
relative paths only, defaults omitted — two identical runs produce the same
|
|
84
|
+
line byte-for-byte, so run from the project root with relative inputs if you
|
|
85
|
+
care about a stable REPORT.md.
|
|
86
|
+
|
|
87
|
+
## Running from a checkout
|
|
88
|
+
|
|
89
|
+
`bun src/bin.ts <args>` and `bun src/cli.ts <args>` both work (cli.ts has an
|
|
90
|
+
`import.meta.main` guard), as does the compiled `bin/flawless-react`. In
|
|
91
|
+
sandboxed or scripted contexts still prefer the programmatic API with evidence
|
|
92
|
+
written to files — stdout may be captured, truncated, or lost, and the
|
|
93
|
+
flawless-regen skill's run-proof discipline applies.
|
|
94
|
+
|
|
95
|
+
Cwd matters in Next.js mode: `nextjs: "."` resolves against the process cwd,
|
|
96
|
+
and the run merges `flawless:*` scripts into THAT root's `package.json`.
|
|
97
|
+
`process.chdir(appRoot)` first, or pass absolute paths deliberately.
|
|
98
|
+
The merge never clobbers a user-customised `flawless:*` value: only the
|
|
99
|
+
tool's own recognised spellings upgrade; kept keys surface in
|
|
100
|
+
`CoreResult.keptScripts`, the `--ci` blob, and REPORT.md.
|
|
101
|
+
|
|
102
|
+
## The analysis surface
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { analyse } from "flawless-react";
|
|
106
|
+
|
|
107
|
+
const tree = await analyse({ entry: "src/Nav.tsx", depth: 3, includeChildren: true });
|
|
108
|
+
// tree: ComponentTree — entry: ComponentInfo (propsShape, variants, hooks,
|
|
109
|
+
// interactions, dataDeps, unresolvedProps/unresolvedTypes), children: ComponentTree[]
|
|
110
|
+
// A dataDep whose usage inference came up empty may carry inferredReturn
|
|
111
|
+
// (a fixture value from its Promise<T> annotation, provenance "annotation")
|
|
112
|
+
// or wrapsFetch (method+path for the emit-time OpenAPI join); a server-action
|
|
113
|
+
// moduleImport may carry serverActionReturns (module-side name -> literal).
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`analyse()` is pure static analysis — no files written — and is the right base
|
|
117
|
+
for custom reporting. The honesty contract worth preserving in anything you
|
|
118
|
+
build on it: an extraction that fails is RECORDED (`unresolvedProps`,
|
|
119
|
+
`unresolvedTypes`, `uncertain` on data deps), never silently empty; treat an
|
|
120
|
+
empty shape with empty unresolved records as a bug to report, not a component
|
|
121
|
+
with no props.
|
|
122
|
+
|
|
123
|
+
Tasks are a registry: `findTask(id).run(ctx)` with
|
|
124
|
+
`ctx: { trees, outDir, force, … }` returning a `TaskResult`. A custom task is
|
|
125
|
+
a new entry in `src/tasks/index.ts` honouring one rule — write findings and
|
|
126
|
+
outputs, throw only for genuine failure, and make `ok` mean what it says.
|
|
127
|
+
|
|
128
|
+
## The MCP server — the same contracts for a host
|
|
129
|
+
|
|
130
|
+
`flawless-react mcp` serves this tool over the Model Context Protocol on stdio:
|
|
131
|
+
eight tools, and **no new API surface to learn**. `flawless_run` answers with the
|
|
132
|
+
child's exit line plus the same `--ci` blob documented above, so every contract in
|
|
133
|
+
this file — `testRun` as the only proof suites ran, `errors` belonging to no task,
|
|
134
|
+
`absent` meaning "look elsewhere" (FR-046) — is how to read what an agent gets
|
|
135
|
+
back. It spawns a `flawless-react` child per run call (a suite runner inherits
|
|
136
|
+
stdout; in-process would write vitest's report into a JSON-RPC frame), so nothing
|
|
137
|
+
is held between calls. The six `flawless_*` read tools parse in-process and write
|
|
138
|
+
nothing; unknown arguments are rejected, not ignored; `flawless_findings` is the
|
|
139
|
+
static engine only, so zero findings is not "accessible and lint-clean". List the
|
|
140
|
+
tools from the binary rather than from memory:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
flawless-react mcp
|
|
144
|
+
flawless-react mcp --help
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The two planes, the arguments and the honest limits: `docs/mcp.md`.
|
|
148
|
+
|
|
149
|
+
## Never
|
|
150
|
+
|
|
151
|
+
- Never scrape the human console output — use `--ci` or the API's `results`.
|
|
152
|
+
- Never run Next.js mode from an arbitrary cwd without pinning it: the
|
|
153
|
+
package.json merge follows the resolved root, wherever that is.
|
|
154
|
+
- Never treat `exitCode: 1` as "the tool crashed". Two distinct causes: a
|
|
155
|
+
task failed (`results[].error` names it) or a `--run-tests`/`--changed`
|
|
156
|
+
suite run went red — in the suite case every `results[]` entry is `ok:true`
|
|
157
|
+
and the outcome lives in `exitCode` and `CoreResult.testRun` ({mode:
|
|
158
|
+
"affected"|"generated", green}), not in any error field. A re-run over an
|
|
159
|
+
existing suite is not a failure: it refreshes the harness and exits 0.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-api-fixtures
|
|
3
|
+
description: Give flawless-react real API data — `--api` with an OpenAPI/Swagger file, `--swagger-url` against a live docs endpoint (including the automatic `--nextjs` probe and `--swagger-url=off`), the MSW handlers and `swagger.json` it writes and which of them survive a re-run, how a body is synthesised when the spec has no example, and the four fetch cases every generated spec carries. Use when generated fixtures are empty `{}`, when a component's fetch is not matched by a handler, when a spec should assert loading or empty-state copy, or when deciding between mocking in specs and running a real backend. For the programmatic API (`runFlawless`, `analyse`) see flawless-api.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flawless API fixtures
|
|
7
|
+
|
|
8
|
+
Two different things live in this repo's vocabulary and mixing them up wastes an
|
|
9
|
+
hour: **MSW handlers** (`flawless/mocks/`), which intercept `fetch` inside the unit
|
|
10
|
+
and browser planes, and the **mock server** (`flawless/mock/`), a Bun server you can
|
|
11
|
+
point an app at. Both are derived from the same source: your OpenAPI spec, when the
|
|
12
|
+
tool can get one.
|
|
13
|
+
|
|
14
|
+
## Getting a spec
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bunx @lariocpt/flawless-react "src/app/dashboard/page.tsx" --nextjs . --api openapi.json --swagger-url=off
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **`--api <file>`** — a spec on disk. The value is required and must be a path.
|
|
21
|
+
- **`--swagger-url <url>`** — fetch it from a running app (the docs help names
|
|
22
|
+
`http://localhost:3000/api/docs` from `next-swagger-doc`). A spec obtained this way
|
|
23
|
+
is **preferred over static AST inference**.
|
|
24
|
+
- In `--nextjs` mode that URL is **probed automatically**, with a 1.5 s timeout, so a
|
|
25
|
+
docs-enabled app needs no flag at all. The probe never throws: a non-2xx, a
|
|
26
|
+
non-JSON content type, or a dead port is reported and the run continues on inferred
|
|
27
|
+
routes. `--swagger-url` forces a different URL; `--swagger-url=off` skips the probe
|
|
28
|
+
— do that when the app under test is slow to boot, or when a *different* app's docs
|
|
29
|
+
endpoint is listening on 3000.
|
|
30
|
+
- **With no spec at all**, the tool walks `app/**/api/**/route.ts` and derives routes
|
|
31
|
+
from it — useless for a catch-all API (Hono, tRPC), which is exactly the case
|
|
32
|
+
`--api` exists for.
|
|
33
|
+
|
|
34
|
+
A spec obtained by either route is written **verbatim** to `flawless/swagger.json`;
|
|
35
|
+
without one, that file is **derived from the inferred routes and rewritten every
|
|
36
|
+
run**.
|
|
37
|
+
|
|
38
|
+
## What is written, and what survives a re-run
|
|
39
|
+
|
|
40
|
+
| file | refresh behaviour |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `flawless/swagger.json` | rewritten every run |
|
|
43
|
+
| `flawless/mocks/handlers.ts` | **written once, never overwritten — not by `--force` either** |
|
|
44
|
+
| `mocks/browser.ts`, `mocks/node.ts`, `mocks/README.md` | written once, refreshed by `--force` |
|
|
45
|
+
| `flawless/mock/server.ts` | only with `--api`: rewritten each run |
|
|
46
|
+
| `flawless/mock/routes.ts` | only with `--api`: written once (hand-tuned bodies can't be rebuilt from the spec) |
|
|
47
|
+
|
|
48
|
+
The handlers file is the one you edit. Its header says so, and it is the reason a
|
|
49
|
+
spec with a `{}` body can be fixed once and stay fixed.
|
|
50
|
+
|
|
51
|
+
How the three MSW entry points wire up:
|
|
52
|
+
|
|
53
|
+
- **Vitest** — `setupServer` from `mocks/node.ts`, started in `flawless/setup.ts`, so
|
|
54
|
+
a component's `fetch` hits your mock responses in jsdom.
|
|
55
|
+
- **Browser specs and the perf host** — `host/main.tsx` calls `startMockWorker()`
|
|
56
|
+
from `mocks/browser.ts` before mounting; a service worker intercepts page `fetch`.
|
|
57
|
+
- **Server components, server actions, SSR** — not covered by either. When you run
|
|
58
|
+
against a real Next dev server, wire your own instrumentation to import
|
|
59
|
+
`mocks/node.ts`. The honest reason `e2e` runs against the real app: MSW cannot
|
|
60
|
+
intercept a server component's direct database access, and mocking that would be a
|
|
61
|
+
lie (flawless-e2e).
|
|
62
|
+
|
|
63
|
+
## Where a fixture body comes from
|
|
64
|
+
|
|
65
|
+
Per response media type, in order: `example`, then the first `examples[*].value`,
|
|
66
|
+
then a body **synthesised from the schema**. Synthesis is literal and worth knowing,
|
|
67
|
+
because it explains most empty-looking specs:
|
|
68
|
+
|
|
69
|
+
- `object` → every property, recursively; `array` → **exactly one item**;
|
|
70
|
+
- `string` → `""`, except `format: date-time` → `1970-01-01T00:00:00.000Z`;
|
|
71
|
+
- `integer`/`number` → `0`; `boolean` → `false`; `enum` → its **first** member;
|
|
72
|
+
- `allOf` → the branches merged into one object;
|
|
73
|
+
- `$ref` → resolved **only for local `#/…` refs**; a cycle or an external ref yields
|
|
74
|
+
`null` for that property.
|
|
75
|
+
- Response selection prefers a coded status; a `default` response is used last and
|
|
76
|
+
counted as 200.
|
|
77
|
+
|
|
78
|
+
So `{ id: 0, createdAt: "1970-01-01T00:00:00.000Z", items: [ … ] }` is not a bug — it
|
|
79
|
+
is a spec with no examples. The fix is `examples` in the spec, or hand-editing
|
|
80
|
+
`mocks/handlers.ts` (which nothing will overwrite), or both.
|
|
81
|
+
|
|
82
|
+
When a component's endpoint has no derivable body, the generated spec says so in
|
|
83
|
+
place:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
// TODO(flawless): no example found in the API spec — replace {} with a realistic fixture.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What every fetch generates
|
|
90
|
+
|
|
91
|
+
One `describe("<Component> GET /api/items")` per detected fetch, with four cases:
|
|
92
|
+
|
|
93
|
+
1. **success** — `server.use(http.get(URL, …))` returning `FIXTURE`, plus a call
|
|
94
|
+
counter so the spec can prove the request happened;
|
|
95
|
+
2. **loading** — the same handler behind `delay(150)`, ending in
|
|
96
|
+
`TODO(flawless): assert the loading indicator while the response is pending.`;
|
|
97
|
+
3. **error** — `new HttpResponse(null, { status: 500 })`, asserting the component
|
|
98
|
+
survives;
|
|
99
|
+
4. **empty** — `FIXTURE` with **the envelope preserved and every array member
|
|
100
|
+
emptied** (`{ items: [] }`, not `[]`), ending in `TODO(flawless): assert the
|
|
101
|
+
empty-state copy.`
|
|
102
|
+
|
|
103
|
+
The loading and empty assertions are the intended hardening work: the harness proves
|
|
104
|
+
the state is reachable, you say what the state looks like.
|
|
105
|
+
|
|
106
|
+
Path parameters are handled before any of that. A URL hole that corresponds to a
|
|
107
|
+
**prop** is filled with `test-<hole>` at render time, because the prop's synthesised
|
|
108
|
+
default is `""` and `/api/x//y` never matches a `:param` handler — the request would
|
|
109
|
+
silently miss every handler in the file.
|
|
110
|
+
|
|
111
|
+
Handler matching is **param-name-agnostic**: `/api/x/{id}/y`, `/api/x/:id/y` and
|
|
112
|
+
`/api/x/:buildingId/y` are the same shape. So a spec that asks for
|
|
113
|
+
`:buildingId` is satisfied by a route declared `{id}`.
|
|
114
|
+
|
|
115
|
+
## The mock server
|
|
116
|
+
|
|
117
|
+
With `--api` (a file, not a live probe — `--swagger-url` enriches routes and
|
|
118
|
+
`swagger.json` but scaffolds no server), `flawless/mock/` is scaffolded too: `bun
|
|
119
|
+
mock/server.ts` serves on `127.0.0.1:${MOCK_PORT:-5174}` (default 5174), compiling
|
|
120
|
+
OpenAPI templates (`/users/{id}`) into matchers, and answering anything it does not
|
|
121
|
+
know with
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "error": "not mocked", "method": "POST", "path": "/api/orders" }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
That 404 body is the diagnostic: a run that behaves oddly against the mock and
|
|
128
|
+
correctly against the real API is missing routes in `mock/routes.ts`, not broken
|
|
129
|
+
components. Two limits to keep in view: **`routes.ts` is never rewritten** (your
|
|
130
|
+
hand-tuned bodies are yours), and **the response body is static** — a path parameter
|
|
131
|
+
is captured but does not change the answer, so `/users/1` and `/users/2` return the
|
|
132
|
+
same document. Anything where the id must change the response belongs in a handler
|
|
133
|
+
you write, or in a real backend.
|
|
134
|
+
|
|
135
|
+
## Never
|
|
136
|
+
|
|
137
|
+
- Never read a `{}` fixture as "the tool generated nothing": it means the spec had no
|
|
138
|
+
example. Add `examples`, or fill the handler.
|
|
139
|
+
- Never rely on MSW in the unit plane to prove what a server component renders —
|
|
140
|
+
server-side data access is not interceptable there.
|
|
141
|
+
- Never let the loading and empty-state TODOs stand while claiming the fetch path is
|
|
142
|
+
covered: the states are reached, not asserted.
|
|
143
|
+
- Never treat the mock server as contract validation. For violation reporting, put
|
|
144
|
+
crust's validating proxy in front of the real app (flawless-e2e).
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-e2e
|
|
3
|
+
description: Run and maintain flawless-react's route e2e specs against a real dev server — seeded-DB prerequisites, the global-setup login and storageState, params.json ids and `landing` overrides, FLAWLESS_E2E_BASEURL, the auth wall that 404s or serves the login page at a real path, and the landing assertion that decides whether a green e2e run tested a route. Use right before running `flawless:e2e`, when e2e fails at global setup, when every route 404s, or when a suite passes suspiciously cheaply. For load pipes and their baselines see flawless-load.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flawless route e2e
|
|
7
|
+
|
|
8
|
+
Everything here runs against the **real app**: MSW cannot intercept a server
|
|
9
|
+
component's direct database access, so mocking it would be a lie. That realism is
|
|
10
|
+
also where every vacuous pass hides — a green route spec is only worth what its
|
|
11
|
+
landing assertion is worth.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bunx @lariocpt/flawless-react "src/app/**/page.tsx" --nextjs . --tasks e2e
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Prerequisites, in order
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun run db:up && bun run db:migrate && bun run db:seed # Postgres + seed users
|
|
21
|
+
bun run dev:web # app on :3001
|
|
22
|
+
bun run flawless:e2e
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
From the suite dir, with the plane's own config and its own copy of Playwright:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node node_modules/@playwright/test/cli.js test --config e2e/playwright.config.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Running it from the app root instead is how `test.describe() called here` /
|
|
32
|
+
`No tests found` appear: two `@playwright/test` copies (app root + suite) and the
|
|
33
|
+
run picks up the wrong one.
|
|
34
|
+
|
|
35
|
+
## Signing in
|
|
36
|
+
|
|
37
|
+
`flawless/e2e/global-setup.ts` is where you sign in as the seeded user and persist
|
|
38
|
+
`storageState` to `e2e/.auth.json`. **What the tool emits is a stub** that writes an
|
|
39
|
+
empty state (so the config's `storageState` always resolves) plus a worked example.
|
|
40
|
+
It is write-once — never overwritten, `--force` included, because it is yours the
|
|
41
|
+
moment you touch it.
|
|
42
|
+
|
|
43
|
+
A filled setup should **throw if login lands back on /login**. That throw is an
|
|
44
|
+
anti-vacuous guard, not a bug: when it fires the seed drifted (wrong user, wrong
|
|
45
|
+
password, unseeded DB). Fix the seed, never the guard.
|
|
46
|
+
|
|
47
|
+
You are not relying on remembering that, though. **Every emitted route spec asserts
|
|
48
|
+
it landed on the route it asked for** — both the render test and the interaction
|
|
49
|
+
test — so an unauthenticated suite fails naming its escapes instead of quietly
|
|
50
|
+
testing the login screen. Until the hook is filled, REPORT.md's hand-work table
|
|
51
|
+
says so; the check is byte-identity against the stub, so it clears the moment you
|
|
52
|
+
edit the file.
|
|
53
|
+
|
|
54
|
+
**What that assertion does and does not catch**, because an overclaim here is the
|
|
55
|
+
same bug wearing a different hat. It catches redirects that CHANGE THE URL and have
|
|
56
|
+
landed by the time the spec looks — a server 3xx, and a client-side navigation that
|
|
57
|
+
completes before `networkidle` settles. It does **not** catch a middleware *rewrite*
|
|
58
|
+
that serves the login page at the requested path, a 401 body, or a redirect that
|
|
59
|
+
fires later than the spec waited. A 401 usually still reds the spec, but through the
|
|
60
|
+
console-error check, not this one.
|
|
61
|
+
|
|
62
|
+
## The auth wall, in its three shapes
|
|
63
|
+
|
|
64
|
+
The same cause — the app's own middleware (`auth.protect()`, a Clerk/next-auth
|
|
65
|
+
guard) with placeholder or missing credentials — surfaces one stage earlier each
|
|
66
|
+
time you chase it:
|
|
67
|
+
|
|
68
|
+
1. **`Timed out waiting …ms from config.webServer`, no spec ran.** Playwright calls
|
|
69
|
+
a server up only on `200 ≤ status < 404` from the readiness URL, which is the app
|
|
70
|
+
root; behind the wall the root 404s and Playwright retries the whole timeout
|
|
71
|
+
(`[WebServer] GET / 404` repeating in the log).
|
|
72
|
+
2. **Every route 404s while `/` loads in a browser.** The guard is per-route and the
|
|
73
|
+
root is public.
|
|
74
|
+
3. **Specs pass on a login page.** A rewrite keeps the path and returns 200.
|
|
75
|
+
|
|
76
|
+
There is no tool-side fix for placeholder keys. Either put real credentials in
|
|
77
|
+
`e2e/global-setup.ts`, or set `FLAWLESS_E2E_BASEURL` to a server you started
|
|
78
|
+
yourself (the emitted config drops its own `webServer` block for that — useful when
|
|
79
|
+
the app is already running, or when you want the wall up and provably authenticated
|
|
80
|
+
past), or do not claim those routes.
|
|
81
|
+
|
|
82
|
+
## ids and landings: `flawless/e2e/params.json`
|
|
83
|
+
|
|
84
|
+
Write-once and merge-kept, so `--force` never touches what you fill in. An
|
|
85
|
+
unresolvable id never fabricates a URL: the spec emits a `test.skip` naming the file
|
|
86
|
+
to fill (`no id fixture for /posts/[id] — fill flawless/e2e/params.json`), so an
|
|
87
|
+
unfilled route is a visible skip, not a 404 measured as a pass. On the load side the
|
|
88
|
+
same hole is `routes.setup.ts`, which refuses the run instead of measuring the 404
|
|
89
|
+
handler — see flawless-load.
|
|
90
|
+
|
|
91
|
+
The resolver (`e2e/params.ts`) tries four things, in order:
|
|
92
|
+
|
|
93
|
+
1. `FLAWLESS_E2E_<PARAM>` — per-parameter environment override, uppercased;
|
|
94
|
+
2. `routes[<route>][<param>]` in params.json — for a route that needs a
|
|
95
|
+
**consistent set** (a building and one of its floors);
|
|
96
|
+
3. `defaults[<param>]` — the usual case, one seeded record;
|
|
97
|
+
4. `FLAWLESS_E2E_ID` — the legacy single-id escape hatch.
|
|
98
|
+
|
|
99
|
+
`landing` records a route that redirects **on purpose** (a default sub-tab, a locale
|
|
100
|
+
landing) so its spec asserts the real destination instead of failing forever:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "landing": { "/dash": "/dash/overview" } }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Both belong in `params.json`, not in the spec: `--force` regenerates specs and
|
|
107
|
+
leaves this file alone.
|
|
108
|
+
|
|
109
|
+
## Contract-checked runs (optional)
|
|
110
|
+
|
|
111
|
+
Put crust's validating proxy in front and assert no response violated the OpenAPI
|
|
112
|
+
spec:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
crust -c 'mock-server --swagger openapi.json --port 4747 --proxy http://localhost:3001' &
|
|
116
|
+
FLAWLESS_E2E_BASEURL=http://localhost:4747 bun run flawless:e2e
|
|
117
|
+
crust -c 'GET :4747/__crust/violations | (v => v.violations.filter(x => x.direction === "response")) | assert (a => a.length === 0)'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The filter to `direction === "response"` matters: request-direction violations are
|
|
121
|
+
usually the app talking to itself with fields the spec never described, and they
|
|
122
|
+
will drown the ones that mean the app's own output is off-contract.
|
|
123
|
+
|
|
124
|
+
## Under `--expo`
|
|
125
|
+
|
|
126
|
+
`e2e` RUNS under `--expo` since wave 6 — expo-router routes against
|
|
127
|
+
`expo start --web` (Metro's webPort, default 8081), authenticated against the API
|
|
128
|
+
origin with a **cookie** via the setup stub's request-context login + storageState
|
|
129
|
+
(the app short-circuits `expo-secure-store` on web and sends `credentials:
|
|
130
|
+
"include"`). Three differences to know:
|
|
131
|
+
|
|
132
|
+
- Set `EXPO_PUBLIC_API_URL` before running. Metro bakes it into the bundle; unset,
|
|
133
|
+
the app falls back to whatever its source hardcodes — on one dogfooded LAN, a
|
|
134
|
+
production container.
|
|
135
|
+
- Metro's statuses are REAL (unknown routes 404, per-route HTML — measured in
|
|
136
|
+
`docs/archive/2026-09-expo-probes.md`), so a 404 is a routing bug, not harness
|
|
137
|
+
noise.
|
|
138
|
+
- Each spec carries a third test, the **copy witness** (`getByText` on the screen's
|
|
139
|
+
longest analysis-proven literal), because an auth wall rendered in place keeps the
|
|
140
|
+
URL and a clean-ish console. The witness is the claim that cannot be true of a
|
|
141
|
+
wall.
|
|
142
|
+
|
|
143
|
+
A cold start that always lands on the login screen is a `landing` override in
|
|
144
|
+
params.json, not a spec edit. See flawless-expo for the rest of the native plane.
|
|
145
|
+
|
|
146
|
+
## Never
|
|
147
|
+
|
|
148
|
+
- Never weaken or delete global-setup's throw-on-/login guard.
|
|
149
|
+
- Never "fix" a params.json syntax error by deleting the file: an invalid file fails
|
|
150
|
+
the run with `is not valid JSON — fix or delete it and re-run` precisely because a
|
|
151
|
+
hand-edited file's contents are yours; repair the JSON.
|
|
152
|
+
- Never read "N routes pass" as meaning anything until the spec's landing assertion
|
|
153
|
+
was real (a filled `params.json`, a storageState that is not the empty stub).
|
|
154
|
+
- Never add a `landing` override to make a red spec green without checking what it
|
|
155
|
+
lands on: that is how a wall becomes permanent coverage of the login page.
|