Remote / foldkit-remote
Testing what a reader sees: Trace
"A list on screen never blinks to Loading" depends on the reducer, the read
entries and the order answers land in. foldkit-remote/testing checks it in
Vitest, with no browser. Trace.run runs your update and the domain's own
Subscriptions from a Model. It holds every request they make. Each step is
one of your Messages, or a Respond that lets one held request go on, answered
by the RemoteClient you pass. Every Model in between is recorded, so a
regression that lasts one Message cannot fall between samples.
ts
import { Respond, Trace } from 'foldkit-remote/testing'
const trace = await Trace.run(Data, {
start: loaded, // a Model showing the list
update,
active,
client: server, // a fake, or your server's in-process client
steps: [
Message.Created({ id: 'p2', name: 'Two' }),
Respond.mutation(Create), // the answer lands first
Respond.read(Project, 'p2'), // then the new row's fields
],
})
expect(Trace.regressions(trace, { list })).toEqual([])
expect(Trace.tags(trace, list)).toEqual(['Ready', 'Refreshing', 'Ready'])- The trace decides when; the client decides what.
Respond.query(ref),Respond.read(Entity, id)andRespond.mutation(M)each answer the oldest held request of their kind, and each takes{ fail }to answer with an error instead.Respond.allanswers everything held, and what that leads to, until nothing is. Reorder the steps to try another interleaving. Trace.regressionsnames each time a read that showed a value readLoading,Initial, orFailedwithout it. It gives the step, the Message andData.why; it isData.timelineover the trace, kept to its regressions.NotFoundis not a regression, since the server said the row is gone.Trace.tagsis each change of a read's tag.- It fails loudly. A
Respondthat matches nothing held throws and lists what is held. A request abandoned by a restart is no longer held. A Subscription or Command that dies throws. Anupdatethat does not take Remote's Messages is a type error. - What it leaves out. Its clock stands still, at
now(0 unless given): every Command it runs and every answer it asks for reads that time (Remote.now, an answer's date), and a retentiongrace, an age-out and a live restart never fire. A live stream sends nothing, so script its events asLiveReceivedMessages. Only Remote's Commands run, andentry.skippednames the others. It drives a domain whose Messages are in your union, not aRemote.fold. - Durable writes need the outbox. Pass
outbox(yourData.outboxwiring) and the trace starts from itsrestore, over an empty store of its own, and runs its Subscriptions as one tab: a durable write is stored and sent by it, andRespond.mutationanswers that send.