mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
WIP: rename sample app to Folio (#20)
* refactor: rename examples/sample-app to examples/folio Directory-level rename and path references in Go tests, bundle-check, top-level README, and getting-started docs. Package declarations, Gradle config, iOS bundle IDs, and class names follow in later commits. * refactor(folio): rename Kotlin package dev.uatu.sample to app.folio Moves source dirs and sqldelight schema from dev/uatu/sample to app/folio, updates package declarations and imports, and switches Android namespace/applicationId, iOS binaryOption bundleId, and sqldelight database packageName to the new identifier. * refactor(folio): rename SampleApplication to FolioApplication Android manifest now points at .FolioApplication with label 'Folio' instead of 'Uatu Sample'. * refactor(folio): set iOS bundle id and display name to Folio bundleIdPrefix + PRODUCT_BUNDLE_IDENTIFIER -> app.folio. CFBundleName + CFBundleDisplayName -> 'Folio'. * refactor(folio): update demo email to [email protected] * refactor(folio): point justfile at app.folio bundle id Updates xcrun simctl launch target, uatu test --bundle-id, and the build/uninstall comments to reference folio instead of sample. * test: update fixture package ids to app.folio Sidecar activity-resolver test and verifier spec-integration XML fixtures referenced the old dev.uatu.sample Android package. Updates them to match the folio app's real package id so the tests stay representative of what the CLI sees on-device. * test(verifier): rename SampleApp identifiers to Folio Renames TestSampleAppSpec* functions, bundleSampleAppSpec helper, and sampleAppHierarchyXML const (now loginHierarchyXML for consistency with the other per-screen fixtures). Updates trailing sample-app mentions in comments and assertion messages. * refactor(folio): rename Gradle/npm/wasm project identifiers to folio settings.gradle.kts rootProject.name, package.json + package-lock.json name, and the WasmJS index.html <title> all still read 'uatu-sample' / 'Uatu Sample'. Realigns them with the Folio brand. * docs(folio): rewrite README title + getting-started bundle id examples/folio/README.md is now titled 'Folio' with the Kotlin source paths corrected to app/folio. Getting-started example uses --bundle-id app.folio. Harness launch message is now generic ('app under test') since uatu-sample-harness is not specific to folio. * chore(folio): drop trailing 'sample' reference in gradle.properties * refactor(folio): rename LoginPage composable to LoginScreen Align with KMP/Android industry convention (NowInAndroid, Cash App, JetBrains samples use Screen, not Page). * refactor(folio): rename HomePage composable to HomeScreen * refactor(folio): rename AddAccountPage composable to AddAccountScreen * refactor(folio): rename LedgerPage composable to LedgerScreen * refactor(folio): rename AddTransactionPage composable to AddTransactionScreen * refactor(folio): split Models.kt into app.folio.data package Account, Transaction (with TxnType), and Session move into their own files under app.folio.data, matching NowInAndroid-style per-type organization. * refactor(folio): move data layer into app.folio.data package Repository, LedgerStore (expect + interface), SqlLedgerStore, WebLedgerStore, DriverFactory (expect + actuals), AndroidLedgerContext, and Snapshot move into app.folio.data. Update all consumer imports. * refactor(folio): move Navigation into app.folio.navigation package Split the former Navigation.kt into Route.kt (sealed interface) and Navigator.kt (singleton). Update consumer imports across screens, App.kt, and FolioApplication. * refactor(folio): move Platform and Format into app.folio.platform Both files carry expect declarations (Platform object, formatDate); grouping them into a dedicated platform package makes the KMP seam obvious and mirrors the structure used by JetBrains samples. * refactor(folio): move login into feature/auth package Create app.folio.feature.auth with LoginScreen + LoginUiState. Inline the former Auth.kt (DEMO_EMAIL, DEMO_PASSWORD, checkCredentials) into LoginScreen since it is the sole caller. * refactor(folio): move HomeScreen into feature/home package * refactor(folio): move account creation into feature/account package AddAccountScreen gets its own AddAccountUiState colocated with the screen, replacing the shared UiState.addAccountError. * refactor(folio): move ledger screens into feature/ledger package LedgerScreen and AddTransactionScreen move into app.folio.feature.ledger with AddTransactionUiState (txnError, txnFormType) colocated. The former catch-all UiState.kt is removed now that each screen owns its state alongside its UI. * refactor(folio): split Theme.kt; move theme and icons to subpackages Theme split into Theme.kt (tokens, layout dims, LedgerTheme) and Type.kt (typography) under app.folio.ui.theme. Icons moves to app.folio.ui.icon. Update every consumer's imports to match. * refactor(folio): split ui components into per-file under ui/component Former Widgets.kt and Components.kt become 10 focused files: AppButton, Card, EmptyState, ErrorText, FieldLabel, Header, IconButton (w/ BackButton), Screen, Segmented, TextInput. Matches NowInAndroid style of one composable per file in a designsystem/component package. * chore(folio): consolidate uatu testing files under uatu/ folder Move spec.ts, package.json, package-lock.json into examples/folio/uatu so all uatu-specific testing artifacts live in one place. runs/ and node_modules/ follow the same convention (both remain gitignored). Update justfile, README, and the two Go consumers (bundle-check tool + verifier/trace tests) that referenced the old path. * refactor(trace): drop folio path in writer test Round-trip only needs a non-empty string; neutralize to keep the library free of folio references. * refactor(sidecar): neutralize ResolveActivity test fixtures Swap app.folio for com.example.app in the fixture strings so the sidecar tests don't reference the example app by name. * refactor(bundle-check): take spec path as argument Previously the tool hardcoded examples/folio/uatu/spec.ts. Accept a positional spec path instead so the tool works for any example and leaves no folio reference in the library surface. * test(verifier): add neutral integration spec and hierarchy fixtures Adds testdata/integration_spec.ts with two routes ("list", "form"), an InputText on text_field, a Tap on primary/secondary_action, a safety property (itemCountNonNegative), and a liveness property (submitEventually). Adds hierarchies_test.go with matching XML fixtures. Constants intentionally go in a _test.go at package root rather than testdata/hierarchies.go because go skips .go files under testdata/. * refactor(verifier): replace folio integration tests with neutral ones Renames bundleFolioSpec -> bundleIntegrationSpec and the three Test* entry points to TestIntegrationSpec*. Uses the synthetic spec and hierarchies added in the previous commit so the library's test suite no longer references examples/folio at all. Folio-specific coverage remains covered by examples/folio/justfile's 'just test' which exercises the real spec on device/emulator. * chore: remove cmd/uatu-sample-harness Not referenced by Makefile, docs, CI, or any script. Duplicates the adb reverse helpers already in cmd/uatu/test_run.go, and its name implies ownership by the sample app which violates the library/ example decoupling. If a bare-protocol debugging tool is later needed it belongs inside cmd/uatu/. * docs(folio): drop Layout section and KMP layout paragraph; fix AVD override syntax The directory-tree Layout section rots faster than the code and duplicates what ls shows for free. The expect/actual paragraph in Stack was the same kind of filler. The README also claimed 'just AVD=Pixel_7 test' but the justfile reads AVD as an env var via env_var_or_default, so the correct invocation is 'AVD=Pixel_7 just test'.
This commit is contained in:
88 files changed
+949
-900
No files matched your search
@@ -0,0 +1,19 @@
|
||||
package verifier
|
||||
|
||||
const listHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="test.app" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.widget.TextView" text="Items" bounds="[100,200][900,300]" />
|
||||
<node index="1" class="android.view.View" content-desc="primary_action" clickable="true" enabled="true" bounds="[64,2200][1016,2320]" />
|
||||
<node index="2" class="android.view.View" content-desc="secondary_action" clickable="true" enabled="true" bounds="[980,80][1060,160]" />
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
const formHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="test.app" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.view.View" content-desc="text_field" clickable="true" enabled="true" bounds="[64,320][1016,440]" />
|
||||
<node index="1" class="android.view.View" content-desc="primary_action" clickable="true" enabled="true" bounds="[64,2200][1016,2320]" />
|
||||
<node index="2" class="android.view.View" content-desc="secondary_action" clickable="true" enabled="true" bounds="[32,80][112,160]" />
|
||||
</node>
|
||||
</hierarchy>`
|
||||
@@ -10,62 +10,12 @@ import (
|
||||
"github.com/priyanshujain/uatu/internal/ltl"
|
||||
)
|
||||
|
||||
const sampleAppHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="dev.uatu.sample" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.widget.LinearLayout" bounds="[64,96][1016,2336]">
|
||||
<node index="0" class="android.widget.TextView" text="Sign in" bounds="[100,200][900,300]" />
|
||||
<node index="1" class="android.view.View" content-desc="login_email" clickable="true" enabled="true" bounds="[100,400][900,520]" />
|
||||
<node index="2" class="android.view.View" content-desc="login_password" clickable="true" enabled="true" bounds="[100,560][900,680]" />
|
||||
<node index="3" class="android.view.View" content-desc="login_submit" clickable="true" enabled="true" bounds="[100,800][900,920]" />
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
const homeHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="dev.uatu.sample" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.view.View" content-desc="logout_button" clickable="true" bounds="[980,80][1060,160]" />
|
||||
<node index="1" class="android.view.View" content-desc="account_card:acc-1" clickable="true" bounds="[64,320][1016,440]" />
|
||||
<node index="2" class="android.view.View" content-desc="account_card:acc-2" clickable="true" bounds="[64,460][1016,580]" />
|
||||
<node index="3" class="android.view.View" content-desc="add_account_button" clickable="true" bounds="[64,2200][1016,2320]" />
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
const addAccountHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="dev.uatu.sample" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.view.View" content-desc="Back" clickable="true" bounds="[32,80][112,160]" />
|
||||
<node index="1" class="android.view.View" content-desc="account_name_field" clickable="true" bounds="[64,320][1016,440]" />
|
||||
<node index="2" class="android.view.View" content-desc="add_account_submit" clickable="true" bounds="[64,2200][1016,2320]" />
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
const ledgerHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="dev.uatu.sample" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.view.View" content-desc="Back" clickable="true" bounds="[32,80][112,160]" />
|
||||
<node index="1" class="android.view.View" content-desc="add_txn_button" clickable="true" bounds="[64,2200][1016,2320]" />
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
const addTxnHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="dev.uatu.sample" bounds="[0,0][1080,2400]">
|
||||
<node index="0" class="android.view.View" content-desc="Back" clickable="true" bounds="[32,80][112,160]" />
|
||||
<node index="1" class="android.view.View" content-desc="txn_credit" clickable="true" bounds="[64,280][540,360]" />
|
||||
<node index="2" class="android.view.View" content-desc="txn_debit" clickable="true" bounds="[540,280][1016,360]" />
|
||||
<node index="3" class="android.view.View" content-desc="txn_amount" clickable="true" bounds="[64,440][1016,560]" />
|
||||
<node index="4" class="android.view.View" content-desc="txn_note" clickable="true" bounds="[64,600][1016,720]" />
|
||||
<node index="5" class="android.view.View" content-desc="txn_submit" clickable="true" bounds="[64,2200][1016,2320]" />
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
// bundleSampleAppSpec bundles examples/sample-app/spec.ts via the real
|
||||
// @uatu/spec API so the integration test exercises the same path the CLI uses.
|
||||
func bundleSampleAppSpec(t *testing.T) string {
|
||||
// bundleIntegrationSpec bundles testdata/integration_spec.ts via the real
|
||||
// @uatu/spec API so the integration test exercises the same path the CLI
|
||||
// uses, with no reference to any specific example app.
|
||||
func bundleIntegrationSpec(t *testing.T) string {
|
||||
t.Helper()
|
||||
specPath, err := filepath.Abs("../../examples/sample-app/spec.ts")
|
||||
specPath, err := filepath.Abs("testdata/integration_spec.ts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -90,155 +40,98 @@ func bundleSampleAppSpec(t *testing.T) string {
|
||||
return string(bundle.JavaScript)
|
||||
}
|
||||
|
||||
func loginSnapshots() Snapshots {
|
||||
func listSnapshots() Snapshots {
|
||||
return Snapshots{
|
||||
"route": json.RawMessage(`"login"`),
|
||||
"logged_in": json.RawMessage(`false`),
|
||||
"auth_status": json.RawMessage(`"logged-out"`),
|
||||
"account_count": json.RawMessage(`0`),
|
||||
"accounts": json.RawMessage(`[]`),
|
||||
"total_balance": json.RawMessage(`0`),
|
||||
"active_account_id": json.RawMessage(`null`),
|
||||
"ledger_rows": json.RawMessage(`[]`),
|
||||
"ledger_balance": json.RawMessage(`0`),
|
||||
"focused_input": json.RawMessage(`null`),
|
||||
"txn_form_type": json.RawMessage(`null`),
|
||||
"txn_form_account_id": json.RawMessage(`null`),
|
||||
"login_error": json.RawMessage(`""`),
|
||||
"add_account_error": json.RawMessage(`""`),
|
||||
"txn_error": json.RawMessage(`""`),
|
||||
"route": json.RawMessage(`"list"`),
|
||||
"item_count": json.RawMessage(`0`),
|
||||
"has_submitted": json.RawMessage(`false`),
|
||||
}
|
||||
}
|
||||
|
||||
// twoAccountsJSON is shared between home, ledger, and add-transaction
|
||||
// snapshots so invariants that correlate accounts with ledger rows stay
|
||||
// consistent across routes.
|
||||
const twoAccountsJSON = `[` +
|
||||
`{"id":"acc-1","name":"Checking","balance":0,"txnCount":0},` +
|
||||
`{"id":"acc-2","name":"Savings","balance":0,"txnCount":0}` +
|
||||
`]`
|
||||
|
||||
func homeSnapshots() Snapshots {
|
||||
func formSnapshots() Snapshots {
|
||||
return Snapshots{
|
||||
"route": json.RawMessage(`"home"`),
|
||||
"logged_in": json.RawMessage(`true`),
|
||||
"auth_status": json.RawMessage(`"logged-in"`),
|
||||
"account_count": json.RawMessage(`2`),
|
||||
"accounts": json.RawMessage(twoAccountsJSON),
|
||||
"total_balance": json.RawMessage(`0`),
|
||||
"active_account_id": json.RawMessage(`null`),
|
||||
"ledger_rows": json.RawMessage(`[]`),
|
||||
"ledger_balance": json.RawMessage(`0`),
|
||||
"focused_input": json.RawMessage(`null`),
|
||||
"txn_form_type": json.RawMessage(`null`),
|
||||
"txn_form_account_id": json.RawMessage(`null`),
|
||||
"login_error": json.RawMessage(`""`),
|
||||
"add_account_error": json.RawMessage(`""`),
|
||||
"txn_error": json.RawMessage(`""`),
|
||||
"route": json.RawMessage(`"form"`),
|
||||
"item_count": json.RawMessage(`0`),
|
||||
"has_submitted": json.RawMessage(`false`),
|
||||
}
|
||||
}
|
||||
|
||||
func addAccountSnapshots() Snapshots {
|
||||
s := homeSnapshots()
|
||||
s["route"] = json.RawMessage(`"add-account"`)
|
||||
return s
|
||||
}
|
||||
|
||||
func ledgerSnapshots() Snapshots {
|
||||
s := homeSnapshots()
|
||||
s["route"] = json.RawMessage(`"ledger"`)
|
||||
s["active_account_id"] = json.RawMessage(`"acc-1"`)
|
||||
return s
|
||||
}
|
||||
|
||||
func addTxnSnapshots() Snapshots {
|
||||
s := ledgerSnapshots()
|
||||
s["route"] = json.RawMessage(`"add-transaction"`)
|
||||
s["txn_form_type"] = json.RawMessage(`"credit"`)
|
||||
s["txn_form_account_id"] = json.RawMessage(`"acc-1"`)
|
||||
return s
|
||||
}
|
||||
|
||||
// TestSampleAppSpecFiresLoginActions verifies the bundled sample-app spec
|
||||
// emits Tap actions targeting the login screen elements when they are present
|
||||
// in the hierarchy.
|
||||
func TestSampleAppSpecFiresLoginActions(t *testing.T) {
|
||||
// TestIntegrationSpecFiresInputActions verifies the bundled neutral spec
|
||||
// emits an InputText action on the text field and a Tap action on the
|
||||
// primary button when both are present in the hierarchy.
|
||||
func TestIntegrationSpecFiresInputActions(t *testing.T) {
|
||||
v := newVerifier(t)
|
||||
if err := v.Load(bundleSampleAppSpec(t)); err != nil {
|
||||
if err := v.Load(bundleIntegrationSpec(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tree, err := hierarchy.Parse(sampleAppHierarchyXML)
|
||||
tree, err := hierarchy.Parse(formHierarchyXML)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.PushSnapshot(SnapshotInput{Snapshots: loginSnapshots(), Tree: tree}); err != nil {
|
||||
if err := v.PushSnapshot(SnapshotInput{Snapshots: formSnapshots(), Tree: tree}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
typeEmailHits := 0
|
||||
tapSubmitHits := 0
|
||||
typeFieldHits := 0
|
||||
tapPrimaryHits := 0
|
||||
for range 400 {
|
||||
action, err := v.NextAction()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case action.Kind == ActionKindInputText && action.On == "desc:login_email":
|
||||
typeEmailHits++
|
||||
case action.Kind == ActionKindTap && action.On == "desc:login_submit":
|
||||
tapSubmitHits++
|
||||
case action.Kind == ActionKindInputText && action.On == "desc:text_field":
|
||||
typeFieldHits++
|
||||
case action.Kind == ActionKindTap && action.On == "desc:primary_action":
|
||||
tapPrimaryHits++
|
||||
}
|
||||
}
|
||||
if typeEmailHits == 0 {
|
||||
t.Fatal("loginHelper never typed into desc:login_email on sample-app hierarchy")
|
||||
if typeFieldHits == 0 {
|
||||
t.Fatal("typeIntoField never typed into desc:text_field on form hierarchy")
|
||||
}
|
||||
if tapSubmitHits == 0 {
|
||||
t.Fatal("adversarialLogin never tapped desc:login_submit on sample-app hierarchy")
|
||||
if tapPrimaryHits == 0 {
|
||||
t.Fatal("tapPrimary never tapped desc:primary_action on form hierarchy")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSampleAppSpecPropertiesEvaluate checks the properties declared in the
|
||||
// sample-app spec evaluate sensibly across a small snapshot sequence. The
|
||||
// TestIntegrationSpecPropertiesEvaluate checks the properties declared in
|
||||
// the neutral spec evaluate sensibly across a small snapshot sequence. The
|
||||
// spec mixes safety and liveness properties; Pending verdicts are expected
|
||||
// for liveness properties that haven't had time to resolve yet.
|
||||
func TestSampleAppSpecPropertiesEvaluate(t *testing.T) {
|
||||
func TestIntegrationSpecPropertiesEvaluate(t *testing.T) {
|
||||
v := newVerifier(t)
|
||||
if err := v.Load(bundleSampleAppSpec(t)); err != nil {
|
||||
if err := v.Load(bundleIntegrationSpec(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tree, err := hierarchy.Parse(sampleAppHierarchyXML)
|
||||
tree, err := hierarchy.Parse(listHierarchyXML)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.PushSnapshot(SnapshotInput{Snapshots: loginSnapshots(), Tree: tree}); err != nil {
|
||||
if err := v.PushSnapshot(SnapshotInput{Snapshots: listSnapshots(), Tree: tree}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verdicts := v.EvaluateProperties()
|
||||
if verdicts["accountCountNonNegative"] != ltl.VerdictHolds {
|
||||
t.Errorf("accountCountNonNegative: got %v, want holds", verdicts["accountCountNonNegative"])
|
||||
}
|
||||
if verdicts["noUncaughtExceptions"] != ltl.VerdictHolds {
|
||||
t.Errorf("noUncaughtExceptions: got %v, want holds", verdicts["noUncaughtExceptions"])
|
||||
}
|
||||
if verdicts["authStatusIsKnown"] != ltl.VerdictHolds {
|
||||
t.Errorf("authStatusIsKnown: got %v, want holds", verdicts["authStatusIsKnown"])
|
||||
if verdicts["itemCountNonNegative"] != ltl.VerdictHolds {
|
||||
t.Errorf("itemCountNonNegative: got %v, want holds", verdicts["itemCountNonNegative"])
|
||||
}
|
||||
if verdicts["routeIsKnown"] != ltl.VerdictHolds {
|
||||
t.Errorf("routeIsKnown: got %v, want holds", verdicts["routeIsKnown"])
|
||||
}
|
||||
// Liveness: loginReachable hasn't resolved yet.
|
||||
if verdicts["loginReachable"] != ltl.VerdictPending {
|
||||
t.Errorf("loginReachable: got %v, want pending", verdicts["loginReachable"])
|
||||
if verdicts["noUncaughtExceptions"] != ltl.VerdictHolds {
|
||||
t.Errorf("noUncaughtExceptions: got %v, want holds", verdicts["noUncaughtExceptions"])
|
||||
}
|
||||
// Liveness: submitEventually hasn't resolved yet.
|
||||
if verdicts["submitEventually"] != ltl.VerdictPending {
|
||||
t.Errorf("submitEventually: got %v, want pending", verdicts["submitEventually"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestSampleAppSpecActionsFireOnEachRoute pushes a hierarchy + snapshot pair
|
||||
// representative of each sample-app route and verifies the expected action
|
||||
// generators fire against that state. Guards against silent breakage of any
|
||||
// one route's generators (a regression only e2e would otherwise catch).
|
||||
func TestSampleAppSpecActionsFireOnEachRoute(t *testing.T) {
|
||||
// TestIntegrationSpecActionsFireOnEachRoute pushes a hierarchy + snapshot
|
||||
// pair representative of each route and verifies the expected action
|
||||
// generators fire against that state.
|
||||
func TestIntegrationSpecActionsFireOnEachRoute(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
xml string
|
||||
@@ -247,47 +140,25 @@ func TestSampleAppSpecActionsFireOnEachRoute(t *testing.T) {
|
||||
expectOns []string
|
||||
}{
|
||||
{
|
||||
name: "home",
|
||||
xml: homeHierarchyXML,
|
||||
snapshots: homeSnapshots(),
|
||||
name: "list",
|
||||
xml: listHierarchyXML,
|
||||
snapshots: listSnapshots(),
|
||||
expectKind: ActionKindTap,
|
||||
expectOns: []string{
|
||||
"desc:add_account_button",
|
||||
"desc:logout_button",
|
||||
"descPrefix:account_card:",
|
||||
},
|
||||
expectOns: []string{"desc:primary_action", "desc:secondary_action"},
|
||||
},
|
||||
{
|
||||
name: "add-account",
|
||||
xml: addAccountHierarchyXML,
|
||||
snapshots: addAccountSnapshots(),
|
||||
name: "form",
|
||||
xml: formHierarchyXML,
|
||||
snapshots: formSnapshots(),
|
||||
expectKind: ActionKindTap,
|
||||
expectOns: []string{"desc:add_account_submit", "desc:Back"},
|
||||
},
|
||||
{
|
||||
name: "ledger",
|
||||
xml: ledgerHierarchyXML,
|
||||
snapshots: ledgerSnapshots(),
|
||||
expectKind: ActionKindTap,
|
||||
expectOns: []string{"desc:add_txn_button", "desc:Back"},
|
||||
},
|
||||
{
|
||||
name: "add-transaction",
|
||||
xml: addTxnHierarchyXML,
|
||||
snapshots: addTxnSnapshots(),
|
||||
expectKind: ActionKindTap,
|
||||
expectOns: []string{
|
||||
"desc:txn_submit",
|
||||
"desc:txn_debit",
|
||||
"desc:Back",
|
||||
},
|
||||
expectOns: []string{"desc:primary_action", "desc:secondary_action"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
v := newVerifier(t)
|
||||
if err := v.Load(bundleSampleAppSpec(t)); err != nil {
|
||||
if err := v.Load(bundleIntegrationSpec(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err := hierarchy.Parse(tc.xml)
|
||||
@@ -316,16 +187,8 @@ func TestSampleAppSpecActionsFireOnEachRoute(t *testing.T) {
|
||||
t.Errorf("%s: no %s action on %q; saw %v", tc.name, tc.expectKind, on, keysOf(sawOn))
|
||||
}
|
||||
}
|
||||
if tc.name == "add-account" && !sawInputText["desc:account_name_field"] {
|
||||
t.Errorf("add-account: typeAccountName never fired; saw %v", keysOf(sawInputText))
|
||||
}
|
||||
if tc.name == "add-transaction" {
|
||||
if !sawInputText["desc:txn_amount"] {
|
||||
t.Errorf("add-transaction: typeAmount never fired; saw %v", keysOf(sawInputText))
|
||||
}
|
||||
if !sawInputText["desc:txn_note"] {
|
||||
t.Errorf("add-transaction: typeNote never fired; saw %v", keysOf(sawInputText))
|
||||
}
|
||||
if tc.name == "form" && !sawInputText["desc:text_field"] {
|
||||
t.Errorf("form: typeIntoField never fired; saw %v", keysOf(sawInputText))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
InputText,
|
||||
Tap,
|
||||
actions,
|
||||
always,
|
||||
eventually,
|
||||
extract,
|
||||
weighted,
|
||||
} from "@uatu/spec";
|
||||
import { noUncaughtExceptions } from "@uatu/spec/defaults/properties";
|
||||
|
||||
const route = extract<string>(
|
||||
(state) => (state.snapshots.route as string) ?? "",
|
||||
);
|
||||
const itemCount = extract<number>(
|
||||
(state) => (state.snapshots.item_count as number) ?? 0,
|
||||
);
|
||||
const hasSubmitted = extract<boolean>(
|
||||
(state) => (state.snapshots.has_submitted as boolean) ?? false,
|
||||
);
|
||||
|
||||
const primaryAction = extract((state) => state.ax.find("desc:primary_action"));
|
||||
const secondaryAction = extract((state) =>
|
||||
state.ax.find("desc:secondary_action"),
|
||||
);
|
||||
const textField = extract((state) => state.ax.find("desc:text_field"));
|
||||
|
||||
const itemCountNonNegative = always(() => itemCount.current >= 0);
|
||||
const routeIsKnown = always(
|
||||
() => route.current === "list" || route.current === "form",
|
||||
);
|
||||
const submitEventually = eventually(() => hasSubmitted.current).within(
|
||||
30,
|
||||
"seconds",
|
||||
);
|
||||
|
||||
const typeIntoField = actions(() => {
|
||||
if (route.current !== "form") return [];
|
||||
const field = textField.current;
|
||||
if (!field) return [];
|
||||
return [InputText({ into: field, text: "hello" })];
|
||||
});
|
||||
|
||||
const tapPrimary = actions(() => {
|
||||
const button = primaryAction.current;
|
||||
return button ? [Tap({ on: button })] : [];
|
||||
});
|
||||
|
||||
const tapSecondary = actions(() => {
|
||||
const button = secondaryAction.current;
|
||||
return button ? [Tap({ on: button })] : [];
|
||||
});
|
||||
|
||||
export const properties = {
|
||||
itemCountNonNegative,
|
||||
routeIsKnown,
|
||||
submitEventually,
|
||||
noUncaughtExceptions,
|
||||
};
|
||||
|
||||
export const actionsRoot = weighted(
|
||||
[40, typeIntoField],
|
||||
[30, tapPrimary],
|
||||
[30, tapSecondary],
|
||||
);
|
||||
|
||||
(globalThis as { actions?: unknown; properties?: unknown }).actions = actionsRoot;
|
||||
(globalThis as { properties?: unknown }).properties = properties;
|
||||
Reference in new issue
Block a user