import assert from "node:assert/strict"; import { test } from "node:test"; import { countSubmitsInWindow, readHomeCards, readHomeTotalBalance, routeOfFrame, submitChangesBalanceByTypedAmount, } from "../../../examples/folio/sanderling/predicates.ts"; // The spec's own screen table. A frame is the set of markers its accessibility // tree carries, which is all routeOfFrame is allowed to look at. const SCREENS = { login: "LoginScreen", "add-account": "AddAccountScreen", "add-transaction": "AddTransactionScreen", ledger: "LedgerScreen", home: "HomeScreen", }; const frame = (...tags: string[]) => (tag: string) => tags.includes(tag); test("a frame showing one screen names its route", () => { assert.equal(routeOfFrame(SCREENS, frame("LoginScreen")), "login"); assert.equal(routeOfFrame(SCREENS, frame("AddAccountScreen")), "add-account"); assert.equal(routeOfFrame(SCREENS, frame("AddTransactionScreen")), "add-transaction"); assert.equal(routeOfFrame(SCREENS, frame("LedgerScreen")), "ledger"); assert.equal(routeOfFrame(SCREENS, frame("HomeScreen")), "home"); }); test("a frame showing no screen at all is unknown", () => { assert.equal(routeOfFrame(SCREENS, frame()), null); assert.equal(routeOfFrame(SCREENS, frame("SomethingElse")), null); }); // The android transition frame: 425 of 1879 steps across 17 measured runs carry // two screens, in every combination the navigation graph allows. Ranking the // markers and taking the first answers add-transaction for the first of these // while a second, unscoped look answers "on Home" -- and two answers for one // frame is the defect. There is one answer now, and on a transition frame it is // "I do not know". test("a transition frame showing two screens names neither", () => { assert.equal(routeOfFrame(SCREENS, frame("AddTransactionScreen", "HomeScreen")), null); assert.equal(routeOfFrame(SCREENS, frame("HomeScreen", "LedgerScreen")), null); assert.equal(routeOfFrame(SCREENS, frame("AddAccountScreen", "HomeScreen")), null); assert.equal(routeOfFrame(SCREENS, frame("HomeScreen", "LoginScreen")), null); assert.equal(routeOfFrame(SCREENS, frame("AddTransactionScreen", "LedgerScreen")), null); }); test("the three-screen frames android also emits name nothing", () => { assert.equal( routeOfFrame(SCREENS, frame("AddTransactionScreen", "HomeScreen", "LedgerScreen")), null, ); }); // Everything read off Home takes the route as its only input, so a frame that // is not Home cannot be read as Home by anything. test("a transition frame's half-drawn Home total is not a reading", () => { const route = routeOfFrame(SCREENS, frame("AddTransactionScreen", "HomeScreen")); assert.deepEqual( readHomeTotalBalance({ route, totalText: "$86,911.00", previousCarrier: 8681600 }), { value: 8681600, carrier: 8681600, fresh: false }, ); }); test("nor is its half-drawn card list", () => { const route = routeOfFrame(SCREENS, frame("AddAccountScreen", "HomeScreen")); const carried = { Travel: "8", Checking: "0" }; const partial = { Travel: "8" }; assert.deepEqual(readHomeCards>({ route, reading: partial, previousCarrier: carried }), { value: carried, carrier: carried, fresh: false, }); }); // The false conviction itself, android seed 3, steps 98-102 of the recorded // trace. Five submits deep into the window the app sits on AddTransaction with // "339" typed; a double-tap on Back starts the trip Home; the frame that comes // back carries BOTH screens with Home's total already drawn behind the outgoing // one. The old spec read that total as a fresh Home reading, reset the window to // zero, and aimed the next tap at a TxnSubmit button that had stopped existing. // The tap landed on Home, committed nothing, and the property demanded 33900 of // movement for it. Nine of the eleven android convictions were this, all at // delta 0.0x, all a single Tap where the real bug is a DoubleTap. test("the measured android transition chain no longer convicts at delta 0", () => { let carrier: number | null = 8681600; let submits = 5; const step = ( tags: string[], totalText: string | undefined, lastAction: { kind: string; on: string } | null, ) => { const route = routeOfFrame(SCREENS, frame(...tags)); const reading = readHomeTotalBalance({ route, totalText, previousCarrier: carrier }); const window = countSubmitsInWindow({ previousCount: submits, lastAction, fresh: reading.fresh }); carrier = reading.carrier; submits = window.next; return { route, total: reading.value, submits: window.reported }; }; const back = { kind: "DoubleTap", on: "id:BackButton" }; const phantomSubmit = { kind: "Tap", on: "testTag:AddTransactionScreen > testTag:TxnSubmit" }; const transition = step(["AddTransactionScreen", "HomeScreen"], "$86,911.00", back); assert.equal(transition.route, null); assert.equal(transition.total, 8681600); assert.equal(transition.submits, 5); const landing = step(["HomeScreen"], "$86,911.00", phantomSubmit); assert.equal(landing.submits, 6); assert.equal( submitChangesBalanceByTypedAmount({ route: landing.route, lastAction: phantomSubmit, submitsInWindow: landing.submits, typedAmount: 33900, prevTotalBalance: transition.total, currTotalBalance: landing.total, }), true, ); // What the reset bought the old spec: the same landing, judged against a // window of one and a total the transition frame had already banked. assert.equal( submitChangesBalanceByTypedAmount({ route: "home", lastAction: phantomSubmit, submitsInWindow: 1, typedAmount: 33900, prevTotalBalance: 8691100, currTotalBalance: 8691100, }), false, ); });