mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
fix(folio-web): drop aria-label data-carrier abuse
Account cards now expose data-account-id + data-balance attrs and use a human-readable aria-label. total-balance / ledger / ledger-balance carry data-cents and data-txn-count instead of stuffing values into title. Spec rewritten to read structured attrs via object-form selectors.
This commit is contained in:
1 parent
8e9ac938f3
commit
0dda618187
3 files changed
+65
-74
No files matched your search
@@ -15,74 +15,73 @@ import {
|
|||||||
} from "@sanderling/spec";
|
} from "@sanderling/spec";
|
||||||
import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
|
import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
|
||||||
|
|
||||||
// Route detection via stable DOM ids
|
// Page-presence checks via stable element ids.
|
||||||
const onLoginPage = extract((s) => !!s.ax.find("id:email"));
|
const onLoginPage = extract((s) => !!s.ax.find({ id: "email" }));
|
||||||
const onHomePage = extract((s) => !!s.ax.find("id:add-account"));
|
const onHomePage = extract((s) => !!s.ax.find({ id: "add-account" }));
|
||||||
const onAddAccountPage = extract((s) => !!s.ax.find("id:account-name"));
|
const onAddAccountPage = extract((s) => !!s.ax.find({ id: "account-name" }));
|
||||||
const onLedgerPage = extract((s) => !!s.ax.find("id:ledger"));
|
const onLedgerPage = extract((s) => !!s.ax.find({ id: "ledger" }));
|
||||||
const onAddTxnPage = extract((s) => !!s.ax.find("id:txn-amount"));
|
const onAddTxnPage = extract((s) => !!s.ax.find({ id: "txn-amount" }));
|
||||||
|
|
||||||
// Auth state: true on any authenticated page, false only on login page
|
// Auth state: true on any authenticated page, false only on login page.
|
||||||
const loggedIn = extract((s) => {
|
const loggedIn = extract((s) => {
|
||||||
if (s.ax.find("id:email")) return false;
|
if (s.ax.find({ id: "email" })) return false;
|
||||||
return !!(
|
return !!(
|
||||||
s.ax.find("id:logout") ||
|
s.ax.find({ id: "logout" }) ||
|
||||||
s.ax.find("id:add-account") ||
|
s.ax.find({ id: "add-account" }) ||
|
||||||
s.ax.find("id:ledger") ||
|
s.ax.find({ id: "ledger" }) ||
|
||||||
s.ax.find("id:account-name") ||
|
s.ax.find({ id: "account-name" }) ||
|
||||||
s.ax.find("id:txn-amount") ||
|
s.ax.find({ id: "txn-amount" }) ||
|
||||||
s.ax.find("id:add-txn")
|
s.ax.find({ id: "add-txn" })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Total balance from title attribute on id:total-balance (set to raw cents integer)
|
// Read raw cents off explicit data-cents attributes; no aria-label parsing.
|
||||||
|
function readCents(value: string | undefined): number {
|
||||||
|
if (!value) return 0;
|
||||||
|
const parsed = parseInt(value, 10);
|
||||||
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
const totalBalance = extract((s) => {
|
const totalBalance = extract((s) => {
|
||||||
const el = s.ax.find("id:total-balance");
|
const el = s.ax.find({ id: "total-balance" });
|
||||||
if (!el) return 0;
|
return readCents(el?.attrs?.["data-cents"]);
|
||||||
const v = parseInt((el as { desc?: string }).desc ?? "", 10);
|
|
||||||
return isNaN(v) ? 0 : v;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Account cards expose "account:{id}:{balance}" via aria-label -> content-desc
|
// Account cards expose `data-account-id` + `data-balance` so the spec reads
|
||||||
|
// structured data without parsing aria-label.
|
||||||
const accountCards = extract((s) => {
|
const accountCards = extract((s) => {
|
||||||
return s.ax.findAll("descPrefix:account:").map((el) => {
|
return s.ax.findAll({ "data-testid": "account-card" }).map((el) => ({
|
||||||
const parts = ((el as { desc?: string }).desc ?? "").split(":");
|
element: el,
|
||||||
const balance = parseInt(parts[2] ?? "0", 10);
|
id: el.attrs?.["data-account-id"] ?? "",
|
||||||
return { element: el, id: parts[1] ?? "", balance: isNaN(balance) ? 0 : balance };
|
balance: readCents(el.attrs?.["data-balance"]),
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ledger txn count from title on id:ledger
|
|
||||||
const ledgerTxnCount = extract((s) => {
|
const ledgerTxnCount = extract((s) => {
|
||||||
const el = s.ax.find("id:ledger");
|
const el = s.ax.find({ id: "ledger" });
|
||||||
if (!el) return 0;
|
return readCents(el?.attrs?.["data-txn-count"]);
|
||||||
const v = parseInt((el as { desc?: string }).desc ?? "", 10);
|
|
||||||
return isNaN(v) ? 0 : v;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ledger balance from title on id:ledger-balance
|
|
||||||
const ledgerBalance = extract((s) => {
|
const ledgerBalance = extract((s) => {
|
||||||
const el = s.ax.find("id:ledger-balance");
|
const el = s.ax.find({ id: "ledger-balance" });
|
||||||
if (!el) return 0;
|
return readCents(el?.attrs?.["data-cents"]);
|
||||||
const v = parseInt((el as { desc?: string }).desc ?? "", 10);
|
|
||||||
return isNaN(v) ? 0 : v;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// UI element handles
|
// UI element handles.
|
||||||
const emailField = extract((s) => s.ax.find("id:email"));
|
const emailField = extract((s) => s.ax.find({ id: "email" }));
|
||||||
const passwordField = extract((s) => s.ax.find("id:password"));
|
const passwordField = extract((s) => s.ax.find({ id: "password" }));
|
||||||
const loginSubmit = extract((s) => s.ax.find("id:login-submit"));
|
const loginSubmit = extract((s) => s.ax.find({ id: "login-submit" }));
|
||||||
const logoutButton = extract((s) => s.ax.find("id:logout"));
|
const logoutButton = extract((s) => s.ax.find({ id: "logout" }));
|
||||||
const addAccountButton = extract((s) => s.ax.find("id:add-account"));
|
const addAccountButton = extract((s) => s.ax.find({ id: "add-account" }));
|
||||||
const accountNameField = extract((s) => s.ax.find("id:account-name"));
|
const accountNameField = extract((s) => s.ax.find({ id: "account-name" }));
|
||||||
const addAccountSubmit = extract((s) => s.ax.find("id:add-account-submit"));
|
const addAccountSubmit = extract((s) => s.ax.find({ id: "add-account-submit" }));
|
||||||
const addTxnButton = extract((s) => s.ax.find("id:add-txn"));
|
const addTxnButton = extract((s) => s.ax.find({ id: "add-txn" }));
|
||||||
const txnAmountField = extract((s) => s.ax.find("id:txn-amount"));
|
const txnAmountField = extract((s) => s.ax.find({ id: "txn-amount" }));
|
||||||
const txnNoteField = extract((s) => s.ax.find("id:txn-note"));
|
const txnNoteField = extract((s) => s.ax.find({ id: "txn-note" }));
|
||||||
const txnCreditButton = extract((s) => s.ax.find("id:txn-credit"));
|
const txnCreditButton = extract((s) => s.ax.find({ id: "txn-credit" }));
|
||||||
const txnDebitButton = extract((s) => s.ax.find("id:txn-debit"));
|
const txnDebitButton = extract((s) => s.ax.find({ id: "txn-debit" }));
|
||||||
const txnSubmit = extract((s) => s.ax.find("id:txn-submit"));
|
const txnSubmit = extract((s) => s.ax.find({ id: "txn-submit" }));
|
||||||
const backButton = extract((s) => s.ax.find("id:back"));
|
const backButton = extract((s) => s.ax.find({ id: "back" }));
|
||||||
|
|
||||||
// -- Properties --
|
// -- Properties --
|
||||||
|
|
||||||
@@ -98,8 +97,6 @@ const loggedOutReachesLogin = always(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Total balance must equal the sum of all account balances visible on home page.
|
|
||||||
// Only checked when home page is active and account cards are loaded.
|
|
||||||
const totalBalanceMatchesAccounts = always(() => {
|
const totalBalanceMatchesAccounts = always(() => {
|
||||||
if (!onHomePage.current) return true;
|
if (!onHomePage.current) return true;
|
||||||
const cards = accountCards.current;
|
const cards = accountCards.current;
|
||||||
@@ -108,8 +105,6 @@ const totalBalanceMatchesAccounts = always(() => {
|
|||||||
return sum === totalBalance.current;
|
return sum === totalBalance.current;
|
||||||
});
|
});
|
||||||
|
|
||||||
// When a transaction is added (ledger txn count increases by 1 on the same
|
|
||||||
// account), the ledger balance delta matches what was actually stored.
|
|
||||||
const balanceMatchesTransactionDelta = always(
|
const balanceMatchesTransactionDelta = always(
|
||||||
now(() => onLedgerPage.current && ledgerTxnCount.current > 0).implies(
|
now(() => onLedgerPage.current && ledgerTxnCount.current > 0).implies(
|
||||||
next(() => {
|
next(() => {
|
||||||
@@ -119,7 +114,6 @@ const balanceMatchesTransactionDelta = always(
|
|||||||
if (curCount !== prevCount + 1) return true;
|
if (curCount !== prevCount + 1) return true;
|
||||||
const prevBal = ledgerBalance.previous ?? 0;
|
const prevBal = ledgerBalance.previous ?? 0;
|
||||||
const curBal = ledgerBalance.current;
|
const curBal = ledgerBalance.current;
|
||||||
// Delta must be non-zero (a valid transaction was added)
|
|
||||||
return curBal !== prevBal;
|
return curBal !== prevBal;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -149,19 +143,7 @@ export const properties = {
|
|||||||
const DEMO_EMAIL = "[email protected]";
|
const DEMO_EMAIL = "[email protected]";
|
||||||
const DEMO_PASSWORD = "ledger123";
|
const DEMO_PASSWORD = "ledger123";
|
||||||
|
|
||||||
const loginHelper = actions(() => {
|
function focusedField(): string | null {
|
||||||
if (loggedIn.current) return [];
|
|
||||||
const email = emailField.current;
|
|
||||||
const password = passwordField.current;
|
|
||||||
const submit = loginSubmit.current;
|
|
||||||
if (!email || !password || !submit) return [];
|
|
||||||
const focused = s_focused();
|
|
||||||
if (focused === "password") return [Tap({ on: submit })];
|
|
||||||
if (focused === "email") return [InputText({ into: password, text: DEMO_PASSWORD })];
|
|
||||||
return [InputText({ into: email, text: DEMO_EMAIL })];
|
|
||||||
});
|
|
||||||
|
|
||||||
function s_focused(): string | null {
|
|
||||||
const email = emailField.current;
|
const email = emailField.current;
|
||||||
const password = passwordField.current;
|
const password = passwordField.current;
|
||||||
if (email && (email as { focused?: boolean }).focused) return "email";
|
if (email && (email as { focused?: boolean }).focused) return "email";
|
||||||
@@ -169,6 +151,18 @@ function s_focused(): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginHelper = actions(() => {
|
||||||
|
if (loggedIn.current) return [];
|
||||||
|
const email = emailField.current;
|
||||||
|
const password = passwordField.current;
|
||||||
|
const submit = loginSubmit.current;
|
||||||
|
if (!email || !password || !submit) return [];
|
||||||
|
const focused = focusedField();
|
||||||
|
if (focused === "password") return [Tap({ on: submit })];
|
||||||
|
if (focused === "email") return [InputText({ into: password, text: DEMO_PASSWORD })];
|
||||||
|
return [InputText({ into: email, text: DEMO_EMAIL })];
|
||||||
|
});
|
||||||
|
|
||||||
const adversarialLogin = actions(() => {
|
const adversarialLogin = actions(() => {
|
||||||
if (loggedIn.current) return [];
|
if (loggedIn.current) return [];
|
||||||
const submit = loginSubmit.current;
|
const submit = loginSubmit.current;
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ export function HomePage(props: {
|
|||||||
<div
|
<div
|
||||||
id="total-balance"
|
id="total-balance"
|
||||||
className="balance-display"
|
className="balance-display"
|
||||||
data-value={totalBalance}
|
data-cents={totalBalance}
|
||||||
title={String(totalBalance)}
|
|
||||||
>
|
>
|
||||||
<span className="balance-label">Total balance</span>
|
<span className="balance-label">Total balance</span>
|
||||||
<span
|
<span
|
||||||
@@ -164,7 +163,7 @@ export function HomePage(props: {
|
|||||||
data-name={a.name}
|
data-name={a.name}
|
||||||
data-balance={bal}
|
data-balance={bal}
|
||||||
data-txn-count={countTxns(txns, a.id)}
|
data-txn-count={countTxns(txns, a.id)}
|
||||||
aria-label={'account:' + a.id + ':' + bal}
|
aria-label={`${a.name}, ${formatCents(bal)}`}
|
||||||
onClick={() => navigate(`/accounts/${a.id}`)}
|
onClick={() => navigate(`/accounts/${a.id}`)}
|
||||||
>
|
>
|
||||||
<span className="account-avatar" aria-hidden="true">
|
<span className="account-avatar" aria-hidden="true">
|
||||||
|
|||||||
@@ -93,13 +93,11 @@ export function LedgerPage(props: {
|
|||||||
data-account-id={props.accountId}
|
data-account-id={props.accountId}
|
||||||
data-account-name={account?.name ?? ''}
|
data-account-name={account?.name ?? ''}
|
||||||
data-txn-count={txns?.length ?? 0}
|
data-txn-count={txns?.length ?? 0}
|
||||||
title={String(txns?.length ?? 0)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="ledger-balance"
|
id="ledger-balance"
|
||||||
className="balance-display"
|
className="balance-display"
|
||||||
data-value={balance}
|
data-cents={balance}
|
||||||
title={String(balance)}
|
|
||||||
>
|
>
|
||||||
<span className="balance-label">Balance</span>
|
<span className="balance-label">Balance</span>
|
||||||
<span
|
<span
|
||||||
|
|||||||
Reference in new issue
Block a user