Skip to content

Fix what end-to-end testing turned up - #38

Merged
wak327 merged 5 commits into
mainfrom
mobile-e2e-fixes
Sep 12, 2026
Merged

wak327 merged 5 commits into
mainfrom
mobile-e2e-fixes

Conversation

@wak327

@wak327 wak327 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Running the app end to end against the local demo stack and then against production. Five commits: three bugs the testing found, and two pieces of test work that finding them required.

The bugs

A board whose columns rendered two pixels wide. Every column collapsed to 2px and every card to 0px. measureInWindow never resolves on web, and tab screens lay out while still detached, so the measured width was never anything. The window width minus the rail is the source now, with the measurement used only once it means something.

Status colours the web stores as CSS variables. This is the one only production could have shown. A status created through the web UI stores its colour as var(--color-status-progress) — production has one, "In QA" on team BE. Seeded statuses carry hex, which is why every local instance looked fine. A browser resolves that against the stylesheet; React Native hands it to the native layer, which cannot, so the dot renders as nothing at all. resolveColor maps the variable back onto the active theme's tokens, which has the side benefit of making those dots follow light and dark instead of freezing at whatever the web's light palette happened to be. Anything already paintable passes through untouched, and an unrecognised value falls back to a visible grey rather than disappearing. Every status and label dot goes through one Dot primitive now.

Theme options that could not be announced or operated. The System/Light/Dark controls had no accessible name, and react-native-web does not map accessibilityState onto aria-*.

The test work

The offline queue's pause and replay, against a real server. The conflict rule was unit-tested; the part actually carrying the feature was not — that React Query withholds these mutations when the network goes away and replays them when it returns. Four round trips now assert that, including a move somebody else overtook being refused rather than applied on top of their work.

Live suites that tripped over each other. Two things that look like product failures and are not:

  • They picked their fixture team by position, and GET /teams promises no order. The team-administration and onboarding suites create teams the API offers no way to delete, so an instance these have run against accumulates empty teams permanently — twenty on the local stack by now. teams[0] eventually lands on one and the board suite fails on an empty page.
  • They cannot run in parallel. Several register accounts, registration is throttled per IP and charged even on success. The 429 is an AxiosError whose circular req/res pair crashes the jest worker trying to serialize it, so it surfaces as Test suite failed to run with no mention of throttling. npm run test:live runs them in band.

Checks

41 suites / 261 tests pass with every live suite enabled against both the local stack and production, typecheck clean, 0 lint errors. CI runs without credentials, so the live suites skip there as before: 169 pass, 92 skip.

Production was left as it was found — read-only apart from one scratch issue created and deleted in the same run, verified by a residue check.

Not covered

Nothing ran on a real device. Everything went through react-native-web, so SecureStore proper, NetInfo, the splash hold and board drag-and-drop are unverified on hardware. Production could only be driven through the client modules rather than the rendered UI, because its CORS allows only https://softtrack.quantadev.io.

🤖 Generated with Claude Code

wak327 and others added 5 commits September 12, 2026 01:24
Found by running the app rather than by any test.

The board sized its columns from a width obtained with measureInWindow, which
is asynchronous and never resolves on web -- and react-navigation lays a tab
screen out while it is still detached, so the one onLayout that does fire
reports zero. A zero width made every column, and so every card, zero pixels
wide: the whole board was in the DOM and none of it was visible.

Width now comes from the window less the nav rail, which is always known, and a
real measurement refines it if one arrives. Verified live at all three size
classes: 320px columns on a 390 phone (neighbours peeking), exactly two 306px
columns beside the rail at 700, exactly four 292px at 1280.

Also from running it:

SecureStore has no web implementation, so signing in on the web target threw
and the token was never stored. It falls back to localStorage there -- the same
store the web app keeps its own token in. The web build is for development and
review against a real instance, which is precisely what it could not do.

The teams query fired before anyone had signed in, because TeamProvider mounts
outside the auth guard. It is gated on a session now, and the team list is
treated as untrusted on the way out of a week-long persisted cache: a poisoned
entry degraded to a crash at boot above the error boundary.

react-native-web does not map accessibilityState.checked onto aria-checked, so
a task list on the web build announced checkboxes with no state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three appearance options carried no accessible label. Every other control
in the app has one, and without it a screen reader announces three unnamed
buttons -- and nothing automated can drive them either, which is how this was
found.

Also sets aria-selected, since react-native-web does not map
accessibilityState.selected onto it, the same gap the task checkboxes had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A status created through the web UI stores its colour as
`var(--color-status-progress)` rather than a hex value -- the production
instance has one, "In QA". A browser resolves that against the stylesheet;
React Native hands it to the native layer, which cannot, so the dot renders
as nothing. Seeded statuses carry hex and hid this.

`resolveColor` maps the variable back onto the active theme's tokens, which
also makes these dots follow light and dark rather than freezing at whatever
the web's light palette happened to be. Anything already paintable passes
through untouched, and an unrecognised value falls back to a visible grey
instead of disappearing.

Routed through a `Dot` primitive so every status and label dot resolves the
same way, along with `Avatar` and `Chip`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The existing tests cover the conflict rule in isolation, which leaves the
part that actually carries the feature untested: that React Query pauses
these mutations when the network goes away and replays them when it returns.

Three round trips against a running instance -- a move, a comment and an
issue creation -- asserting the request is genuinely withheld while offline
rather than merely failing, and that a move somebody else has overtaken is
refused instead of applied on top of their work.

The client is mounted, because an unmounted one never subscribes to
`onlineManager` and so never resumes anything; and mutation `gcTime` is zero,
because a settled mutation otherwise sits behind a five-minute collection
timer that keeps the runner alive after the last assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running them all against one instance surfaced two things that look like
product failures and are not.

They pick their fixture team by position, and `GET /teams` promises no order.
The team-administration and onboarding suites create teams the API offers no
way to delete, so an instance these have run against accumulates empty teams
for good -- twenty of them on the local stack by now. `teams[0]` eventually
lands on one, and the board suite fails with an empty page. `seededTeam()`
asks for the seeded key instead, falling back to any team that actually has
issues.

They also cannot run in parallel. Several register accounts, registration is
throttled per IP and charged even on success, and concurrent suites exhaust
the budget at once. The 429 that comes back is an AxiosError whose circular
req/res pair crashes the jest worker trying to serialize it, so it surfaces as
`Test suite failed to run` with no mention of throttling -- which is what made
this worth a note in the README rather than a shrug. `npm run test:live` runs
them in band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wak327
wak327 merged commit 2dcf969 into main Sep 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant