softr-vibe-coding 2.15.3 → 2.16.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/CHANGELOG.md +4 -0
- package/README.md +31 -5
- package/SKILL.md +11 -4
- package/datasources/fields.md +5 -1
- package/datasources/multi-datasource.md +3 -1
- package/datasources/reading.md +26 -0
- package/datasources/softr-database.md +6 -0
- package/datasources/writing.md +34 -0
- package/package.json +1 -1
- package/references/anti-patterns.md +23 -4
- package/references/browser-checks.md +433 -19
- package/references/common-patterns.md +193 -15
- package/references/native-chrome-styling.md +1 -1
- package/references/printing.md +2 -0
- package/references/qa-playbook.md +370 -0
- package/references/quick-reference.md +2 -0
- package/references/searchable-dropdown.md +8 -3
- package/references/softr-mcp.md +167 -18
- package/ui-ux-guidelines.md +46 -6
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# QA of a Softr Vibe Coding app
|
|
2
|
+
|
|
3
|
+
How to QA an app made of Vibe Coding blocks, end to end, in the order to run it. This file is the
|
|
4
|
+
procedure and the reasons behind it. The mechanics are linked, not repeated: the browser commands
|
|
5
|
+
are in [browser-checks.md](browser-checks.md), the MCP in [softr-mcp.md](softr-mcp.md) and the data
|
|
6
|
+
shapes in [datasources/](../datasources/overview.md).
|
|
7
|
+
|
|
8
|
+
**Verified 2026-10-08.** Written from one end-to-end QA pass of a 15-block back-office app on Softr
|
|
9
|
+
Database (LCDB QA pass): 68 findings, two rounds of fixes and a live write pass. Each rule below
|
|
10
|
+
cost someone a wrong result first.
|
|
11
|
+
|
|
12
|
+
## When to use it
|
|
13
|
+
|
|
14
|
+
When an app's blocks are built and pushed (each push has passed the hash check in
|
|
15
|
+
[softr-mcp.md → Verifying a push](softr-mcp.md#verifying-a-push--the-deployed-source-is-the-only-proof)),
|
|
16
|
+
before the client relies on it, and again after each round of fixes. For a quick look at one
|
|
17
|
+
block, [browser-checks.md](browser-checks.md) is enough.
|
|
18
|
+
|
|
19
|
+
## The order of a pass
|
|
20
|
+
|
|
21
|
+
1. [Set-up](#set-up): which app to test, with which build, in which zone.
|
|
22
|
+
2. [Never writing by accident](#never-writing-by-accident): the guard first, the proof last.
|
|
23
|
+
3. [Roles](#roles): each page as each kind of user.
|
|
24
|
+
4. [Logged out](#logged-out): access from outside the browser.
|
|
25
|
+
5. [Measuring](#measuring): sizes, and numbers instead of looks.
|
|
26
|
+
6. [Checking numbers against the database](#checking-numbers-against-the-database).
|
|
27
|
+
7. [Testing edges on purpose](#testing-edges-on-purpose): failures, clocks, odd values.
|
|
28
|
+
8. [Proving a fix before and after](#proving-a-fix-before-and-after).
|
|
29
|
+
9. [Live write pass with a read-only checker](#live-write-pass-with-a-read-only-checker), only when
|
|
30
|
+
a fix needs a real save.
|
|
31
|
+
10. [Running QA with several agents and skeptics](#running-qa-with-several-agents-and-skeptics).
|
|
32
|
+
|
|
33
|
+
## Set-up
|
|
34
|
+
|
|
35
|
+
- **Check changes in the preview (the draft), not the live app.** A push reaches the draft, and the
|
|
36
|
+
live app changes only when someone publishes. Only two things run on the published app: the
|
|
37
|
+
[logged-out checks](#logged-out) and the re-check after a publish.
|
|
38
|
+
- **Mint a fresh preview link for each session and after every push** (`application_preview`). The
|
|
39
|
+
link is a sign-in token: never print it, store it, or write it into a script file
|
|
40
|
+
([browser-checks.md → Gotchas](browser-checks.md#gotchas)).
|
|
41
|
+
- **Prove which build is served before you judge a change**
|
|
42
|
+
([browser-checks.md → step 2](browser-checks.md#2-session-preview-cookie-page)).
|
|
43
|
+
- **Run every check in the client's time zone**
|
|
44
|
+
([browser-checks.md → step 1](browser-checks.md#1-the-clients-time-zone)).
|
|
45
|
+
- **Give each agent its own `--session` name.** Agents that share an agent-browser session
|
|
46
|
+
overwrite each other's pages.
|
|
47
|
+
- **Log times with `date -u`.** The machine's zone may not be the client's: the pass ran on a Mac at
|
|
48
|
+
UTC+3 for an app used at UTC−7, and UTC is the one clock that the stored stamps use.
|
|
49
|
+
|
|
50
|
+
Why: agents judged fixes on stale builds, shared one browser session across parallel agents and
|
|
51
|
+
logged times in the wrong zone (LCDB QA pass, 2026-10-08).
|
|
52
|
+
|
|
53
|
+
## Never writing by accident
|
|
54
|
+
|
|
55
|
+
The preview is wired to the live data
|
|
56
|
+
([softr-mcp.md](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)), so a
|
|
57
|
+
pass against a client's database has to leave no trace.
|
|
58
|
+
|
|
59
|
+
- **Block every save before the first click, and prove the block works**
|
|
60
|
+
([browser-checks.md → step 6](browser-checks.md#6-block-saves-before-any-click-and-prove-it)).
|
|
61
|
+
Probe again after every page load.
|
|
62
|
+
- **Press Save on purpose only with the guard proven**, and after you have read the save's own
|
|
63
|
+
guard in the source. Confirm in the request log that each write was aborted. A write that gets
|
|
64
|
+
through is reported at once, with its record id.
|
|
65
|
+
- **Afterwards, prove with MCP reads that nothing was written:**
|
|
66
|
+
- exact per-table counts against a baseline taken before the pass;
|
|
67
|
+
- the records you opened, by `updatedAt` (get the record);
|
|
68
|
+
- rows whose own fields point at the QA day or the QA logins (entered by, updated by, a date
|
|
69
|
+
field).
|
|
70
|
+
|
|
71
|
+
`database_search_records` ignores a sort on `updatedAt` and rejects a filter on `id`, so do not
|
|
72
|
+
rely on either to show "nothing unlisted" (LCDB write pass checker, 2026-10-08).
|
|
73
|
+
- **A race you cannot reproduce live with writes blocked stays in the local harness**, and the
|
|
74
|
+
report says so. "A second row still saving" is one: the first write fails at once, so the second
|
|
75
|
+
is never reached.
|
|
76
|
+
- To put a state on screen without changing data, see [Forcing
|
|
77
|
+
states](browser-checks.md#forcing-states).
|
|
78
|
+
|
|
79
|
+
## Roles
|
|
80
|
+
|
|
81
|
+
Role bugs were found only when a pass compared roles side by side: hints that pointed users to pages
|
|
82
|
+
they could not open, admin pages that opened blank for volunteers, and an update action with no
|
|
83
|
+
record condition (LCDB QA pass, 2026-10-08).
|
|
84
|
+
|
|
85
|
+
- **Check each page as an administrator, as an ordinary user, and as a logged-in user in no
|
|
86
|
+
group.** Impersonate rather than log in
|
|
87
|
+
([softr-mcp.md → Preview as](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)).
|
|
88
|
+
- **Read each block's Visibility first.** A block gated to one group does not render for the
|
|
89
|
+
others, so its role-dependent copy cannot be checked from those roles.
|
|
90
|
+
- **Use the administrator as the positive control.** Count the controls the lower role must not see
|
|
91
|
+
(void buttons, adjustments): an administrator saw 8 and 13 void buttons on two records where the
|
|
92
|
+
volunteer saw none, only a hint. A missing control is then proven, not just unseen.
|
|
93
|
+
- **Prove each impersonation took effect.** After `fetch('/studio/impersonate/<softrUserId>')` on
|
|
94
|
+
the preview origin, reopen the direct page URL and read the global:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
JSON.stringify(window.__softr_current_user) // name, email, avatar, userGroups
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
On a direct page URL it sits in the top window; only the toolbar shell lacks it, where it is in
|
|
101
|
+
the app iframe ([Preview as](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)).
|
|
102
|
+
The `fetch` returns 200 even before the page switches user, so read the global after reopening.
|
|
103
|
+
Take `<softrUserId>` from `application_list_users`, which lists manual memberships only: a
|
|
104
|
+
conditionally matched group shows only in this global
|
|
105
|
+
([softr-mcp.md → Condition-based user groups](softr-mcp.md#condition-based-user-groups)).
|
|
106
|
+
- **Read what each role is told, not only what it can click.** An empty state or a hint that sends a
|
|
107
|
+
role to a page it cannot open is a bug: three blocks pointed volunteers at Settings.
|
|
108
|
+
- **Check page visibility for every role**, and **every UPDATE or DELETE action on a per-user
|
|
109
|
+
table.** At "logged-in users" with no record condition, any login can overwrite another user's
|
|
110
|
+
row. What the server does and does not enforce is in
|
|
111
|
+
[softr-mcp.md](softr-mcp.md#what-the-server-enforces-on-a-blocks-data-endpoints).
|
|
112
|
+
|
|
113
|
+
## Logged out
|
|
114
|
+
|
|
115
|
+
A publish can change what an anonymous visitor reaches, and nothing in the browser session shows it.
|
|
116
|
+
After every publish, check from outside the browser:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
HOST=<subdomain>.softr.app # or the app's custom domain
|
|
120
|
+
curl -sI "https://$HOST/<page>" | grep -i -E '^(HTTP|location)'
|
|
121
|
+
# HTTP/… 301
|
|
122
|
+
# location: /login?next-page=/<page>
|
|
123
|
+
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H 'content-type: application/json' -d '{}' \
|
|
124
|
+
"https://$HOST/v1/datasource/applications/<app>/pages/<page>/blocks/<block>/datasources/<name>/records"
|
|
125
|
+
# 403
|
|
126
|
+
curl -s -o /dev/null -w '%{http_code}\n' "https://$HOST/sign-up" # 404 when sign-up is off
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **Every page that is not meant to be public** answers with a 301 to
|
|
130
|
+
`/login?next-page=<path>`. An app with a public landing page returns 200 there.
|
|
131
|
+
- **A POST to a block's records endpoint with no session** answers 403. The published-host form of
|
|
132
|
+
this URL is inferred from the one the preview calls (on `<subdomain>.preview.softr.app`); the 403
|
|
133
|
+
was seen on 2026-10-08.
|
|
134
|
+
- **If sign-up is meant to be off**, `/sign-up` returns 404, and on `/login` the public
|
|
135
|
+
`window.application_context` shows `signUpSettings.policy` as `DISABLED` (a read-only check).
|
|
136
|
+
- **Never POST to a `records-trigger` URL to test a write endpoint.** If the action is open, it
|
|
137
|
+
writes. Read the block's action permissions through the MCP instead (a push puts them back to
|
|
138
|
+
Softr's defaults: [softr-mcp.md](softr-mcp.md#verifying-a-push--the-deployed-source-is-the-only-proof)).
|
|
139
|
+
|
|
140
|
+
## Measuring
|
|
141
|
+
|
|
142
|
+
Measure, do not eyeball. The rules are in
|
|
143
|
+
[browser-checks.md → step 4](browser-checks.md#4-measuring-with-eval): prove the console capture on
|
|
144
|
+
every load, numbers for heights and scroll widths, an error inside its dialog's edges, a shadow root
|
|
145
|
+
without `innerText`, and the sizes to check, which include the width a block really gets beside a
|
|
146
|
+
sidebar.
|
|
147
|
+
|
|
148
|
+
- **Hit-test in two parts before a coordinate click.** `document.elementFromPoint` must be the
|
|
149
|
+
block's host and `root.elementFromPoint` the element you mean. The shadow root alone ignores
|
|
150
|
+
Softr's fixed phone tab bar, so a phone-size test that uses only it passes under the bar
|
|
151
|
+
([browser-checks.md → step 3](browser-checks.md#3-reaching-into-the-block)).
|
|
152
|
+
- **Look at the whole page, not just the part you changed.** Two buttons wrapping onto a second line
|
|
153
|
+
at 1280 were noticed during an unrelated focus check and fixed the next day.
|
|
154
|
+
|
|
155
|
+
## Checking numbers against the database
|
|
156
|
+
|
|
157
|
+
A page can look right and be wrong, and a browser cannot tell. Compare it with the database.
|
|
158
|
+
|
|
159
|
+
- **Recompute every figure with the MCP's aggregate and list reads**, using the client's month
|
|
160
|
+
boundaries and the app's own exclusions (voided rows, records entered in error). Compare each
|
|
161
|
+
page's figure with that number, and with the same figure on every other page that shows it.
|
|
162
|
+
- **Hold one counting rule per figure.** The "same number on every page" lens found 7 of the 15
|
|
163
|
+
major findings: allotments counted as served, received with or without repacked bulk, headcount
|
|
164
|
+
per row or per person. Where two pages cannot show the same number, compare it term by term
|
|
165
|
+
against the database and word the check "same rule, same terms".
|
|
166
|
+
- **Before you trust a boundary, check that rows exist on the boundary days** (Dec 31 and Jan 1, the
|
|
167
|
+
last and first day of each month in range). If none do, say so: every total matches with or
|
|
168
|
+
without a one-day bug. Base the boundary on the code (dates compared as `yyyy-mm-dd` text) and on
|
|
169
|
+
a one-day range read against the server. In one report the nearest rows were Dec 29 and Jan 2.
|
|
170
|
+
- **Read the live table before you trust a counting rule.** Old or migrated rows may have empty
|
|
171
|
+
links and drop out of past years. Check what one linked row stands for: a link counted child
|
|
172
|
+
rows, not visits, with voided rows included.
|
|
173
|
+
- **Pair each before and after with an independent count.** `DISTINCT` on a link field counted
|
|
174
|
+
distinct people in one call (it worked in one run and was refused in another): 57 + 296 + 43 = 396
|
|
175
|
+
showed that a tile reading 442 was wrong, and later matched the fixed tile. When the call is
|
|
176
|
+
refused, count the people from the rows.
|
|
177
|
+
- **Recompute the expected figure at check time.** Never use a baseline plus deltas from notes:
|
|
178
|
+
when agents write at the same time, the shared totals move under you.
|
|
179
|
+
- **Check one complete past year as well as the year in progress**, and for a header-and-lines
|
|
180
|
+
ledger make the period's line count equal the sum of the headers' link counts (an `IS_EMPTY`
|
|
181
|
+
filter on the line's header link must return 0).
|
|
182
|
+
- **A reconciliation flag must be seen to fire, or it is no guard.** A "table total against pivot
|
|
183
|
+
total" flag built from the same ledger rows cannot detect an orphaned row, because both sides
|
|
184
|
+
include it: build the fixture and watch it fire.
|
|
185
|
+
- **Rows shown rounded to two decimals can differ from their total by 0.01 per row.** That is
|
|
186
|
+
display rounding, not a bug: allow a tolerance of 0.01 times the row count.
|
|
187
|
+
- **Name the figure you now expect after each fix** (for example "442 → 396"), so the fixer and the
|
|
188
|
+
final cross-check prove the fix with a number.
|
|
189
|
+
- **Date-only values and boundaries** have their own steps: [browser-checks.md → step
|
|
190
|
+
5](browser-checks.md#5-date-only-values-against-the-stored-ones).
|
|
191
|
+
- **Every filter must be seen to narrow the result**, with row counts with and without it: filters
|
|
192
|
+
fail open ([reading.md](../datasources/reading.md#filters-fail-open)).
|
|
193
|
+
- **Prefer filter-only calls with one metric each**, and check that the parts add up to the
|
|
194
|
+
unfiltered total. The aggregate tool refused some combinations in the pass, and its limits are in
|
|
195
|
+
[softr-mcp.md → Softr Database tools](softr-mcp.md#softr-database-tools). Leave voided rows out
|
|
196
|
+
with an `IS_NOT true` filter, and for a handful of rows read them with `database_search_records`
|
|
197
|
+
and add them up.
|
|
198
|
+
|
|
199
|
+
## Testing edges on purpose
|
|
200
|
+
|
|
201
|
+
The happy path passed everywhere. The real bugs were on these edges, and several checks passed
|
|
202
|
+
without reaching the step they asserted (LCDB QA pass, 2026-10-08). The mechanics for the
|
|
203
|
+
first four are in [browser-checks.md → Forcing states](browser-checks.md#forcing-states).
|
|
204
|
+
|
|
205
|
+
- **A fake clock**: month end, year end, a clock change.
|
|
206
|
+
- **Held saves and double taps** ([step 7](browser-checks.md#7-click-then-read-what-it-sent)).
|
|
207
|
+
- **Partial and total read failures**, then every Try again and every section after it.
|
|
208
|
+
- **Served reads** for empty-like values: `null`, `''`, `0`, and the field missing. A blank number
|
|
209
|
+
is not zero: `Number(null)` and `Number("")` are both 0, and a partner with no monthly allocation
|
|
210
|
+
showed 0.
|
|
211
|
+
- **Detail pages with no id, and with an id that does not exist.** One page said "Partner not
|
|
212
|
+
found"; another opened an editable "Unnamed volunteer" that accepted Log hours; a third offered
|
|
213
|
+
Try again forever.
|
|
214
|
+
- **Keyboard number entry.** `fill` cannot enter unreadable text, so type quantities with the
|
|
215
|
+
keyboard, in Firefox as well, and probe with `5e`. What goes wrong and how to refuse it is in
|
|
216
|
+
[ui-ux-guidelines.md §11](../ui-ux-guidelines.md#number-inputs).
|
|
217
|
+
- **Retry after an edit**, and **leaving mid-save**
|
|
218
|
+
([step 7](browser-checks.md#7-click-then-read-what-it-sent)).
|
|
219
|
+
- **Reusing the form after a save, without a reload.** A form filled by an effect keyed on query
|
|
220
|
+
data was not refilled when the re-read returned the same data.
|
|
221
|
+
- **A void that leaves rows of zeros** in a per-entity totals table: a report built from ledger
|
|
222
|
+
links includes entities whose rows all net to zero.
|
|
223
|
+
- **Logged out** and **roles**: the two sections above.
|
|
224
|
+
|
|
225
|
+
Two rules for every one of these:
|
|
226
|
+
|
|
227
|
+
- **Pick test values where the old and the new behaviour give different results, and that do not
|
|
228
|
+
collide with seed data.** A size at 2,180 on hand is Low at a default of 2,500 but not at 2,000, so
|
|
229
|
+
the check proves the setting was read. A five-digit test phone number collided with a seed family
|
|
230
|
+
and opened the duplicate panel.
|
|
231
|
+
- **A negative check must reach the step it asserts.** Prove it got there: the request was or was not
|
|
232
|
+
sent, the guard's message showed. `ab click` on a disabled calendar day prints "Done" and does
|
|
233
|
+
nothing, so a refused day is proven by its disabled state, not by the click.
|
|
234
|
+
|
|
235
|
+
## Proving a fix before and after
|
|
236
|
+
|
|
237
|
+
A fix that passes against a friendly stub can fail live, and a check that cannot fail on the old
|
|
238
|
+
code proves nothing. Prove each fix with a figure or a failing-then-passing check.
|
|
239
|
+
|
|
240
|
+
### A local harness
|
|
241
|
+
|
|
242
|
+
React 18.2 in a shadow root with Tailwind v4, the block file bundled exactly as it is, fake data
|
|
243
|
+
seeded from live rows. Run the same test on the deployed version (it must fail) and on the fix (it
|
|
244
|
+
must pass): a date fix showed 18 failures on the deployed copy and none after.
|
|
245
|
+
|
|
246
|
+
- **Match the block's real width.** Either a fake sidebar of the measured width at the real window
|
|
247
|
+
size, or no sidebar and a viewport narrowed to the block's measured width (985px copied one app's
|
|
248
|
+
1280 window). Key a table-or-stack switch on the content's own `@container`, and confirm the
|
|
249
|
+
container-query branches in the real app, since a harness at the wrong width picks the wrong one.
|
|
250
|
+
- **Copy Softr's `scroll-behavior: smooth` on `html`.** Without it, block code that scrolls and then
|
|
251
|
+
measures passes in the harness and fails live (a calendar landed outside a landscape phone's
|
|
252
|
+
strip). Put any tab-bar stand-in in the light DOM at document level, as Softr's is.
|
|
253
|
+
- **Load the real table rows.** With them the harness matched the live column widths to the pixel,
|
|
254
|
+
and a column min-width fix that sounded right pushed every row button out of its card at 1280
|
|
255
|
+
until it was measured on the real rows. MCP records keyed by field id load into the harness as
|
|
256
|
+
they are.
|
|
257
|
+
- **Honest stubs:**
|
|
258
|
+
- blanks as `''` and as `null` (real blanks arrive both ways);
|
|
259
|
+
- a link field sometimes a single `{id, label}` object, not an array;
|
|
260
|
+
- `where` honoured, with a switch to ignore it: that proves the client-side filter still shows
|
|
261
|
+
the right rows if the server ignores `where`;
|
|
262
|
+
- `onError` called when a write rejects (a stub that ignores it hides the deployed block's error
|
|
263
|
+
toasts, and "no toast" is then an artifact);
|
|
264
|
+
- no list refresh after a write, and a re-read with the same rows returns the same `data`
|
|
265
|
+
object (otherwise a reset and a refetch land in one render and mask a refill bug);
|
|
266
|
+
- switches to fail the Nth write and to add per-table delays, and `root.unmount()` exposed so a
|
|
267
|
+
test can leave mid-save.
|
|
268
|
+
- **Clear `sessionStorage` between tests that share a tab.** A draft feature leaked between tests
|
|
269
|
+
and turned two unrelated date tests red.
|
|
270
|
+
|
|
271
|
+
### A check that can fail
|
|
272
|
+
|
|
273
|
+
- **A check proves something only if it fails on the pre-fix bundle.** Run it there and report both
|
|
274
|
+
results (3 of 11 checks passed on the base, 11 of 11 on the fix).
|
|
275
|
+
- **Rebuild the bundle after every edit**, stubs included. A printout-against-screen check passed
|
|
276
|
+
on a stale bundle because both sides were stale.
|
|
277
|
+
- **Run the full suite before you quote counts.** A filtered run overwrote a results file with one
|
|
278
|
+
test.
|
|
279
|
+
- **Treat older suites as a no-new-fails baseline.** They fail for stale reasons (a button that is
|
|
280
|
+
gone, a removed default, changed copy): count fails against the previous baseline, not against zero.
|
|
281
|
+
- **Make every lookup require its match** ([browser-checks.md → step
|
|
282
|
+
4](browser-checks.md#4-measuring-with-eval)).
|
|
283
|
+
- **Diff the page's text before and after, against a whitelist of the intended lines.** It catches
|
|
284
|
+
copy changes nobody meant.
|
|
285
|
+
- **Test pure logic in node.** Logic moved out of `Block()` can be compared with the old function
|
|
286
|
+
over thousands of generated inputs in several time zones (3,007 date ranges in three zones), and a
|
|
287
|
+
multi-source block renders to HTML with a `useRecords` stub keyed on `from`. Cut the logic out
|
|
288
|
+
between marker comments and run it with `esbuild` and `vm`, so the test runs the shipped code and
|
|
289
|
+
not a copy.
|
|
290
|
+
- **Check `uptime` before blaming a timing test.** A load average of 111 broke a 400 ms check that
|
|
291
|
+
passed alone.
|
|
292
|
+
|
|
293
|
+
### Which engine ran
|
|
294
|
+
|
|
295
|
+
- **Report the engine that actually ran each check.** A harness launcher quietly ran Chromium when
|
|
296
|
+
asked for WebKit, and one Firefox build would not launch, so cross-engine claims were Chromium-only.
|
|
297
|
+
- **Test focus and number entry in Firefox as well as Chromium.** Safari and macOS Firefox do not
|
|
298
|
+
focus a clicked button, so code that returns focus to "the button that opened this" must be handed
|
|
299
|
+
that button; number fields differ by engine and by the Mac's region
|
|
300
|
+
([Testing edges](#testing-edges-on-purpose)).
|
|
301
|
+
- **For WebKit without a download**, a small Swift `WKWebView` runner can load the harness page; it
|
|
302
|
+
found a WebKit-only scroll bug.
|
|
303
|
+
- **Give each parallel harness its own port.** A shared fixed-port Firefox wrapper killed other
|
|
304
|
+
agents' runs.
|
|
305
|
+
- **Firefox quirk:** it reports a `console.error` of an error object as just "Error".
|
|
306
|
+
|
|
307
|
+
## Live write pass with a read-only checker
|
|
308
|
+
|
|
309
|
+
Some fixes can be proven only by a real save. Run a write pass only then, and only with the app
|
|
310
|
+
owner's go-ahead: the preview writes live data. On 2026-10-08 it ran after the owner said yes.
|
|
311
|
+
|
|
312
|
+
- **Use clearly marked test data**, and keep a manifest of record ids per table so that it can be
|
|
313
|
+
purged before go-live.
|
|
314
|
+
- **A test row must not create a user.** If a table is synced as the app's users table, a row with an
|
|
315
|
+
email creates an app user (and an invite, if the sync sends one). Test rows carry no email;
|
|
316
|
+
compare `application_list_users` before and after (the same users, no "QA" match).
|
|
317
|
+
- **Prove every save by reading it back through the MCP.** Do not rely on the logged response: an
|
|
318
|
+
aborted save has none, and one write-pass run showed no response body for a save. The request log
|
|
319
|
+
still shows the payload (`postData`) ([browser-checks.md → step
|
|
320
|
+
7](browser-checks.md#7-click-then-read-what-it-sent)).
|
|
321
|
+
- **With several agents writing, verify through your own rows and the links they carry**, not the
|
|
322
|
+
shared totals. A product's total moved by an unrelated −50 while the pass ran; other agents' rows
|
|
323
|
+
are the ones entered after your start time.
|
|
324
|
+
- **Put residue in the manifest.** Saving a setting and putting it back restores the value but not
|
|
325
|
+
`updated_by`, which stays on the editor.
|
|
326
|
+
- **Run a separate read-only checker afterwards** that recomputes the figures and the counts
|
|
327
|
+
([Never writing by accident](#never-writing-by-accident),
|
|
328
|
+
[Checking numbers](#checking-numbers-against-the-database)).
|
|
329
|
+
- **Re-check live after publishing:** the [logged-out checks](#logged-out) again, every block's
|
|
330
|
+
deployed hash against your copy, and the checker's figures once more.
|
|
331
|
+
|
|
332
|
+
## Running QA with several agents and skeptics
|
|
333
|
+
|
|
334
|
+
- **Split agents by job, not by page.** Our lenses were front desk at the door, warehouse stock,
|
|
335
|
+
partner agencies, volunteer coordination, and reports and settings, plus one for the "same number
|
|
336
|
+
on every page" and one for roles. A person doing one job crosses several pages, and that is
|
|
337
|
+
where the bugs hide.
|
|
338
|
+
- **Tell every agent what is already known and decided**, with a link to the decision log, and
|
|
339
|
+
which items are deferred on purpose. Otherwise they re-report settled behaviour: a live check that
|
|
340
|
+
did not know a shared-component change had been rejected listed it as a failure on 11 blocks, and
|
|
341
|
+
the repair agents then synced the rejected change into every one of them.
|
|
342
|
+
- **A skeptic re-runs and recomputes each finding** and drops what does not reproduce.
|
|
343
|
+
- **A gap agent goes last.** It looks for what nobody checked (pages, controls, sizes, roles, month
|
|
344
|
+
and year ends, empty and failed states) and checks those itself.
|
|
345
|
+
- **Only the orchestrator edits shared documents** (the app map, the decision log, the task list),
|
|
346
|
+
and the prompt says so. Check anyway: one agent edited the app map despite the instruction.
|
|
347
|
+
Project memory reaches subagents too: after "log QA lessons in this file" was saved as a memory,
|
|
348
|
+
agents appended to it themselves, and two agents writing one file at once can lose an edit. Say
|
|
349
|
+
in the prompt whether agents may write to it.
|
|
350
|
+
- **Give agents an absolute scratch folder** for screenshots and scratch files: eight PNGs landed in
|
|
351
|
+
the project root, an agent's working directory.
|
|
352
|
+
- **Verify an agent's claim before you act on it.** Two agents reported the page and block ids
|
|
353
|
+
swapped in the app map; the map was correct.
|
|
354
|
+
- **For parallel work**, give each agent its own browser session and harness port, and pinned
|
|
355
|
+
copies of files another agent may be editing. Another session may be working on the same app: on
|
|
356
|
+
2026-10-08 a second session pushed fixes to five blocks and published the app while the pass ran,
|
|
357
|
+
so scratch copies went stale. Compare the deployed hash with your copy right before editing and
|
|
358
|
+
right before pushing, and start from the project's mirror, not a scratch copy.
|
|
359
|
+
- **A reviewer re-reads the deployed hash, regenerates the diff, reruns the harness and checks that
|
|
360
|
+
the tests a report cites exist.** One review cited a test that was not on disk.
|
|
361
|
+
- **Compare paired fixes side by side.** When one rule lands in several places, check where each block
|
|
362
|
+
shows it, not only how it counts (two reports named the requests not yet fulfilled, Home did
|
|
363
|
+
not). Diff a shared paragraph word for word before you push, because separate editors'
|
|
364
|
+
sentences drift apart. Compare paired dialogs on first focus, close button, button colour and
|
|
365
|
+
where the error shows, and grep every other reader of a field whose label changed.
|
|
366
|
+
- **Let a different agent, ideally a different model, check what one agent built.**
|
|
367
|
+
- **A per-block pipeline** that held: author, two reviews, fixes, a hash-verified push, action
|
|
368
|
+
permissions re-applied and read back (a push resets them), a browser check at the sizes in
|
|
369
|
+
[browser-checks.md → step 4](browser-checks.md#4-measuring-with-eval) with saves blocked, and a
|
|
370
|
+
live write pass only where one is approved.
|
|
@@ -108,6 +108,8 @@ var createRecord = useRecordCreate({
|
|
|
108
108
|
createRecord.mutate({ name: "Jane", email: "jane@example.com" }); // FLAT — no { fields } wrapper
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
`err.message` is the browser's raw text ("Failed to fetch"); in a shipped block, map it through the block's one error helper ([Error Message Formula](../ui-ux-guidelines.md#error-message-formula)).
|
|
112
|
+
|
|
111
113
|
## Update (THE CORRECT PATTERN)
|
|
112
114
|
|
|
113
115
|
```jsx
|
|
@@ -377,7 +377,7 @@ on the trigger from the console (a synthetic click does not move focus either),
|
|
|
377
377
|
typing with real key events. A browser-automation "type" action that inserts text without key
|
|
378
378
|
events drops it into the last focused text field, which makes a correct Combo look broken.
|
|
379
379
|
|
|
380
|
-
**The incident:
|
|
380
|
+
**The incident: LCDB, 2026-10-07.** A browser check reported that in B4's New
|
|
381
381
|
partner modal, letters typed after opening the click-only "Partner type" list went into
|
|
382
382
|
Organization name, and Escape then showed the modal's "Discard your changes?" strip while the
|
|
383
383
|
list stayed open. Part of that report came from the test tool, whose "type" action wrote into the
|
|
@@ -438,7 +438,12 @@ Everything else — the trigger, the card, the rows — stays flat.
|
|
|
438
438
|
`preventDefault` + `stopPropagation` and closes only the list
|
|
439
439
|
([Move focus into the Combo when it opens](#move-focus-into-the-combo-when-it-opens))
|
|
440
440
|
- [ ] `aria-haspopup="listbox"`, `aria-expanded`, `role="listbox"` / `role="option"`,
|
|
441
|
-
`aria-selected
|
|
442
|
-
`
|
|
441
|
+
`aria-selected`; a click-only trigger that carries `aria-activedescendant` also gets
|
|
442
|
+
`role="combobox"`
|
|
443
|
+
- [ ] The trigger is named with `aria-labelledby="<label id> <trigger id>"`, so its accessible
|
|
444
|
+
name holds the field and the selected value. An `aria-label` with only the field name
|
|
445
|
+
replaces the button's own text, so a screen reader never hears the value (LCDB QA write
|
|
446
|
+
pass, 2026-10-08: an edit dialog's dropdowns announced "Partner type" and nothing else,
|
|
447
|
+
while another block's dropdowns announced both)
|
|
443
448
|
- [ ] Loading and empty states (`"Nothing matches that."`)
|
|
444
449
|
- [ ] No `@/components/ui/select` import anywhere in the file
|