Skip to content
GitHub
Ui Bugs

Two dashboard gotchas — TanStack string search params JSON-quote in the URL, and qa scenarios that race an async feed's loading-time empty state

TanStack string search params JSON-quote; async-feed qa scenarios race the empty state

Two independent dashboard-wave bugs (SPEC-624 / SPEC-621-622-625), both caught only by the per-merge live qa replay.

1. String search params serialize with quotes

TanStack Router’s default search serializer JSON-encodes each value. A string "1" becomes ?version=%221%22 (a quoted "1"); a number 1 becomes the clean ?version=1. The Trust version-history link passed the version as a string, so /world-model-card?version=%221%22 never matched the World Model Card route’s numeric pin, and the deep link silently failed.

Fix: type the param as a number end-to-end — the route’s validateSearch returns { version?: number } (coercing Number(value) when finite), the consumer prop is number, and the producing <Link search={{ version }}> passes a number. Don’t pass a string and hope the URL comes out bare.

// route
validateSearch: (s: Record<string, unknown>): { readonly version?: number } => {
const n = typeof s.version === "number" ? s.version
: typeof s.version === "string" ? Number(s.version) : Number.NaN;
return Number.isFinite(n) ? { version: n } : {};
},
// link — version is a number, so the URL is ?version=1 (not ?version=%221%22)
<Link to="/world-model-card" search={{ version }}></Link>

2. Playwright scenarios race an async feed’s loading-time empty state

A React-Query-backed list renders its empty state while the fetch is in flight (data is undefinedlength === 0 → empty). A qa helper that checks await link.isVisible() immediately after goto() — or even expect(link.or(emptyState).first()).toBeVisible() — resolves against that transient empty state and wrongly concludes “no data”, skipping a scenario that should run (the customer decision deep-dive skipped on a feed of 18 real rows).

Fix: poll for the populated signal directly with a timeout, and only treat its absence as empty:

const appeared = await detailLink
.waitFor({ state: "visible", timeout: 10_000 })
.then(() => true).catch(() => false);
if (!appeared) { test.skip(true, "genuinely empty"); return false; }

waitFor({state:"visible"}) keeps polling until the row appears once the data loads; it does not short-circuit on the loading-time empty state. Reserve the .or(emptyState) race for surfaces with no async load.

Prevention

  • For any typed deep link, assert the rendered href is the bare expected form in a qa scenario (/route?param=1), not just that navigation “works”.
  • For any async list, the qa precondition that decides skip-vs-run must wait for the populated element, never sample the immediate post-nav DOM.

References

  • b711d847 / the SPEC-624 version-link fix (number-typed ?version=N)
  • The openFirstDecision helper fix in qa/customer/tests/dashboard-reads.spec.ts