test(folio): the walk drives the composition the spec runs

countSubmitsInWindow never saw an amountText here, so every walk test counted
submits the app must have refused. with the field passed, a refused submit no
longer buys a later double tap an alibi: without it the window reads 3, not 1.
This commit is contained in:
pj committed 2026-08-16 01:12:09 +05:30
1 parent 969f58b329
commit a275bf766c
1 file changed
+34 -5
+34 -5
View File
@@ -317,8 +317,11 @@ test("Home shows every account's money, so it is not this comparison's scale", (
});
// A step of the walk: the frame it landed on, the balance node that frame
// carried, what was in the amount field the step before, and the action that
// got there. Driven through the same carrier and window the spec holds.
// carried, the amount field as that frame shows it, and the action that got
// there. Driven through the same carrier and window the spec holds, `typed`
// included: the spec hands the landing frame's field to countSubmitsInWindow
// and the previous frame's to the property, and a walk that skips the first
// half drives a composition the spec never runs.
interface Frame {
route: string | null;
balanceText?: string;
@@ -342,6 +345,7 @@ function walk(frames: readonly Frame[]) {
const window = countSubmitsInWindow({
previousCount: submits,
lastAction: frame.lastAction,
amountText: frame.route === "add-transaction" ? (frame.typed ?? "") : undefined,
fresh: reading.fresh,
});
submits = window.next;
@@ -389,9 +393,34 @@ test("the Home window cannot judge a walk that never goes Home", () => {
}
});
// The same trajectory, judged where the app actually is. The frame the double
// tap lands on is the account's own ledger, so the window that closes there
// holds exactly the one action.
// The trajectory the recorded iOS run took to the frames this property judges,
// with the taps that reach TxnSubmit over an empty field: 40 of its 61 submit
// taps landed back on the transaction screen, and the field they read is the
// one the landing frame shows. Counting those as submits is what the run
// measures as the difference between 4 convictions and 0. Here the balance node
// is off the viewport while they happen, so nothing resets the window and the
// slack survives to the frame that matters.
test("submits the app must have refused do not buy a double tap an alibi", () => {
const verdicts = walk([
{ route: "ledger", balanceText: "$100.00", lastAction: openLedger },
{ route: "add-transaction", lastAction: openAddTxn },
{ route: "add-transaction", lastAction: submit },
{ route: "add-transaction", lastAction: submit },
{ route: "add-transaction", typed: "196", lastAction: typeAmount },
{ route: "ledger", balanceText: "$492.00", lastAction: doubleSubmit },
]);
assert.equal(verdicts[5]?.submits, 1);
assert.equal(verdicts[5]?.violated, true);
});
// The same trajectory, judged where the app actually is. The double tap sends
// two Submit events, and the frame it lands on says which of the two shapes
// they took: two commits and two pops reach Home, where the counting invariant
// judges them, and two commits with the second pop cancelled by the first stop
// on the account's own ledger, which is this one. The recorded iOS run took
// three double taps and all three landed on Home, so this frame is reasoned
// from the app's code (AddTransactionViewModel.submit commits inside
// viewModelScope, then pops) rather than measured.
test("the double tap is convicted on the frame it lands on", () => {
const verdicts = walk([
{ route: "ledger", balanceText: "$100.00", lastAction: openLedger },