mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(folio): cover merged-card parsing and unknown balances
This commit is contained in:
1 parent
5b80c0701b
commit
94084d9992
2 files changed
+153
-4
No files matched your search
@@ -0,0 +1,149 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
cardAccountName,
|
||||
cardBalanceText,
|
||||
computeHomeTotalBalance,
|
||||
parseDollarCents,
|
||||
} from "../../../examples/folio/sanderling/predicates.ts";
|
||||
|
||||
// Android and iOS expose AccountName/AccountBalance as their own nodes. Web
|
||||
// merges the card into one node whose text is initials + name +
|
||||
// "N transaction(s)" + balance, with no separator between the parts. Both
|
||||
// shapes have to land on the same balance, and the name has to stay usable as
|
||||
// an identity key.
|
||||
|
||||
const balanceOf = (cardText: string) =>
|
||||
parseDollarCents(cardBalanceText({ childText: undefined, cardText }));
|
||||
|
||||
// Renders a card the way HomeScreen.kt does, so a test states the account and
|
||||
// lets the fixture do the concatenating.
|
||||
const card = (initials: string, name: string, count: number, balance: string) =>
|
||||
`${initials}${name}${count === 1 ? "1 transaction" : `${count} transactions`}${balance}`;
|
||||
|
||||
test("structured child wins over the card text", () => {
|
||||
assert.equal(
|
||||
cardBalanceText({ childText: "$118.00", cardText: "SASavings1 transaction$118.00" }),
|
||||
"$118.00",
|
||||
);
|
||||
assert.equal(
|
||||
cardAccountName({ childText: "Savings", cardText: "SASavings1 transaction$118.00" }),
|
||||
"Savings",
|
||||
);
|
||||
});
|
||||
|
||||
test("merged card text: balance is the amount at the end, not scraped digits", () => {
|
||||
// The naive reading, text.replace(/[^0-9]/g, ""), absorbs the 12 of
|
||||
// "12 transactions" and returns 12258900.
|
||||
assert.equal(balanceOf("INInvestments12 transactions$2,589.00"), 258900);
|
||||
});
|
||||
|
||||
test("merged card text: singular transaction label", () => {
|
||||
assert.equal(balanceOf("SASavings1 transaction$118.00"), 11800);
|
||||
});
|
||||
|
||||
test("merged card text: negative balance keeps its sign", () => {
|
||||
assert.equal(cardBalanceText({ childText: undefined, cardText: "TRTravel3 transactions-$45.50" }), "-$45.50");
|
||||
assert.equal(balanceOf("TRTravel3 transactions-$45.50"), -4550);
|
||||
});
|
||||
|
||||
test("structured child: negative balance keeps its sign", () => {
|
||||
assert.equal(parseDollarCents(cardBalanceText({ childText: "-$45.50", cardText: undefined })), -4550);
|
||||
});
|
||||
|
||||
test("zero-balance card is 0, not unknown", () => {
|
||||
assert.equal(balanceOf("EFEmergency Fund0 transactions$0.00"), 0);
|
||||
assert.equal(balanceOf("Aa0 transactions$0.00"), 0);
|
||||
});
|
||||
|
||||
// The trap a lazy balance regex falls into: a name ending in digits runs
|
||||
// straight into the transaction count, so only anchoring the amount at the end
|
||||
// of the string gets it right.
|
||||
test("name ending in digits does not leak into the balance", () => {
|
||||
assert.equal(balanceOf(card("T2", "Travel 2024", 12, "$75.00")), 7500);
|
||||
assert.equal(balanceOf(card("T2", "Travel 2024", 0, "$0.00")), 0);
|
||||
assert.equal(balanceOf(card("20", "2024", 3, "-$1,234.56")), -123456);
|
||||
});
|
||||
|
||||
// The account key only has to be stable and per-account. newAccountBalanceIsZero
|
||||
// reads it as a set member: a key that drifted as an account's transaction
|
||||
// count grew would make an existing account look brand new, and the property
|
||||
// would fire on it for holding the balance it just earned.
|
||||
test("account key is stable as the transaction count and balance move", () => {
|
||||
const cards: [string, string][] = [
|
||||
["CH", "Checking"],
|
||||
["T2", "Travel 2024"],
|
||||
["A2", "Account 2"],
|
||||
["Aa", "a"],
|
||||
["?", ""],
|
||||
["5T", "5 transactions"],
|
||||
["X9", "x9"],
|
||||
];
|
||||
for (const [initials, name] of cards) {
|
||||
const keys = new Set<string>();
|
||||
for (let count = 0; count <= 130; count++) {
|
||||
keys.add(cardAccountName({
|
||||
childText: undefined,
|
||||
cardText: card(initials, name, count, `$${count * 7}.50`),
|
||||
}));
|
||||
}
|
||||
assert.equal(keys.size, 1, `key for ${JSON.stringify(name)} drifted: ${[...keys].join(", ")}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("account keys are distinct across the accounts a run creates", () => {
|
||||
const names: [string, string][] = [
|
||||
["CH", "Checking"],
|
||||
["SA", "Savings"],
|
||||
["TR", "Travel"],
|
||||
["EF", "Emergency Fund"],
|
||||
["IN", "Investments"],
|
||||
["T2", "Travel 2024"],
|
||||
["A2", "Account 2"],
|
||||
["A1", "Account 12"],
|
||||
["Aa", "a"],
|
||||
];
|
||||
const keys = names.map(([initials, name]) =>
|
||||
cardAccountName({ childText: undefined, cardText: card(initials, name, 4, "$9.00") }));
|
||||
assert.equal(new Set(keys).size, names.length);
|
||||
});
|
||||
|
||||
// elementHandle in the web runtime truncates node text at 200 characters, so a
|
||||
// long account name (the input corpus types 4096 "a"s) pushes the balance off
|
||||
// the end of the string. That balance is unknown, and unknown must not read as
|
||||
// zero or the Home total silently drops a card.
|
||||
test("card text truncated past the balance reads as unknown, not zero", () => {
|
||||
const cardText = "AA" + "a".repeat(198);
|
||||
assert.equal(cardBalanceText({ childText: undefined, cardText }), undefined);
|
||||
assert.equal(balanceOf(cardText), null);
|
||||
assert.equal(
|
||||
computeHomeTotalBalance({
|
||||
cardBalanceTexts: [
|
||||
cardBalanceText({ childText: undefined, cardText: "SASavings1 transaction$118.00" }),
|
||||
cardBalanceText({ childText: undefined, cardText }),
|
||||
],
|
||||
previousCarrier: 0,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("empty and missing text are unknown, not zero", () => {
|
||||
assert.equal(parseDollarCents(undefined), null);
|
||||
assert.equal(parseDollarCents(""), null);
|
||||
assert.equal(parseDollarCents("no digits here"), null);
|
||||
assert.equal(parseDollarCents("$12"), null);
|
||||
assert.equal(cardBalanceText({ childText: undefined, cardText: undefined }), undefined);
|
||||
assert.equal(cardAccountName({ childText: undefined, cardText: undefined }), "");
|
||||
});
|
||||
|
||||
test("home total sums merged-text cards the same as structured ones", () => {
|
||||
const merged = ["INInvestments12 transactions$2,589.00", "Aa0 transactions$0.00"].map(cardText =>
|
||||
cardBalanceText({ childText: undefined, cardText }));
|
||||
assert.equal(computeHomeTotalBalance({ cardBalanceTexts: merged, previousCarrier: 0 }), 258900);
|
||||
assert.equal(
|
||||
computeHomeTotalBalance({ cardBalanceTexts: ["$2,589.00", "$0.00"], previousCarrier: 0 }),
|
||||
258900,
|
||||
);
|
||||
});
|
||||
@@ -34,7 +34,7 @@ test("off Home (no cards) with carrier still 0: returns 0", () => {
|
||||
});
|
||||
|
||||
test("sequence: Home $30, off-Home, Home $50 tracks new Home totals", () => {
|
||||
let carrier = 0;
|
||||
let carrier: number | null = 0;
|
||||
carrier = computeHomeTotalBalance({
|
||||
cardBalanceTexts: ["$10.00", "$20.00"],
|
||||
previousCarrier: carrier,
|
||||
@@ -53,7 +53,7 @@ test("sequence: Home $30, off-Home, Home $50 tracks new Home totals", () => {
|
||||
});
|
||||
|
||||
test("Ledger step (no Home cards) holds the carrier, ignores Ledger balance", () => {
|
||||
let carrier = 0;
|
||||
let carrier: number | null = 0;
|
||||
carrier = computeHomeTotalBalance({
|
||||
cardBalanceTexts: ["$10.00", "$20.00"],
|
||||
previousCarrier: carrier,
|
||||
@@ -86,12 +86,12 @@ test("single card on Home overrides any previous carrier", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("undefined card balance text is treated as 0", () => {
|
||||
test("undefined card balance text makes the total unknown, not a partial sum", () => {
|
||||
assert.equal(
|
||||
computeHomeTotalBalance({
|
||||
cardBalanceTexts: [undefined, "$10.00"],
|
||||
previousCarrier: 0,
|
||||
}),
|
||||
1000,
|
||||
null,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user