@terreno/agent-plugin 0.0.0-stage → 57.21.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/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +2124 -0
- package/LICENSE +21 -0
- package/README.md +57 -2
- package/agents/pre-commit.md +65 -0
- package/agents/ui-verifier.md +93 -0
- package/package.json +32 -4
- package/references/async-review-bots.md +76 -0
- package/references/documentation-contract.md +61 -0
- package/references/execution-state.schema.json +77 -0
- package/references/github-attention-contract.md +165 -0
- package/references/independent-review.md +31 -0
- package/references/lifecycle-contract.md +211 -0
- package/references/loop-engineering.md +125 -0
- package/references/pick-roast-loop.md +112 -0
- package/references/pr-deployments.md +69 -0
- package/references/product-ci.md +164 -0
- package/references/push-ownership.md +91 -0
- package/references/stage-result.schema.json +82 -0
- package/references/subagent-briefing.md +43 -0
- package/references/testing-plan.md +163 -0
- package/references/user-guide.md +171 -0
- package/skills/1-grow/SKILL.md +145 -0
- package/skills/1-grow/references/grilling.md +243 -0
- package/skills/2-pick/SKILL.md +170 -0
- package/skills/2-pick/references/mocking.md +34 -0
- package/skills/2-pick/references/testing.md +43 -0
- package/skills/3-roast/SKILL.md +168 -0
- package/skills/4-brew/SKILL.md +201 -0
- package/skills/5-taste/SKILL.md +311 -0
- package/skills/ai-prompt-governance/SKILL.md +100 -0
- package/skills/attach-pr-media/SKILL.md +89 -0
- package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
- package/skills/backend-test-env/SKILL.md +64 -0
- package/skills/backfill-regression/SKILL.md +101 -0
- package/skills/backfill-user-guide/SKILL.md +96 -0
- package/skills/bootstrap-mcp-server/SKILL.md +244 -0
- package/skills/building-admin-interfaces/SKILL.md +83 -0
- package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
- package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
- package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
- package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
- package/skills/building-terreno-apps/SKILL.md +280 -0
- package/skills/building-terreno-apps/agents/openai.yaml +4 -0
- package/skills/building-terreno-apps/references/navigation.md +102 -0
- package/skills/building-terreno-apps/references/route-structure.md +91 -0
- package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
- package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
- package/skills/deploy-gcp/SKILL.md +73 -0
- package/skills/deploy-gcp/references/troubleshooting.md +29 -0
- package/skills/generate-release-announcements/SKILL.md +145 -0
- package/skills/generate-sdk/SKILL.md +156 -0
- package/skills/mongoose-schema-safety/SKILL.md +164 -0
- package/skills/pick-roast-loop/SKILL.md +152 -0
- package/skills/planning-loop/SKILL.md +161 -0
- package/skills/prd/SKILL.md +138 -0
- package/skills/set-up-social-login/SKILL.md +76 -0
- package/skills/store-assets/SKILL.md +221 -0
- package/skills/store-assets/examples/screenshots.yaml +31 -0
- package/skills/store-assets/examples/storeAssets.config.json +44 -0
- package/skills/store-assets/scripts/banner.ts +165 -0
- package/skills/store-assets/scripts/config.ts +255 -0
- package/skills/store-assets/scripts/devClient.ts +635 -0
- package/skills/store-assets/scripts/devices.ts +286 -0
- package/skills/store-assets/scripts/icon.ts +81 -0
- package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
- package/skills/store-assets/scripts/maestro/login.yaml +27 -0
- package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
- package/skills/store-assets/scripts/screenshots.ts +848 -0
- package/skills/taste-sweep/SKILL.md +172 -0
- package/skills/terreno-backend-api/SKILL.md +355 -0
- package/skills/terreno-backend-api/agents/openai.yaml +4 -0
- package/skills/terreno-backend-api/references/custom-routes.md +132 -0
- package/skills/terreno-backend-api/references/model-router.md +116 -0
- package/skills/terreno-backend-api/references/server-setup.md +116 -0
- package/skills/terreno-backend-tests/SKILL.md +176 -0
- package/skills/terreno-data-fetching/SKILL.md +276 -0
- package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
- package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
- package/skills/terreno-data-fetching/references/realtime.md +45 -0
- package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
- package/skills/terreno-frontend-tests/SKILL.md +190 -0
- package/skills/terreno-mobile-tests/SKILL.md +127 -0
- package/skills/terreno-test-setup/SKILL.md +81 -0
- package/skills/terreno-test-setup/references/backend.md +131 -0
- package/skills/terreno-test-setup/references/frontend.md +169 -0
- package/skills/terreno-test-setup/references/mobile.md +134 -0
- package/skills/terreno-ui/SKILL.md +252 -0
- package/skills/terreno-ui/agents/openai.yaml +4 -0
- package/skills/terreno-ui/references/data-display.md +85 -0
- package/skills/terreno-ui/references/feedback.md +122 -0
- package/skills/terreno-ui/references/forms.md +119 -0
- package/skills/terreno-ui/references/layout.md +124 -0
- package/skills/terreno-ui/references/theming.md +87 -0
- package/skills/update-docs/SKILL.md +82 -0
- package/skills/upgrading-terreno/SKILL.md +86 -0
- package/skills/upgrading-terreno/references/ordering.md +40 -0
- package/skills/verify-ui-changes/SKILL.md +605 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Upgrade ordering
|
|
2
|
+
|
|
3
|
+
Why the `upgrading-terreno` skill bumps packages in a fixed order. Packages below are every published job in `.github/workflows/publish-on-tag.yml`.
|
|
4
|
+
|
|
5
|
+
## Package classes
|
|
6
|
+
|
|
7
|
+
| Class | Packages (`publish-on-tag.yml` job) |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Backend | `api`, `test`, `admin-backend`, `blocks`, `ai`, `api-health`, `comms`, `feature-flags`, `mcp` |
|
|
10
|
+
| Frontend | `ui`, `rtk`, `syncdb`, `admin-frontend`, `admin-spa` |
|
|
11
|
+
|
|
12
|
+
`blocks` holds contracts that both `ai` and `ui` depend on, so it moves with the backend step.
|
|
13
|
+
|
|
14
|
+
`demo` and the example apps are not published.
|
|
15
|
+
|
|
16
|
+
## Backend before frontend
|
|
17
|
+
|
|
18
|
+
The OpenAPI document from the running backend is the contract `bun run sdk` consumes. Regenerating the client against an **un-upgraded** backend produces hooks for the old surface: new routes and fields are missing, and consumer screens fail at **compile** (`Property X does not exist on type …` or a missing generated hook).
|
|
19
|
+
|
|
20
|
+
Bump and compile backend packages first, then start that backend, then regenerate.
|
|
21
|
+
|
|
22
|
+
## Typed client only after the new backend is up
|
|
23
|
+
|
|
24
|
+
`example-frontend` codegen reads `http://localhost:4000/openapi.json` (or `OPENAPI_URL`). If the process is still the old binary, the file `store/openApiSdk.ts` is silently stale. Symptom: runtime 404 on new paths or TypeScript errors after you edit screens to call new endpoints that the SDK never emitted.
|
|
25
|
+
|
|
26
|
+
## Expo before `@terreno/ui`
|
|
27
|
+
|
|
28
|
+
`ui/package.json` `peerDependencies` pin `react-native` to the Expo line (for example `~0.86.0` on Terreno 57). Installing `@terreno/ui@57` while the app is still on Expo 54 / RN 0.81 fails peer resolution or breaks at Metro/native build. Symptom: `ERESOLVE` / unmet peer, or a native module ABI mismatch after `expo start`.
|
|
29
|
+
|
|
30
|
+
Run `upgrading-expo` (skill name) before bumping Terreno frontend packages when the Terreno major tracks a new Expo SDK.
|
|
31
|
+
|
|
32
|
+
## Violation symptoms
|
|
33
|
+
|
|
34
|
+
| If you… | What breaks |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Bump `@terreno/ui` before Expo | Unmet `react-native` peers; native rebuild required anyway |
|
|
37
|
+
| Run `bun run sdk` before upgrading/starting the backend | Generated client missing new operations; compile errors in screens |
|
|
38
|
+
| Bump frontend `@terreno/*` before backend compile | Client types and server handlers disagree; 4xx/5xx on new fields |
|
|
39
|
+
| Mix `@terreno/api@57` with `@terreno/ui@0.31` | Unsupported; lockstep publish tags one version for every package |
|
|
40
|
+
| Continue after a failed `compile` | Half-upgraded lockfile; rollback is unclear |
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-ui-changes
|
|
3
|
+
description: >-
|
|
4
|
+
Mandatory when any feature touches the frontend. Bring up a stack you own, log in,
|
|
5
|
+
drive the change under the required checks (right page, not blank, no error overlay,
|
|
6
|
+
console clean, DOM assertion, interaction proof, screenshot), run the repository's
|
|
7
|
+
Maestro flows on the simulator for mobile-focused work (direct simulator commands when
|
|
8
|
+
the app has no Maestro), and post screenshots and
|
|
9
|
+
video to the PR or a secret gist in the report format. Lifecycle composition: Pick
|
|
10
|
+
for implementation runtime checks, Roast for independent proof, and Taste after a
|
|
11
|
+
UI-affecting review/CI fix.
|
|
12
|
+
claudecode:
|
|
13
|
+
model: haiku
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Verify UI Changes
|
|
17
|
+
|
|
18
|
+
Use this skill automatically whenever a task changes UI behavior, UI layout, visual styling, component stories, navigation screens, user-visible copy, or any full-stack feature with a frontend surface.
|
|
19
|
+
|
|
20
|
+
If you made a user-facing change, you must run it, check that it actually works, and
|
|
21
|
+
show evidence. No evidence, no completion claim. A passing build, type check, or unit
|
|
22
|
+
test is necessary, not sufficient. **User-facing** is anything a user could notice:
|
|
23
|
+
components, screens, routes, navigation, styles, layout, responsive behavior, theming,
|
|
24
|
+
forms, validation, error / empty / loading states, copy, translations, API changes whose
|
|
25
|
+
output a client renders, emails, push, SMS, exported files. Pure refactors, internal
|
|
26
|
+
tooling, and backend-only changes with no rendered consumer are exempt, but say so:
|
|
27
|
+
"No user-facing change: <reason>".
|
|
28
|
+
|
|
29
|
+
## Trigger Files
|
|
30
|
+
|
|
31
|
+
Load this skill before validating changes to:
|
|
32
|
+
|
|
33
|
+
- `*.tsx`
|
|
34
|
+
- `*.jsx`
|
|
35
|
+
- `*.html`
|
|
36
|
+
- `*.css`
|
|
37
|
+
- `*.scss`
|
|
38
|
+
- `*.less`
|
|
39
|
+
- `*.styl`
|
|
40
|
+
- `*.vue`
|
|
41
|
+
- Story/demo files that render UI states
|
|
42
|
+
- Theme, layout, navigation, or component configuration that changes rendered UI
|
|
43
|
+
- Full-stack PRs that include any of the above, even when backend files dominate the diff
|
|
44
|
+
|
|
45
|
+
## The loop (every frontend-touching feature)
|
|
46
|
+
|
|
47
|
+
Do not open or update a PR for frontend work until this loop is complete.
|
|
48
|
+
|
|
49
|
+
1. **State the flow in one sentence** before testing:
|
|
50
|
+
`<entry point> -> <user action or state> -> <expected visible result>`. For a general
|
|
51
|
+
smoke: app loads -> first meaningful screen renders -> primary controls respond
|
|
52
|
+
without errors.
|
|
53
|
+
2. **Get the app running on a stack you own** (package-specific sections below). If the
|
|
54
|
+
backend or an app port is already taken by something you did not start, **stop and
|
|
55
|
+
say which ports are taken**. Do not test through them and do not kill them: on a
|
|
56
|
+
developer machine they are usually the simulators' Metros and a backend pointed at
|
|
57
|
+
staging, and the checks could change staging data. When the repository ships a
|
|
58
|
+
`verify-ui` stack script, use it; it refuses foreign ports for you.
|
|
59
|
+
3. **Write adversarial instructions, not happy-path ones.** Bad: "check that the form
|
|
60
|
+
renders". Good: "submit empty, with an invalid email, with a wrong password, with
|
|
61
|
+
valid credentials; assert each error message, the redirect, and the console". Cover
|
|
62
|
+
the error, empty, disabled, and loading states the change touches. Drive the UI
|
|
63
|
+
through `testID`s; if an element has none, add one to the component first
|
|
64
|
+
(`{screen}-{element}-{qualifier}`), never a class or structural selector. As Roast's
|
|
65
|
+
independent verifier, do not add it: report the missing `testID` (see "Who edits").
|
|
66
|
+
4. **Log in and exercise the changed feature** with the seeded accounts documented
|
|
67
|
+
below: navigate to the affected screen and run the flow end to end, not just confirm
|
|
68
|
+
the app or page loads. Visual or layout work gets a desktop and a phone viewport.
|
|
69
|
+
5. **Run the required checks** on every app × viewport run:
|
|
70
|
+
|
|
71
|
+
| Check | Passing looks like |
|
|
72
|
+
|---|---|
|
|
73
|
+
| Right page | Final pathname is the expected one and is not the login page |
|
|
74
|
+
| Not blank | ≥ 40 characters of visible text, not an empty shell or a stuck spinner |
|
|
75
|
+
| No error overlay | No framework red box or compile-error text in the document |
|
|
76
|
+
| Console clean | Zero console or page errors; warnings are listed for you to explain |
|
|
77
|
+
| DOM assertion | ≥ 1 assertion on a visible element, text, or URL, all passed |
|
|
78
|
+
| Interaction proof | ≥ 1 tap / type / key press, followed by an assertion of the new state |
|
|
79
|
+
| Screenshot | ≥ 1 screenshot; take one of the failure state when a step fails |
|
|
80
|
+
|
|
81
|
+
For visual or layout work also look at the phone run for clipping, overlap,
|
|
82
|
+
unreadable text, bad wrapping, layout shift, missing assets, and z-index problems.
|
|
83
|
+
6. **Look at every screenshot.** Check the changed state actually rendered: not a
|
|
84
|
+
spinner, not an empty state caused by seed data, not the login page. DOM assertions
|
|
85
|
+
are primary; screenshots support visual claims, they do not replace them. A console
|
|
86
|
+
error in the changed code path is a failure even when the shot looks right: explain
|
|
87
|
+
it or fix it (see "Baselining console warnings" for the demo app).
|
|
88
|
+
7. **Mobile-focused change? Verify it on the simulator** (section below): the matching
|
|
89
|
+
Maestro flow when the app has Maestro configured, direct simulator commands when it
|
|
90
|
+
does not. Web alone is not enough.
|
|
91
|
+
8. **If anything fails: fix it and re-verify immediately.** Do not stop to report a
|
|
92
|
+
half-working state. Retry a failing tool or UI action once before calling it a
|
|
93
|
+
blocker. Repeat until zero failures. As Roast's independent verifier, do not fix:
|
|
94
|
+
report FAIL with the evidence (see "Who edits").
|
|
95
|
+
9. **Publish and report** in the report format below: to the PR `Verification` table
|
|
96
|
+
(see "GitHub Reviewer Evidence"), or to a secret gist when the repository's
|
|
97
|
+
`verify-ui` publish script exists or there is no PR yet. Give the user the link plus
|
|
98
|
+
the Result line.
|
|
99
|
+
|
|
100
|
+
## Simulator leg (mobile-focused changes)
|
|
101
|
+
|
|
102
|
+
Required when the change is native screens, gestures, navigation stacks, keyboard
|
|
103
|
+
behavior, safe areas, push, camera / permissions, auth storage, `*.ios.tsx`,
|
|
104
|
+
`*.android.tsx`, `*.native.tsx`, `Platform.OS` branches, or the ticket says "mobile" or
|
|
105
|
+
"app".
|
|
106
|
+
|
|
107
|
+
**Maestro configured** (the app has a `<app>/maestro/` directory with flows): verify with
|
|
108
|
+
those **Maestro** flows on the app's dedicated simulator, not with an ad-hoc tap script.
|
|
109
|
+
Run the flow that matches the change. No matching flow? Add one next to the existing
|
|
110
|
+
flows, reusing their launch and login subflows, with `testID` selectors and
|
|
111
|
+
`extendedWaitUntil` instead of sleeps; it is part of the change and stays in the
|
|
112
|
+
repository. Maestro configured but the CLI or Java missing: install them with the
|
|
113
|
+
repository's documented command, or report UNVERIFIED.
|
|
114
|
+
|
|
115
|
+
**Maestro not configured** (no `<app>/maestro/`): do not set Maestro up as part of the
|
|
116
|
+
change. Drive the simulator with direct commands instead, from a temporary script kept
|
|
117
|
+
outside the tree:
|
|
118
|
+
|
|
119
|
+
| Step | iOS simulator | Android emulator |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| Boot and install | `xcrun simctl boot <device>`, `xcrun simctl install booted <app>` | `emulator -avd <avd>`, `adb install <apk>` |
|
|
122
|
+
| Open the screen | `xcrun simctl openurl booted <scheme>://<path>` | `adb shell am start -a android.intent.action.VIEW -d <scheme>://<path>` |
|
|
123
|
+
| Tap and type | `idb ui tap <x> <y>`, `idb ui text "<text>"` | `adb shell input tap <x> <y>`, `adb shell input text "<text>"` |
|
|
124
|
+
| Assert | `idb ui describe-all` (find the `testID` and expected text) | `adb exec-out uiautomator dump /dev/tty` (find the `resource-id` and expected text) |
|
|
125
|
+
| Evidence | `xcrun simctl io booted screenshot <file>`, `xcrun simctl io booted recordVideo <file>` | `adb exec-out screencap -p > <file>`, `adb shell screenrecord <file>` |
|
|
126
|
+
|
|
127
|
+
Take tap coordinates from the accessibility tree, not from guesses. Assert on the tree
|
|
128
|
+
after each interaction; a screenshot alone is not an assertion. The same required checks
|
|
129
|
+
and outcomes apply. Without `idb`, iOS can only open screens and capture evidence, so an
|
|
130
|
+
iOS flow that needs taps is UNVERIFIED (`idb` missing) unless you install it.
|
|
131
|
+
|
|
132
|
+
A missing or stale dev client
|
|
133
|
+
(`Cannot find native module`) is rebuilt with the repository's documented command, then
|
|
134
|
+
the flow reruns. The flow deep-links the dev client to Metro, so Metro and the backend
|
|
135
|
+
must be the stack you started: "started by you" means the process you recorded is still
|
|
136
|
+
alive and holds the port, not that a pid file exists. A stack that crashed and was
|
|
137
|
+
followed by a developer's Metro or a backend on staging is not yours. If the simulator
|
|
138
|
+
or dev client is unavailable, the report says **UNVERIFIED (simulator)**; a passing web
|
|
139
|
+
check never stands in for the mobile one. Keep the three outcomes apart: a flow that ran
|
|
140
|
+
and failed is FAIL, a flow that never ran (stack not yours, no simulator or dev client,
|
|
141
|
+
simulator would not boot, Maestro, Java, or `idb` missing, no report written) is UNVERIFIED
|
|
142
|
+
with the blocker, and neither is ever reported as the other. A repository runner should
|
|
143
|
+
encode that in its exit codes (for example 2 for failed, 3 for UNVERIFIED). Also check
|
|
144
|
+
what web cannot: the keyboard does not cover inputs, safe areas, and the tablet vs
|
|
145
|
+
phone layout fork.
|
|
146
|
+
|
|
147
|
+
## Who edits
|
|
148
|
+
|
|
149
|
+
Only the implementer (Pick, or a Taste fix) changes the tree: adds `testID`s, adds
|
|
150
|
+
Maestro flows, and fixes failures. Roast's independent verifier (the `ui-verifier`
|
|
151
|
+
agent) runs the same loop report-only. A missing `testID` or Maestro flow, or a failing
|
|
152
|
+
check, is FAIL with the evidence; a check it could not run is UNVERIFIED with the
|
|
153
|
+
blocker. It never edits the tree to make a run pass.
|
|
154
|
+
|
|
155
|
+
## Evidence rules
|
|
156
|
+
|
|
157
|
+
- Seed or QA data only. Never screenshot, log, or paste real user data or PHI into
|
|
158
|
+
evidence. Secret gists are unlisted, not private. If the only way to verify is against
|
|
159
|
+
real data, stop and ask.
|
|
160
|
+
- Store screenshots, recordings, and temporary test scripts outside the committed source
|
|
161
|
+
tree (`/opt/cursor/artifacts/` in Cursor Cloud, your scratchpad otherwise) unless the
|
|
162
|
+
project specifies an evidence directory or the user asks for committed artifacts.
|
|
163
|
+
Playwright scenarios and Maestro flows that document a feature may stay in the
|
|
164
|
+
repository.
|
|
165
|
+
- If credentials are missing, ask for them. Never guess.
|
|
166
|
+
|
|
167
|
+
## When you can't verify
|
|
168
|
+
|
|
169
|
+
No browser tooling, app won't start, missing credentials, ports taken by a stack that is
|
|
170
|
+
not yours, needs a device: do not claim it works. Mark it **UNVERIFIED** with the exact
|
|
171
|
+
blocker and the commands or setup steps attempted, say what you did verify (build,
|
|
172
|
+
types, unit tests), give the reviewer the exact steps to verify themselves, and ask for
|
|
173
|
+
missing credentials rather than guessing. In the PR, that is one `⚠️ Not tested` row
|
|
174
|
+
naming the blocked behavior and the reviewer action, with decisive error detail in an
|
|
175
|
+
optional `<details>` block. Do not present compile-only or app-start-only checks as
|
|
176
|
+
complete verification.
|
|
177
|
+
|
|
178
|
+
## Package-Specific Manual Verification
|
|
179
|
+
|
|
180
|
+
### `@terreno/ui` component changes
|
|
181
|
+
|
|
182
|
+
Test UI package component changes only in the demo app.
|
|
183
|
+
|
|
184
|
+
1. Start the demo web app from the repo root:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
bun run demo:start
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Or from `demo/`:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
bun run web
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The demo runs on `http://localhost:8085`.
|
|
197
|
+
|
|
198
|
+
2. Open the developer-mode screen for the changed component:
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
http://localhost:8085/dev
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
3. Select the component and the most relevant story.
|
|
205
|
+
- Direct URLs use the component name from `demo/demoConfig.tsx` and a story query param.
|
|
206
|
+
- **The `?story=` value must exactly match a key of the `stories` object in
|
|
207
|
+
`demo/story-config/<Component>.config.tsx`, URL-encoded.** These keys are human-readable
|
|
208
|
+
display names, not slugs — e.g. `http://localhost:8085/dev/Button?story=Variants`, and they can
|
|
209
|
+
contain spaces, which must be URL-encoded (`?story=Inline%20Feedback%20Prompt`).
|
|
210
|
+
`demo/app/dev/[component].tsx` calls `router.replace("/dev")` only when `story` is absent or the
|
|
211
|
+
component isn't in `demoConfig.tsx`; a *valid component with an unknown story* renders a **blank
|
|
212
|
+
page** instead. Either way you get no error, so confirm the story actually rendered.
|
|
213
|
+
- Use `/dev/<Component>?story=<Story>` to see one raw story full-screen.
|
|
214
|
+
- Use `/demo/<Component>` when you need the **interactive prop controls**
|
|
215
|
+
(`demoOptions.controls` — selects, booleans). Those controls render *only* on the `/demo`
|
|
216
|
+
page, not on `/dev`. The `/demo` page also renders every story at the bottom, so it is the
|
|
217
|
+
fastest way to see all states plus drive props from one screen.
|
|
218
|
+
|
|
219
|
+
4. Verify the changed state and at least one adjacent state that could regress.
|
|
220
|
+
- For visual primitives: compare default, disabled, loading, icon, and full-width states when relevant.
|
|
221
|
+
- For interactive components: click, type, open, close, hover, and keyboard navigate as appropriate.
|
|
222
|
+
|
|
223
|
+
5. When shipping a **new** `@terreno/ui` component or changing public props, also verify the generated docs page:
|
|
224
|
+
- Run `cd ui && bun run types && bun run website:generate`
|
|
225
|
+
- Check the docs deploy preview (or `bun run website:build` locally) for the component page and embedded demo iframe.
|
|
226
|
+
|
|
227
|
+
When the change is `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, `DashboardGrid`, or
|
|
228
|
+
chart layout/paint helpers:
|
|
229
|
+
|
|
230
|
+
1. Run `bun run ui:charts:compare` (starts demo web on port 8085 unless it is already up).
|
|
231
|
+
2. If any fixture fails, follow the `review-chart-visuals` skill: open
|
|
232
|
+
`demo/rendered-snapshots/<id>.png`, `demo/chart-visual-output/actual/<id>.png`, and
|
|
233
|
+
`demo/chart-visual-output/diff/<id>.png` and describe the visual delta. Do not treat
|
|
234
|
+
React test-renderer JSON snapshots as proof of chart paint.
|
|
235
|
+
Operator steps: [Compare rendered chart snapshots](../../../../docs/how-to/compare-chart-rendered-snapshots.md).
|
|
236
|
+
|
|
237
|
+
Do not launch the example app to validate isolated `@terreno/ui` component changes unless the change also affects an app-level integration.
|
|
238
|
+
|
|
239
|
+
#### Driving the `/demo` prop controls
|
|
240
|
+
|
|
241
|
+
The controls are `@terreno/ui` `Field`s, **not native HTML inputs**:
|
|
242
|
+
|
|
243
|
+
- A `type: "select"` control renders a custom dropdown — there is **no `<select>` element**, so
|
|
244
|
+
Playwright/CDP `selectOption`-style helpers do nothing. Click the field to open the option list,
|
|
245
|
+
then click the option row. After opening, the field's `<input>` swaps its value for a
|
|
246
|
+
`placeholder="Search..."`, which is a reliable signal that the list is open.
|
|
247
|
+
- A `type: "boolean"` control renders a toggle `<div>`; click it directly.
|
|
248
|
+
|
|
249
|
+
#### Objectively verifying icon fill, color, and icon family
|
|
250
|
+
|
|
251
|
+
Screenshots alone make solid-vs-outline and two similar greys hard to judge. Read computed styles
|
|
252
|
+
instead — both the Terreno `Icon` and `@expo/vector-icons` render **font glyphs**, so:
|
|
253
|
+
|
|
254
|
+
- **Which icon set is actually rendering** → computed `font-family` on the glyph element. This is
|
|
255
|
+
the definitive check when a component migrates icon libraries: Terreno's `Icon` falls back to
|
|
256
|
+
FontAwesome6 (`FontAwesome6Free-Solid` / `FontAwesome6Free-Regular`), whereas a direct
|
|
257
|
+
`import Feather from "@expo/vector-icons/Feather"` reports `feather` and
|
|
258
|
+
`import MaterialIcons from "@expo/vector-icons/MaterialIcons"` reports `material` (from
|
|
259
|
+
`createIconSet(glyphMap, 'Material Icons', ...)` — the computed value is normalised, so don't
|
|
260
|
+
expect the literal string `MaterialIcons`). Don't try to judge this from a screenshot — the
|
|
261
|
+
glyphs look similar at small sizes. This whole procedure only applies to **font-backed** icons: a
|
|
262
|
+
custom icon registered through `TerrenoProvider`'s icon registry takes precedence over the
|
|
263
|
+
FontAwesome glyph set (`ui/src/Icon.tsx`) and may render SVG or any component, so probe its own
|
|
264
|
+
markup rather than `font-family`.
|
|
265
|
+
- **Fill / `type` prop** (FontAwesome `Icon` only) → computed `font-family`:
|
|
266
|
+
`FontAwesome6Free-Solid` for `type="solid"` vs `FontAwesome6Free-Regular` for `type="regular"`.
|
|
267
|
+
Note that most `@expo/vector-icons` sets (Feather included) have **no solid variant**, so a
|
|
268
|
+
component using them must signal selection some other way — usually a filled surface behind
|
|
269
|
+
the glyph. When that is the design, verify the `backgroundColor` and `borderRadius` of the
|
|
270
|
+
wrapping `Pressable` rather than the glyph's fill (see "Verifying a filled-surface selection
|
|
271
|
+
cue" below).
|
|
272
|
+
- **Fill via a different icon *name*** (the strongest check, and the one to reach for with sets
|
|
273
|
+
that ship paired outline/filled glyphs such as MaterialIcons' `thumb-up-off-alt` vs
|
|
274
|
+
`thumb-up-alt`) → read the glyph element's **`textContent` codepoint**. `@expo/vector-icons`
|
|
275
|
+
renders the icon as a single character from the icon font, so `el.textContent.codePointAt(0)`
|
|
276
|
+
identifies exactly which icon name is rendering. Resolve the expected numbers from the shipped
|
|
277
|
+
glyphmap so you are asserting against ground truth rather than a guess:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# glyphmaps live under node_modules/**/@expo/vector-icons/build/vendor/.../glyphmaps/<Set>.json
|
|
281
|
+
node -e 'const m=require("<path>/MaterialIcons.json");
|
|
282
|
+
for (const n of ["thumb-up-off-alt","thumb-up-alt"]) console.log(n, m[n], "U+"+m[n].toString(16));'
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This matters because a broken name mapping still recolours correctly, so a colour-only check
|
|
286
|
+
passes on a component whose fill never changes. Colour is not proof of fill; the codepoint is.
|
|
287
|
+
- **The icon font actually loaded** → a failed font load renders tofu boxes while the codepoint
|
|
288
|
+
check still passes, so also assert `document.fonts.check('20px <family>')` and confirm the family
|
|
289
|
+
appears with status `loaded` in `document.fonts`. To rule out fallback rendering, measure the
|
|
290
|
+
glyph's advance width in the icon family versus a generic fallback family and check that they
|
|
291
|
+
*differ* — icon glyphs are not guaranteed to be square, so treat a specific width (e.g. exactly
|
|
292
|
+
20px) as a smell rather than a pass/fail rule, and prefer font-loading or network evidence. After a
|
|
293
|
+
migration, assert the **probed glyph's** computed family is the new one; do not require the old
|
|
294
|
+
family to be absent from `document.fonts`, since another element or an earlier mount in this SPA
|
|
295
|
+
session can keep it registered.
|
|
296
|
+
- **Size** → computed `font-size` in px. `iconSizeToNumber` in `ui/src/Common.ts` maps
|
|
297
|
+
`xs/sm/md/lg/xl/2xl` → `8/12/16/20/24/28`.
|
|
298
|
+
- **Color** → computed `color`, compared against `theme.text[...]` in `ui/src/Theme.tsx`.
|
|
299
|
+
Watch out: the `text` color map and the surface/other maps use the **same token names for
|
|
300
|
+
different primitives**. In the light theme `text.secondaryDark` is `secondary800` (`#092E3A`),
|
|
301
|
+
**not** `secondary500` — always trace the map that the component actually reads
|
|
302
|
+
(`Icon` uses `theme.text[color]`).
|
|
303
|
+
|
|
304
|
+
Read these from the icon element (the `Pressable`'s first element child), e.g. a one-shot probe
|
|
305
|
+
returning `{fontFamily, fontSize, color, disabled}` per button, and snapshot it before *and* after
|
|
306
|
+
each interaction. For a disabled no-op, assert the after-state is identical to the before-state,
|
|
307
|
+
then re-enable and repeat the same click to prove the hit target was live all along.
|
|
308
|
+
|
|
309
|
+
Measure geometry with the page at its default scale. Browser page zoom does *not* skew these
|
|
310
|
+
comparisons — `getBoundingClientRect()` and computed `font-size` are both CSS pixels — but a CDP
|
|
311
|
+
`Emulation.setPageScaleFactor`/`setDeviceMetricsOverride` scale factor or a pinch-zoomed
|
|
312
|
+
`visualViewport.scale` does change the mapping to device pixels. Record `window.devicePixelRatio`
|
|
313
|
+
and `visualViewport.scale` alongside the numbers you report so a mismatch is attributable.
|
|
314
|
+
|
|
315
|
+
#### Verifying a filled-surface selection cue (and its contrast)
|
|
316
|
+
|
|
317
|
+
When selection is shown by a background circle/pill instead of a glyph change, probe the
|
|
318
|
+
**`Pressable`** (not the glyph) for `backgroundColor` and `borderRadius`, and assert:
|
|
319
|
+
|
|
320
|
+
- selected → a concrete `rgb(...)` matching the intended `theme.surface[...]` token;
|
|
321
|
+
- unselected → `rgba(0, 0, 0, 0)` (transparent);
|
|
322
|
+
- `borderRadius` is exactly half the tap target at every size, otherwise it is a rounded square
|
|
323
|
+
rather than a circle.
|
|
324
|
+
|
|
325
|
+
Also **compute contrast ratios from the rendered colors**, since a surface-only cue can be too
|
|
326
|
+
subtle where a solid-vs-outline cue never was. Walk up the DOM to find the first ancestor with a
|
|
327
|
+
non-transparent `backgroundColor` to get the real page background (the demo page surface is
|
|
328
|
+
`#F2F2F2`, not white), then apply the WCAG relative-luminance formula. Check both
|
|
329
|
+
glyph-on-surface (aim ≥4.5:1) and **surface-vs-page-background**, which WCAG 1.4.11 wants at ≥3:1
|
|
330
|
+
for a non-text state indicator — pale tokens like `secondary100` (`#B6CDD5`) land around 1.48:1 on
|
|
331
|
+
`#F2F2F2` and are worth flagging to design as a non-blocking observation. Muted-icon-on-`surface.disabled`
|
|
332
|
+
combinations tend to be ~2:1 grey-on-grey; that is cosmetic (disabled controls are exempt from
|
|
333
|
+
WCAG contrast) but still worth a screenshot and a note.
|
|
334
|
+
|
|
335
|
+
#### Verifying pixel geometry against a Figma spec
|
|
336
|
+
|
|
337
|
+
When a PR claims a component now matches a Figma frame, verify the numbers at **browser zoom 1**
|
|
338
|
+
and report what you actually measured rather than rounding to "matches":
|
|
339
|
+
|
|
340
|
+
- Glyph box → `getBoundingClientRect()` on the icon element; also cross-check computed `font-size`.
|
|
341
|
+
- Distance between two icons → measure **both** the tap-target centers and the glyph centers. If
|
|
342
|
+
they disagree, the glyph is not centered in its pressable.
|
|
343
|
+
- Don't take a `gap` prop on trust: measure `next.left - prev.right` in the DOM. Terreno's spacing
|
|
344
|
+
scale means `gap={2}` is 8px, so `24px target + 8px gap = 32px` between centers — verify that
|
|
345
|
+
arithmetic instead of assuming it.
|
|
346
|
+
- Figma often specifies the gap between *icon boxes* while the DOM gap is between *tap targets*.
|
|
347
|
+
With a 16px glyph in a 24px target these differ by 8px (16 vs 8) — both can be "correct" at once,
|
|
348
|
+
so state which one you measured.
|
|
349
|
+
|
|
350
|
+
#### Painted ink vs the em box
|
|
351
|
+
|
|
352
|
+
`getBoundingClientRect()` on a glyph element returns the **em box** (equal to `font-size`), which is
|
|
353
|
+
not what a designer means by "the icon is 16px" — icon fonts pad their artwork inside the em box, so
|
|
354
|
+
a 20px MaterialIcons thumb paints about 16.5 x 14.9 px. When the spec is about the *visible* icon,
|
|
355
|
+
measure the ink:
|
|
356
|
+
|
|
357
|
+
1. **Screenshot pixel analysis (preferred).** Set `document.body.style.zoom = 8`, screenshot, compute
|
|
358
|
+
the bounding box of non-background pixels, and divide by the zoom factor — validating that factor
|
|
359
|
+
independently first (a known 32px centre distance should measure 256px).
|
|
360
|
+
2. **Canvas `TextMetrics`** — `measureText(glyph)` with `actualBoundingBoxLeft/Right/Ascent/Descent`
|
|
361
|
+
in the normalized family at the same px size. Faster, but it rounds outward from the rasterized
|
|
362
|
+
ink and can read 1-2px large. Use a **synchronous** IIFE; an async one returns an unserialized
|
|
363
|
+
Promise (`{}`).
|
|
364
|
+
|
|
365
|
+
Report width and height separately. Icon glyphs are rarely square, so "make the ink exactly NxN" is
|
|
366
|
+
usually impossible with one font-size — give the font-size that satisfies each dimension and let the
|
|
367
|
+
author pick which one the Figma box is keyed to.
|
|
368
|
+
|
|
369
|
+
#### Flush (zero-gap) tap targets
|
|
370
|
+
|
|
371
|
+
When adjacent targets have no gap, assert `positive.right === negative.left` and a non-overlap check
|
|
372
|
+
rather than only the centre distance, and hit-test a few px either side of the shared edge with
|
|
373
|
+
`document.elementFromPoint` to prove neither target steals the other's clicks.
|
|
374
|
+
|
|
375
|
+
#### Prop tables come from generated TypeDoc, not the source
|
|
376
|
+
|
|
377
|
+
The demo Props table is rendered from `demo/ui-types-documentation.json` (mirrored into
|
|
378
|
+
`mcp-server/{src,dist}/docs/`). Those files are **git-ignored build output**, so a PR that removes or
|
|
379
|
+
renames a public prop in `ui/src/Common.ts` will still show the dead prop in a demo app started from
|
|
380
|
+
a stale copy. Regenerate with `bun run generate-types` in `demo/` before reading the Props table, and
|
|
381
|
+
report drift as a local staleness issue, not a missing commit.
|
|
382
|
+
|
|
383
|
+
Note `demoOptions.size` in `demo/story-config/*.config.tsx` is the demo *panel* sizing key used by
|
|
384
|
+
many configs — unrelated to a component's own `size` prop, and not stale metadata.
|
|
385
|
+
|
|
386
|
+
#### Traps when clicking small tap targets
|
|
387
|
+
|
|
388
|
+
Two things will silently produce bogus click results:
|
|
389
|
+
|
|
390
|
+
1. **Screenshot coordinates are not CSS coordinates.** The browser screenshot may be 1024px wide
|
|
391
|
+
while `window.innerWidth` is 1600 (scale 0.64). If your click tool takes *screenshot-image*
|
|
392
|
+
coordinates, multiply CSS coordinates by `screenshotWidth / innerWidth` first; Playwright/CDP
|
|
393
|
+
`mouse.click` takes **viewport CSS pixels**, so pass `getBoundingClientRect()` values straight
|
|
394
|
+
through and do NOT rescale. Know which space your tool uses — guessing wrong makes clicks land on
|
|
395
|
+
nothing and look like a dead hit target.
|
|
396
|
+
2. **A fully-rounded pressable has a circular hit area.** With `borderRadius: tapTarget / 2` the
|
|
397
|
+
bounding-box corners are not clickable — sweeping `document.elementFromPoint` over the box shows
|
|
398
|
+
only ~84% of it hits the button. Always click the **center**, and if you need to characterise the
|
|
399
|
+
real target, do the `elementFromPoint` pixel sweep rather than guessing.
|
|
400
|
+
|
|
401
|
+
Also probe the midpoint of the gap between two adjacent targets with `elementFromPoint`: if it
|
|
402
|
+
returns neither button, the hit areas don't touch and cannot steal each other's clicks. Compare the
|
|
403
|
+
tap-target size against WCAG 2.5.8 (24×24 CSS px); anything smaller may still pass via the spacing
|
|
404
|
+
exception if a 24px circle on each target wouldn't intersect the neighbour's, so report the number
|
|
405
|
+
rather than declaring a violation.
|
|
406
|
+
|
|
407
|
+
#### `accessibilityState.selected` does not reach the web DOM
|
|
408
|
+
|
|
409
|
+
react-native-web maps the `disabled` half of `accessibilityState` (you get `disabled` +
|
|
410
|
+
`tabindex="-1"`) but for `accessibilityRole="button"` it drops `selected` — no `aria-selected`,
|
|
411
|
+
`aria-pressed` or `aria-checked` is emitted. So a `Pressable` with
|
|
412
|
+
`accessibilityState={{selected: isSelected}}` renders a selected DOM node **identical** to an
|
|
413
|
+
unselected one apart from its content. Always verify this by dumping every attribute of both nodes
|
|
414
|
+
rather than trusting the source.
|
|
415
|
+
|
|
416
|
+
This matters most when a design unifies selected/unselected colors so glyph shape is the only visual
|
|
417
|
+
cue: there is then no non-visual cue at all, and docs claiming "selection is announced via
|
|
418
|
+
accessibility state" are false on web. The fix must be a **role-compatible** state: `aria-selected`
|
|
419
|
+
is only valid on selectable composite descendants (tab, option, row, gridcell), so it is wrong on a
|
|
420
|
+
button. Use `accessibilityRole="checkbox"` + `aria-checked` for a two-state control (see the
|
|
421
|
+
keyboard caveat below), or `aria-pressed` if a toggle-button role is available.
|
|
422
|
+
|
|
423
|
+
Confirmed root cause: `react-native-web` (checked at 0.21.2) only consumes **flat** props in
|
|
424
|
+
`dist/modules/createDOMProps/index.js` (`aria-checked`, `aria-selected`, `aria-disabled`, …) and
|
|
425
|
+
never reads `accessibilityState` at all. The `disabled` half only appears to work because
|
|
426
|
+
`Pressable`'s own `disabled` prop sets it through a separate path. Note RN 0.81 has no
|
|
427
|
+
`aria-pressed` prop and RNW has no mapping for `accessibilityRole="togglebutton"`, so
|
|
428
|
+
`accessibilityRole="checkbox"` + `aria-checked` is usually the only two-state option that is both
|
|
429
|
+
typed by RN and forwarded by RNW.
|
|
430
|
+
|
|
431
|
+
#### Changing `accessibilityRole` changes the rendered ELEMENT — always re-test the keyboard
|
|
432
|
+
|
|
433
|
+
This is the trap that a role fix is most likely to introduce, and no amount of attribute dumping
|
|
434
|
+
will catch it. RNW renders `accessibilityRole="button"` as a **real `<button type="button">`**
|
|
435
|
+
(verify on any Terreno `Button`: `/dev/Button?story=Variants` shows
|
|
436
|
+
`<button aria-label="…" type="button">`), so both **Space** and **Enter** activate it via browser
|
|
437
|
+
default with no JS. Any other role — `checkbox`, `switch`, `radio`, `tab` — renders a plain
|
|
438
|
+
`<div role="…" tabindex="0">`, where there is no browser default activation and RNW's Pressable
|
|
439
|
+
keyboard path wires **Enter only**. Switching `button` → `checkbox` therefore silently drops Space,
|
|
440
|
+
which for `role="checkbox"` is the canonical ARIA APG activation key (Enter isn't even required).
|
|
441
|
+
|
|
442
|
+
So after any `accessibilityRole` change, test with the real keyboard, not just clicks:
|
|
443
|
+
|
|
444
|
+
1. `Tab` to the control and confirm a visible focus ring.
|
|
445
|
+
2. Press **Space**; dump `aria-checked`/`aria-pressed` and the visible state.
|
|
446
|
+
3. Press **Enter**; dump again.
|
|
447
|
+
4. Report which keys actually toggle. If Space is dead on a `checkbox`/`switch`/`radio` role, that is
|
|
448
|
+
a real keyboard-accessibility defect — suggest an explicit `onKeyDown` handling `" "` / `"Space"`
|
|
449
|
+
with `preventDefault()` (to suppress page scroll), or backing the control with a real
|
|
450
|
+
`<input type="checkbox">`.
|
|
451
|
+
|
|
452
|
+
When a component does add its own `onKeyDown`, check two things a native button gives you for free:
|
|
453
|
+
that Space does not scroll (test on a page with **real** scroll room and assert `scrollY`/`scrollTop`
|
|
454
|
+
is unchanged), and that a **held** key does not rapid-fire — `keydown` repeats while the key is down,
|
|
455
|
+
so the handler needs `if (event.repeat) return;`. A discrete-key-press harness cannot observe
|
|
456
|
+
auto-repeat, so read the source for the `event.repeat` guard rather than claiming it verified.
|
|
457
|
+
|
|
458
|
+
To decide whether it is a *regression* rather than pre-existing, read the previous commit's source
|
|
459
|
+
(`git show <prev>:path/to/Component.tsx | grep accessibility`) — if it used `accessibilityRole="button"`
|
|
460
|
+
it was a native button and Space used to work.
|
|
461
|
+
|
|
462
|
+
#### Baselining console warnings
|
|
463
|
+
|
|
464
|
+
The demo app emits pre-existing React-Native-Web deprecations on essentially every page
|
|
465
|
+
(`props.pointerEvents is deprecated`). Before blaming your component, load an unrelated component
|
|
466
|
+
page (e.g. `/demo/IconButton`) and diff the console output. Only warnings that appear on your page
|
|
467
|
+
and not the baseline are attributable to your change.
|
|
468
|
+
|
|
469
|
+
The `"shadow*" style props are deprecated. Use "boxShadow".` warning is fixed — `@terreno/ui` uses
|
|
470
|
+
`boxShadow` (see `createBoxShadow` in `ui/src/Utilities.tsx`) and `react-native-modalize` /
|
|
471
|
+
`react-native-actions-sheet` are patched in `patches/`. If it reappears, a new `shadow*` style prop
|
|
472
|
+
was introduced; do not re-add it to the baseline.
|
|
473
|
+
|
|
474
|
+
### `admin-frontend` and example app UI changes
|
|
475
|
+
|
|
476
|
+
Test `admin-frontend` changes in the example full-stack app.
|
|
477
|
+
|
|
478
|
+
1. Start the backend from the repo root:
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
bun run backend:dev
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The backend runs on `http://localhost:4000`.
|
|
485
|
+
|
|
486
|
+
2. Seed login data in a separate terminal:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
cd example-backend && bun run seed
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
3. Start the frontend from the repo root:
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
bun run frontend:web
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
The frontend runs on `http://localhost:8082`.
|
|
499
|
+
|
|
500
|
+
4. Log in with the seeded admin account:
|
|
501
|
+
- Email: `admin@example.com`
|
|
502
|
+
- Password: `testpassword123`
|
|
503
|
+
|
|
504
|
+
5. Navigate to the changed admin screen.
|
|
505
|
+
- Admin home: `http://localhost:8082/admin`
|
|
506
|
+
- Model table: `http://localhost:8082/admin/<modelName>`
|
|
507
|
+
- Configuration: `http://localhost:8082/admin/configuration`
|
|
508
|
+
- Consent forms: `http://localhost:8082/admin/consent-forms`
|
|
509
|
+
- Consent responses: `http://localhost:8082/admin/consent-responses`
|
|
510
|
+
|
|
511
|
+
6. Verify both the changed screen and the path that gets the user there.
|
|
512
|
+
- For list/table changes: verify loading, populated, empty, pagination, sorting, and row navigation when applicable.
|
|
513
|
+
- For form changes: verify create/edit validation, save loading, cancel/back navigation, and error handling.
|
|
514
|
+
- For admin cards/navigation: verify the card appears, the label/copy is correct, and click navigation still works.
|
|
515
|
+
|
|
516
|
+
### Other frontend package changes
|
|
517
|
+
|
|
518
|
+
- `example-frontend` user-facing screens: use `bun run backend:dev`, `cd example-backend && bun run seed`, and `bun run frontend:web`, then log in with `test@example.com` / `testpassword123`, navigate to the changed feature, and exercise it.
|
|
519
|
+
- Demo-only story changes: use the demo app `/dev` route and the story being changed.
|
|
520
|
+
- Generated SDK or API-surface-only frontend changes: combine targeted automated checks with an example app login + feature smoke test when a rendered UI path is affected.
|
|
521
|
+
|
|
522
|
+
## Report format
|
|
523
|
+
|
|
524
|
+
Findings first when anything broke (what the user sees, repro, evidence, fix made or
|
|
525
|
+
open). Then **Result** (PASS / FAIL / UNVERIFIED, one line), **Flow under test**,
|
|
526
|
+
**Environment** (URLs, viewports, tool, database, commit), **Remaining risk**, and per
|
|
527
|
+
app × viewport: the checks table, the interaction steps, the DOM assertions, every
|
|
528
|
+
screenshot labelled, the recording, and the console log; then per simulator flow:
|
|
529
|
+
result, device, failure text, final screen, recording, log tail. Concise, but concrete
|
|
530
|
+
enough that a PR reviewer trusts it without re-running. **Result** is the worst outcome
|
|
531
|
+
across every required leg: any FAIL makes it FAIL, otherwise any UNVERIFIED leg makes it
|
|
532
|
+
UNVERIFIED. A required leg that did not run, such as the simulator for a mobile-focused
|
|
533
|
+
change, is UNVERIFIED with its blocker, never remaining risk. Only platforms the change
|
|
534
|
+
does not require go under remaining risk.
|
|
535
|
+
|
|
536
|
+
## GitHub Reviewer Evidence
|
|
537
|
+
|
|
538
|
+
Post UI verification evidence to GitHub through the PR body so reviewers can see it without local setup.
|
|
539
|
+
|
|
540
|
+
- Follow [`.github/PULL_REQUEST_TEMPLATE.md`](../../../../.github/PULL_REQUEST_TEMPLATE.md):
|
|
541
|
+
one concise `Verification` row per exercised flow, naming the tested URL, the flow and
|
|
542
|
+
state proved, and the Result line. Never include credentials.
|
|
543
|
+
- Save screenshots and videos under `/opt/cursor/artifacts` (or your scratchpad) and
|
|
544
|
+
reference only decisive artifacts with HTML tags, e.g.
|
|
545
|
+
`<video src="/opt/cursor/artifacts/admin_model_list_demo.mp4" controls></video>`.
|
|
546
|
+
Use `RecordScreen` in Cursor Cloud for video walkthroughs.
|
|
547
|
+
- Use the PR management tool to create or update the PR body after artifacts exist.
|
|
548
|
+
Without a tool that uploads files, or when `/opt/cursor/artifacts/` is missing or
|
|
549
|
+
read-only, upload with the `attach-pr-media` skill and use the URLs it returns, or
|
|
550
|
+
link the secret gist the repository's publish script created.
|
|
551
|
+
- Keep evidence minimal: one short video is preferred for interaction flows, plus one screenshot when it shows a static visual state more clearly.
|
|
552
|
+
|
|
553
|
+
## Rationalizations to reject
|
|
554
|
+
|
|
555
|
+
- "The build passes, so it works." Render it.
|
|
556
|
+
- "It's a one-line CSS change." Screenshot it, at phone width too.
|
|
557
|
+
- "I checked the happy path." Check error, empty, and loading states.
|
|
558
|
+
- "The screenshot looks right." Assert on the DOM as well.
|
|
559
|
+
- "There's a console error but it's probably unrelated." Explain it or fix it.
|
|
560
|
+
- "I'll mention it might need testing." Verify it or mark it UNVERIFIED.
|
|
561
|
+
- "It works on web, and it's React Native, so it works on the phone." Not for
|
|
562
|
+
mobile-focused features. Run the Maestro flow, or direct simulator commands when the
|
|
563
|
+
app has no Maestro, on the simulator.
|
|
564
|
+
- "Something is already on the port, I'll just use it." It may be staging. Stop and say
|
|
565
|
+
the ports are taken.
|
|
566
|
+
|
|
567
|
+
## Cursor Cloud Notes
|
|
568
|
+
|
|
569
|
+
- Delegate GUI-driven verification to the `ui-verifier` subagent when your harness supports subagents; in Cursor Cloud, fall back to its built-in browser/GUI capability.
|
|
570
|
+
- For web apps on Bun 1.4+, use the documented [`terreno web`](../../docs/reference/cli.md#web-app-automation) flow to collect an agent-readable snapshot and final screenshot. This can prove static rendered state and complement the required GUI video for interaction flows.
|
|
571
|
+
- Use `RecordScreen` for user-facing video walkthroughs of interactive UI changes.
|
|
572
|
+
- Leave test servers running after verification so the user can continue testing.
|
|
573
|
+
|
|
574
|
+
## Final Response Checklist
|
|
575
|
+
|
|
576
|
+
- Lead with the Result line and the evidence link; include the decisive screenshot or
|
|
577
|
+
video for UI changes.
|
|
578
|
+
- Prefix every command in the testing section with pass, warning, or fail status.
|
|
579
|
+
- Explain why each test or check was run, and which required checks each run passed.
|
|
580
|
+
- Mention any environment limitation only when it prevented expected UI verification,
|
|
581
|
+
and then mark the affected flow UNVERIFIED with the blocker.
|
|
582
|
+
- Confirm PR evidence was posted or will be posted before the PR is opened/updated.
|
|
583
|
+
|
|
584
|
+
## Pixel Measurement: Derive the Screenshot Scale Empirically
|
|
585
|
+
|
|
586
|
+
When measuring painted ink (or any exact pixel geometry) from a zoomed screenshot, never assume
|
|
587
|
+
`saved screenshot px == zoom factor × CSS px`. The screenshot the harness writes to disk can be a
|
|
588
|
+
different size than the image it shows inline, and the browser window may have been resized between
|
|
589
|
+
runs.
|
|
590
|
+
|
|
591
|
+
Do this before dividing by the zoom factor:
|
|
592
|
+
|
|
593
|
+
1. Read `window.innerWidth` and compare it to the width of the saved PNG. If they are equal, one
|
|
594
|
+
screenshot pixel is one CSS layout pixel and the zoom factor is the only divisor.
|
|
595
|
+
2. Cross-validate against a known layout pitch (e.g. two flush 32px targets must sit exactly
|
|
596
|
+
`32 × zoom` screenshot px apart). If the pitch does not match, the scale assumption is wrong and
|
|
597
|
+
every derived number is wrong.
|
|
598
|
+
3. Measure each glyph inside **its own element rect** (from `getBoundingClientRect()` at the same
|
|
599
|
+
zoom), not across the whole image — otherwise adjacent labels and section bands leak into the
|
|
600
|
+
bounding box.
|
|
601
|
+
4. Use the **modal colour of that sub-region** as the background reference rather than a hardcoded
|
|
602
|
+
page background, and re-run the bbox at 2–3 thresholds (e.g. background delta > 10 / 30 / 60).
|
|
603
|
+
A measurement that shifts with the threshold is an antialiasing artefact, not a result.
|
|
604
|
+
5. Report ink width and height separately per variant, plus the non-background pixel count — the
|
|
605
|
+
count is what distinguishes a filled glyph from an outline glyph that shares the same bbox.
|