mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(spec): merchant-ledger property + login/navigation generators
Property ledgerBalanceMatchesTxns asserts displayed balance equals sum(Given) - sum(Received) on either ledger screen. Catches the class of bug where the server-fed CustomerModel.balance diverges from the local-DB-fed CoreDatabaseDao sums (stale cache, partial sync, deleted-txn handling glitch, etc.). Generators are gated by screen state and weighted to push the run through login -> home -> ledger quickly: enterPhone (100), enterOtp (100), openCustomerOrSupplier (80), taps (10), swipes (2). Phone/OTP read from process.env via esbuild defines so credentials never land in source. cmd/internal-tools/bundle-check is a quick sanity tool to confirm the spec bundles before running uatu test.
This commit is contained in:
1 parent
7aa75f3d9f
commit
4dbad9073e
2 files changed
+130
No files matched your search
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/priyanshujain/uatu/internal/bundler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
repoRoot, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "getwd: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
specApiPath := filepath.Join(repoRoot, "pkg/spec-api/src/index.ts")
|
||||
|
||||
result, err := bundler.Bundle(bundler.Options{
|
||||
EntryFile: "examples/specs/merchant-ledger.ts",
|
||||
Defines: map[string]string{
|
||||
"UATU_TEST_PHONE": "+910000000000",
|
||||
"UATU_TEST_OTP": "000000",
|
||||
},
|
||||
Aliases: map[string]string{
|
||||
"@uatu/spec": specApiPath,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bundle: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("bundled: %d bytes, sha256=%s\n", len(result.JavaScript), result.SHA256)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
extract,
|
||||
always,
|
||||
actions,
|
||||
weighted,
|
||||
Tap,
|
||||
InputText,
|
||||
taps,
|
||||
swipes,
|
||||
} from "@uatu/spec";
|
||||
import type { State } from "@uatu/spec";
|
||||
|
||||
// ── Snapshot extractors ────────────────────────────────────────
|
||||
const screen = extract<string>((state) => (state.snapshots.screen as string) ?? "");
|
||||
const balance = extract<number>(
|
||||
(state) => (state.snapshots["ledger.balance"] as number) ?? 0,
|
||||
);
|
||||
const totalGiven = extract<number>(
|
||||
(state) => (state.snapshots["ledger.totalGiven"] as number) ?? 0,
|
||||
);
|
||||
const totalReceived = extract<number>(
|
||||
(state) => (state.snapshots["ledger.totalReceived"] as number) ?? 0,
|
||||
);
|
||||
|
||||
// ── Screen detection via accessibility tree ────────────────────
|
||||
const onLoginPhone = extract<boolean>((state) =>
|
||||
Boolean(state.ax.find("id:login_phone_field")),
|
||||
);
|
||||
const onLoginOtp = extract<boolean>((state) =>
|
||||
Boolean(state.ax.find("id:login_otp_field")),
|
||||
);
|
||||
const onHome = extract<boolean>(
|
||||
(state) =>
|
||||
state.ax.findAll("id:home_customer_row").length > 0 ||
|
||||
state.ax.findAll("id:home_supplier_row").length > 0,
|
||||
);
|
||||
const onLedger = extract<boolean>(
|
||||
() => screen.current === "customer_ledger" || screen.current === "supplier_ledger",
|
||||
);
|
||||
|
||||
// ── Properties ─────────────────────────────────────────────────
|
||||
export const properties = {
|
||||
// The displayed balance is server-fed; the totals are local-DB-fed.
|
||||
// Any divergence — stale cache, partial sync, optimistic-update glitch,
|
||||
// mishandled deleted txn — is a violation.
|
||||
ledgerBalanceMatchesTxns: always(
|
||||
() =>
|
||||
!onLedger.current ||
|
||||
balance.current === totalGiven.current - totalReceived.current,
|
||||
),
|
||||
};
|
||||
|
||||
// ── Action generators (gated by screen state) ──────────────────
|
||||
const enterPhone = actions(() =>
|
||||
onLoginPhone.current
|
||||
? [
|
||||
InputText({
|
||||
into: "id:login_phone_field",
|
||||
text: process.env.UATU_TEST_PHONE ?? "",
|
||||
}),
|
||||
Tap({ on: "id:login_continue" }),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
const enterOtp = actions(() =>
|
||||
onLoginOtp.current
|
||||
? [
|
||||
InputText({
|
||||
into: "id:login_otp_field",
|
||||
text: process.env.UATU_TEST_OTP ?? "",
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
const openCustomerOrSupplier = actions((): ReturnType<typeof Tap>[] => {
|
||||
if (!onHome.current) return [];
|
||||
// The verifier resolves the selector; we just hand back a list of
|
||||
// candidate row taps weighted equally by the runtime's pickFromResult.
|
||||
return [
|
||||
Tap({ on: "id:home_customer_row" }),
|
||||
Tap({ on: "id:home_supplier_row" }),
|
||||
];
|
||||
});
|
||||
|
||||
export const actionsRoot = weighted(
|
||||
[100, enterPhone],
|
||||
[100, enterOtp],
|
||||
[80, openCustomerOrSupplier],
|
||||
[10, taps],
|
||||
[2, swipes],
|
||||
);
|
||||
|
||||
// Verifier looks for `globalThis.actions`; re-export the weighted root.
|
||||
(globalThis as { actions?: unknown }).actions = actionsRoot;
|
||||
Reference in new issue
Block a user