@terreno/agent-plugin 0.0.0-stage → 58.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +2213 -0
- package/LICENSE +21 -0
- package/README.md +57 -2
- package/agents/pre-commit.md +65 -0
- package/agents/ui-verifier.md +93 -0
- package/package.json +32 -4
- package/references/async-review-bots.md +76 -0
- package/references/documentation-contract.md +61 -0
- package/references/execution-state.schema.json +77 -0
- package/references/github-attention-contract.md +165 -0
- package/references/independent-review.md +31 -0
- package/references/lifecycle-contract.md +211 -0
- package/references/loop-engineering.md +125 -0
- package/references/pick-roast-loop.md +112 -0
- package/references/pr-deployments.md +69 -0
- package/references/product-ci.md +164 -0
- package/references/push-ownership.md +91 -0
- package/references/stage-result.schema.json +82 -0
- package/references/subagent-briefing.md +43 -0
- package/references/testing-plan.md +163 -0
- package/references/user-guide.md +171 -0
- package/skills/1-grow/SKILL.md +145 -0
- package/skills/1-grow/references/grilling.md +243 -0
- package/skills/2-pick/SKILL.md +170 -0
- package/skills/2-pick/references/mocking.md +34 -0
- package/skills/2-pick/references/testing.md +43 -0
- package/skills/3-roast/SKILL.md +168 -0
- package/skills/4-brew/SKILL.md +201 -0
- package/skills/5-taste/SKILL.md +311 -0
- package/skills/ai-prompt-governance/SKILL.md +100 -0
- package/skills/attach-pr-media/SKILL.md +89 -0
- package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
- package/skills/backend-test-env/SKILL.md +64 -0
- package/skills/backfill-regression/SKILL.md +101 -0
- package/skills/backfill-user-guide/SKILL.md +96 -0
- package/skills/bootstrap-mcp-server/SKILL.md +244 -0
- package/skills/building-admin-interfaces/SKILL.md +86 -0
- package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
- package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
- package/skills/building-admin-interfaces/references/patterns-and-tools.md +74 -0
- package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
- package/skills/building-terreno-apps/SKILL.md +294 -0
- package/skills/building-terreno-apps/agents/openai.yaml +4 -0
- package/skills/building-terreno-apps/references/navigation.md +108 -0
- package/skills/building-terreno-apps/references/route-structure.md +104 -0
- package/skills/building-terreno-apps/references/screen-patterns.md +174 -0
- package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
- package/skills/deploy-gcp/SKILL.md +73 -0
- package/skills/deploy-gcp/references/troubleshooting.md +29 -0
- package/skills/generate-release-announcements/SKILL.md +145 -0
- package/skills/generate-sdk/SKILL.md +165 -0
- package/skills/mongoose-schema-safety/SKILL.md +164 -0
- package/skills/pick-roast-loop/SKILL.md +152 -0
- package/skills/planning-loop/SKILL.md +161 -0
- package/skills/prd/SKILL.md +138 -0
- package/skills/set-up-social-login/SKILL.md +76 -0
- package/skills/store-assets/SKILL.md +221 -0
- package/skills/store-assets/examples/screenshots.yaml +31 -0
- package/skills/store-assets/examples/storeAssets.config.json +44 -0
- package/skills/store-assets/scripts/banner.ts +165 -0
- package/skills/store-assets/scripts/config.ts +255 -0
- package/skills/store-assets/scripts/devClient.ts +635 -0
- package/skills/store-assets/scripts/devices.ts +286 -0
- package/skills/store-assets/scripts/icon.ts +81 -0
- package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
- package/skills/store-assets/scripts/maestro/login.yaml +27 -0
- package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
- package/skills/store-assets/scripts/screenshots.ts +848 -0
- package/skills/taste-sweep/SKILL.md +172 -0
- package/skills/terreno-backend-api/SKILL.md +355 -0
- package/skills/terreno-backend-api/agents/openai.yaml +4 -0
- package/skills/terreno-backend-api/references/custom-routes.md +132 -0
- package/skills/terreno-backend-api/references/model-router.md +112 -0
- package/skills/terreno-backend-api/references/server-setup.md +116 -0
- package/skills/terreno-backend-tests/SKILL.md +176 -0
- package/skills/terreno-data-fetching/SKILL.md +288 -0
- package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
- package/skills/terreno-data-fetching/references/auth-and-tokens.md +89 -0
- package/skills/terreno-data-fetching/references/realtime.md +50 -0
- package/skills/terreno-data-fetching/references/sdk-customization.md +55 -0
- package/skills/terreno-frontend-tests/SKILL.md +190 -0
- package/skills/terreno-mobile-tests/SKILL.md +127 -0
- package/skills/terreno-test-setup/SKILL.md +81 -0
- package/skills/terreno-test-setup/references/backend.md +131 -0
- package/skills/terreno-test-setup/references/frontend.md +169 -0
- package/skills/terreno-test-setup/references/mobile.md +134 -0
- package/skills/terreno-ui/SKILL.md +252 -0
- package/skills/terreno-ui/agents/openai.yaml +4 -0
- package/skills/terreno-ui/references/data-display.md +87 -0
- package/skills/terreno-ui/references/feedback.md +122 -0
- package/skills/terreno-ui/references/forms.md +119 -0
- package/skills/terreno-ui/references/layout.md +124 -0
- package/skills/terreno-ui/references/theming.md +87 -0
- package/skills/update-docs/SKILL.md +82 -0
- package/skills/upgrading-terreno/SKILL.md +93 -0
- package/skills/upgrading-terreno/references/ordering.md +40 -0
- package/skills/verify-ui-changes/SKILL.md +605 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Auth and Tokens — Terreno
|
|
2
|
+
|
|
3
|
+
Session state lives in `@terreno/syncdb/auth`. There is no Redux store. Reference:
|
|
4
|
+
`docs/reference/syncdb.md#auth`, `example-frontend/lib/betterAuth.ts`, `app/login.tsx`.
|
|
5
|
+
|
|
6
|
+
## Better Auth client
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
// lib/betterAuth.ts
|
|
10
|
+
import {createBetterAuthClient} from "@terreno/syncdb/auth";
|
|
11
|
+
import {baseUrl} from "@terreno/syncdb/config";
|
|
12
|
+
|
|
13
|
+
export const betterAuthClient = createBetterAuthClient({
|
|
14
|
+
baseURL: baseUrl,
|
|
15
|
+
scheme: "myapp",
|
|
16
|
+
storagePrefix: "myapp",
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Storage is `expo-secure-store` on native and AsyncStorage on web. Never store tokens in
|
|
21
|
+
plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
|
|
22
|
+
|
|
23
|
+
## Session provider
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
|
|
27
|
+
<App />
|
|
28
|
+
</SessionProvider>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import {useSession} from "@terreno/syncdb/auth";
|
|
33
|
+
|
|
34
|
+
const {userId, user, isAuthenticated, isLoading, error, logout, refresh} = useSession();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Login
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
const {refresh} = useSession();
|
|
41
|
+
|
|
42
|
+
const result = await betterAuthClient.signIn.email({email, password});
|
|
43
|
+
if (!result.error) {
|
|
44
|
+
await refresh(); // resolve userId now instead of waiting for the session atom
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `SocialLoginButton` from `@terreno/ui` with `betterAuthClient.signIn.social({provider})`
|
|
49
|
+
for OAuth.
|
|
50
|
+
|
|
51
|
+
## Logout
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const {logout} = useSession();
|
|
55
|
+
await logout();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`logout()` signs out each `syncDbs` client first (wiping local rows when created with
|
|
59
|
+
`wipeOnSignOut: true`), then the auth server, then runs `onLogout`.
|
|
60
|
+
|
|
61
|
+
## Token refresh
|
|
62
|
+
|
|
63
|
+
`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))` is the shared
|
|
64
|
+
`AuthProvider` for `createSyncDb` and `createApiClient`. The request client adds
|
|
65
|
+
`Authorization: Bearer`, and on a 401 calls `authProvider.refresh()` once and retries.
|
|
66
|
+
No manual refresh logic in app code. Code outside the client (SSE streams, `AdminProvider`
|
|
67
|
+
`getAuthHeaders`) reads `authProvider.getToken()`.
|
|
68
|
+
|
|
69
|
+
## JWT backends (legacy)
|
|
70
|
+
|
|
71
|
+
Apps on `@terreno/api` JWT routes use `jwtAuthAdapter` as both the `authProvider` and the
|
|
72
|
+
`SessionProvider` `source`, plus `useJwtAuth({adapter})` for `login`, `signup`,
|
|
73
|
+
`googleLogin`, `resetPassword`. It refreshes when under 130 s remain and on a 401, with one
|
|
74
|
+
shared refresh in flight. See `docs/reference/syncdb.md#jwt-auth-legacy`.
|
|
75
|
+
|
|
76
|
+
## Current user
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import {useGetAuthMeQuery} from "@/store/apiSdk";
|
|
80
|
+
|
|
81
|
+
const {userId} = useSession();
|
|
82
|
+
const {data: me} = useGetAuthMeQuery(undefined, {skip: !userId});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Protected routes
|
|
86
|
+
|
|
87
|
+
Gate navigation on `useSession().userId` in root `_layout.tsx`. Redirect unauthenticated
|
|
88
|
+
users to `/login`. For admin routes, check `canOpenAdminPage({admin: me?.admin, permissions:
|
|
89
|
+
me?.permissions})` from `@terreno/syncdb/app`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Realtime — Terreno
|
|
2
|
+
|
|
3
|
+
The syncdb client owns the app's only socket. Do not open a second one.
|
|
4
|
+
|
|
5
|
+
## Server push events
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import {useSocketEvent} from "@terreno/syncdb/react";
|
|
9
|
+
|
|
10
|
+
useSocketEvent("announcementPublished", (payload) => {
|
|
11
|
+
console.info("announcement", payload);
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Inside `SyncDbProvider` the hook uses that client; above it, pass `{client: syncDb}`, or
|
|
16
|
+
`{client: null}` to listen to nothing. Outside React: `syncDb.onSocketEvent(event, handler)`
|
|
17
|
+
returns an unsubscribe.
|
|
18
|
+
|
|
19
|
+
## Feature flags
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {OpenFeatureProvider} from "@openfeature/react-sdk";
|
|
23
|
+
import {useTerrenoFeatureFlags} from "@terreno/syncdb/flags";
|
|
24
|
+
|
|
25
|
+
const {userId} = useSession();
|
|
26
|
+
useTerrenoFeatureFlags({client: apiClient, skip: !userId, socket: syncDb, userId});
|
|
27
|
+
return <OpenFeatureProvider domain="feature-flags">{children}</OpenFeatureProvider>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Flags load through the request client and refetch when the server pushes
|
|
31
|
+
`featureFlagsChanged` on the sync socket. Screens can read flat accessors with
|
|
32
|
+
`useFeatureFlags({skip, socket, userId})`. See `example-frontend/app/_layout.tsx` and
|
|
33
|
+
`docs/reference/feature-flags.md`.
|
|
34
|
+
|
|
35
|
+
## Sync status
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import {useSyncStatus} from "@terreno/syncdb/react";
|
|
39
|
+
|
|
40
|
+
const {isOnline, queuedCount, conflictCount, failedCount, paused} = useSyncStatus();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Render `SyncStatusBanner` / `OfflineBanner` from `@terreno/ui` with these values.
|
|
44
|
+
|
|
45
|
+
## Realtime model updates
|
|
46
|
+
|
|
47
|
+
Live collection updates come from `sync` on the backend `modelRouter`; read them with
|
|
48
|
+
`useQuery` / `useEntity`. `RealtimeApp` hosts the socket for `sync:delta` and app events.
|
|
49
|
+
Terreno 58 removed `modelRouter` `realtime` and every `@terreno/rtk` socket helper
|
|
50
|
+
(`useSocketConnection`, `realtimeList`, `realtimeDocument`, `useRealtimeDebug`).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SDK Customization — Terreno
|
|
2
|
+
|
|
3
|
+
## Generated vs hand-maintained
|
|
4
|
+
|
|
5
|
+
| File | Editable? | Purpose |
|
|
6
|
+
|------|-----------|---------|
|
|
7
|
+
| `store/apiSdk.ts` | **Never** | Typed hooks for every non-synced OpenAPI operation (`--api-out`) |
|
|
8
|
+
| `store/syncDbSdk.ts` | **Never** | `SYNC_COLLECTIONS` + collection hooks (`--out`) |
|
|
9
|
+
| `store/apiClient.ts` | **Yes** | `createApiClient` (auth, headers, `onError`) |
|
|
10
|
+
| `syncdb-codegen.json` | **Yes** | Codegen config (`excludePaths`, per-collection `overrides`) |
|
|
11
|
+
|
|
12
|
+
## Generated hooks
|
|
13
|
+
|
|
14
|
+
Names follow the old RTK codegen rule: camelCased `operationId`, otherwise method + path with
|
|
15
|
+
`{param}` as `By<Param>` (`PATCH /gpt/histories/{id}` → `usePatchGptHistoriesByIdMutation`).
|
|
16
|
+
Each operation also gets `<Name>Args`, `<Name>Res`, and `<name>Request(arg)` for
|
|
17
|
+
`apiClient.request(...)`. Tags come from the operation's OpenAPI tags.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Routes outside OpenAPI
|
|
24
|
+
|
|
25
|
+
Prefer documenting the route in OpenAPI and regenerating. When that is not possible, call it
|
|
26
|
+
directly through the request client:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {useApiMutation, useApiQuery, useLazyApiQuery} from "@terreno/syncdb/react";
|
|
30
|
+
|
|
31
|
+
const {data, isLoading, refetch} = useApiQuery<Report>(
|
|
32
|
+
{url: "/reports/summary", params: {range}},
|
|
33
|
+
{providesTags: ["reports"], skip: !userId}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const [rebuild, {isLoading: isRebuilding}] = useApiMutation<RebuildBody, Report>(
|
|
37
|
+
(body) => ({body, method: "POST", url: "/reports/rebuild"}),
|
|
38
|
+
{invalidatesTags: ["reports"]}
|
|
39
|
+
);
|
|
40
|
+
await rebuild({range}).unwrap();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Cache tags
|
|
44
|
+
|
|
45
|
+
Queries register `providesTags`; mutations `invalidatesTags`. Imperatively:
|
|
46
|
+
`apiClient.invalidateTags(["reports"])`. `apiClient.reset()` clears the cache (done on logout
|
|
47
|
+
via `SessionProvider` `onLogout`). Synced collections have no request cache; their data is local.
|
|
48
|
+
|
|
49
|
+
## Workflow after backend change
|
|
50
|
+
|
|
51
|
+
1. Start backend (`bun run backend:dev`)
|
|
52
|
+
2. Run `cd example-frontend && bun run sdk` (and `bun run sync-sdk` for synced schema changes)
|
|
53
|
+
3. Import new hooks from `@/store/apiSdk` (or `@/store/syncDbSdk`)
|
|
54
|
+
|
|
55
|
+
See the `generate-sdk` skill for the full procedure.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: terreno-frontend-tests
|
|
3
|
+
description: >-
|
|
4
|
+
Write and fix frontend tests for a Terreno Expo app: Bun + React Native Testing Library unit
|
|
5
|
+
tests for components, screens, hooks, Redux/RTK and syncdb data, and Playwright web E2E for
|
|
6
|
+
flows across screens and the backend. Covers renderWithTerreno, testID selectors, mocking
|
|
7
|
+
data hooks without leaking module mocks, async waiting, and Playwright login/fixtures. Use
|
|
8
|
+
when adding or changing frontend behavior or when a frontend test fails. If the app has no
|
|
9
|
+
test preload or Playwright config, run terreno-test-setup first.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Writing Terreno frontend tests
|
|
13
|
+
|
|
14
|
+
## 1. Pick the layer
|
|
15
|
+
|
|
16
|
+
| Behavior | Layer |
|
|
17
|
+
|----------|-------|
|
|
18
|
+
| Pure function, reducer, selector, formatter | Bun unit test, no rendering |
|
|
19
|
+
| Component or screen renders states and reacts to input | Bun + React Native Testing Library |
|
|
20
|
+
| Custom hook | `renderHook` with `createTerrenoWrapper` |
|
|
21
|
+
| Flow across screens, real backend, auth, navigation, persistence | Playwright web E2E |
|
|
22
|
+
| Native-only behavior (gestures, push, camera, deep links on device) | `terreno-mobile-tests` |
|
|
23
|
+
|
|
24
|
+
Prefer the lowest layer that proves the behavior; add one E2E per user-visible flow, not per
|
|
25
|
+
edge case.
|
|
26
|
+
|
|
27
|
+
Done when: the test sits next to the code (`TodoList.tsx` → `TodoList.test.tsx`; E2E in
|
|
28
|
+
`e2e/<feature>.spec.ts`).
|
|
29
|
+
|
|
30
|
+
## 2. testIDs first
|
|
31
|
+
|
|
32
|
+
Every element a test touches gets a `testID` in the component, named
|
|
33
|
+
`{screen}-{element}-{qualifier}`: `todos-screen`, `todos-create-button`,
|
|
34
|
+
`todo-item-${id}`, `login-screen-email-input`. Add missing IDs to the component before writing
|
|
35
|
+
the test. Select by `testID` in both layers; fall back to role, then text.
|
|
36
|
+
|
|
37
|
+
Always give testIDs to: each screen's root `Box`/`Page`, buttons and pressables, inputs, error
|
|
38
|
+
and validation text, loading indicators, list containers, and list items (dynamic IDs).
|
|
39
|
+
|
|
40
|
+
## 3. Component tests (Bun + RNTL)
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
44
|
+
import {renderWithTerreno} from "@terreno/test/expo/renderWithTerreno";
|
|
45
|
+
import {fireEvent} from "@testing-library/react-native";
|
|
46
|
+
import {TodoForm} from "./TodoForm";
|
|
47
|
+
|
|
48
|
+
describe("TodoForm", () => {
|
|
49
|
+
it("submits the trimmed title", async () => {
|
|
50
|
+
const onSubmit = mock((_title: string) => undefined);
|
|
51
|
+
const {findByTestId, getByTestId} = renderWithTerreno(<TodoForm onSubmit={onSubmit} />);
|
|
52
|
+
await findByTestId("todo-form");
|
|
53
|
+
|
|
54
|
+
fireEvent.changeText(getByTestId("todo-form-title-input"), " Buy milk ");
|
|
55
|
+
fireEvent.press(getByTestId("todo-form-submit-button"));
|
|
56
|
+
|
|
57
|
+
expect(onSubmit).toHaveBeenCalledWith("Buy milk");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("shows a validation error for an empty title", async () => {
|
|
61
|
+
const {findByTestId, getByTestId} = renderWithTerreno(<TodoForm onSubmit={mock()} />);
|
|
62
|
+
await findByTestId("todo-form");
|
|
63
|
+
fireEvent.press(getByTestId("todo-form-submit-button"));
|
|
64
|
+
expect(await findByTestId("todo-form-title-error")).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `renderWithTerreno(ui, {store?, theme?})` wraps in `TerrenoProvider` (and Redux `Provider`
|
|
70
|
+
with `store`). Use it instead of hand-wrapping.
|
|
71
|
+
- Start every test with `await findByTestId("<root testID>")`: `TerrenoProvider` loads fonts
|
|
72
|
+
asynchronously, and querying synchronously before that settles logs
|
|
73
|
+
`not wrapped in act(...)` warnings.
|
|
74
|
+
- Then `getBy*` for what is rendered, `findBy*` for what appears after an effect or promise,
|
|
75
|
+
`queryBy*` to assert absence.
|
|
76
|
+
- Assert with `bun:test`'s `expect`: `expect(getByText("Buy milk")).toBeTruthy()`,
|
|
77
|
+
`within(getByTestId("todos-list")).getByText(...)`, `expect(queryByTestId(...)).toBeNull()`.
|
|
78
|
+
RNTL's extra matchers (`toHaveTextContent`, `toBeVisible`) run but do not type-check
|
|
79
|
+
against `bun:test`, so `tsc` fails on them.
|
|
80
|
+
- Import order: `bun:test`, then `@terreno/*`, then `@testing-library/*`, then local files
|
|
81
|
+
(what Biome's `organizeImports` expects).
|
|
82
|
+
- Interact with `fireEvent.press` / `fireEvent.changeText`; Terreno `Button` calls `onClick`
|
|
83
|
+
through `onPress`.
|
|
84
|
+
- Cover each state the component has: loading, empty, error, populated, disabled.
|
|
85
|
+
- Hooks: `renderHook(() => useThing(), {wrapper: createTerrenoWrapper({store})})` from
|
|
86
|
+
`@testing-library/react-native` and `@terreno/test/expo/renderWithTerreno`.
|
|
87
|
+
|
|
88
|
+
## 4. Data in component tests
|
|
89
|
+
|
|
90
|
+
Choose in this order:
|
|
91
|
+
|
|
92
|
+
1. **Props.** Split data loading (container) from rendering (presentational) and test the
|
|
93
|
+
presentational component with literal props. Most screens need nothing else.
|
|
94
|
+
2. **syncdb collections in screens.** Wrap the app's collection hooks in one module
|
|
95
|
+
(`store/syncDbSdk.ts` exporting `useTodos()` etc.) and mock that module in a preload with
|
|
96
|
+
controllable rows, the same way as RTK hooks below. Test syncing behavior itself (outbox,
|
|
97
|
+
conflicts, deltas) against a real client built with `createFakeTransport` from
|
|
98
|
+
`@terreno/syncdb/testing`; it also needs a fake `httpChannel`, a memory
|
|
99
|
+
`persisterFactory`, and an `authProvider` — copy the `makeHarness` pattern from
|
|
100
|
+
`node_modules/@terreno/syncdb/src/client.test.ts`.
|
|
101
|
+
3. **Generated RTK hooks (`useGetXQuery`) and app modules.** Mock them **once in a preload**
|
|
102
|
+
(a second preload file after `testPreload.ts`) with controllable state, never with
|
|
103
|
+
`mock.module` inside a test file — module mocks last for the whole process and leak into
|
|
104
|
+
every later file:
|
|
105
|
+
```typescript
|
|
106
|
+
// tests/sdkMocks.ts (preloaded)
|
|
107
|
+
import {mock} from "bun:test";
|
|
108
|
+
export const sdkState = {todos: [] as unknown[], isLoading: false};
|
|
109
|
+
const actualSdk = await import("@/store/sdk");
|
|
110
|
+
mock.module("@/store/sdk", () => ({
|
|
111
|
+
...actualSdk,
|
|
112
|
+
useGetTodosQuery: () => ({data: {data: sdkState.todos}, isLoading: sdkState.isLoading}),
|
|
113
|
+
}));
|
|
114
|
+
```
|
|
115
|
+
Tests set `sdkState` in `beforeEach`; reset it in the preload's `afterEach`.
|
|
116
|
+
|
|
117
|
+
Auth tokens: `setAuthTokens({authToken: "token"})` / `clearAuthTokenStorage()` from
|
|
118
|
+
`@terreno/test/expo/authTokenStorage` seed the in-memory SecureStore/AsyncStorage the preload
|
|
119
|
+
installs (cleared after every test).
|
|
120
|
+
|
|
121
|
+
## 5. Async, time, and isolation
|
|
122
|
+
|
|
123
|
+
- Wait for UI with `findBy*` or RNTL's `waitFor`. For non-render conditions (mock calls,
|
|
124
|
+
store state, socket events) use `waitUntil` / `waitForCalls` from `@terreno/test`. Never
|
|
125
|
+
sleep with `setTimeout`.
|
|
126
|
+
- Freeze the clock with `freezeTime("2024-06-01T10:30:00Z")` from `@terreno/test` (Date.now,
|
|
127
|
+
new Date, and Luxon together).
|
|
128
|
+
- `spyOn` is restored after every test by the preload. `mock.module` is not — keep it in
|
|
129
|
+
preload files.
|
|
130
|
+
- `terreno-test-hygiene` (from `@terreno/test`) flags module-scope `mock.module`, sleeps,
|
|
131
|
+
barrel mocks, and component snapshots; run it before pushing if the repo uses it.
|
|
132
|
+
- Do not snapshot components (`toMatchSnapshot`): assert the text, testIDs, and props that
|
|
133
|
+
matter.
|
|
134
|
+
|
|
135
|
+
## 6. Playwright web E2E
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import {expect, test} from "@playwright/test";
|
|
139
|
+
import {loginAs} from "./helpers/login";
|
|
140
|
+
|
|
141
|
+
test.describe("Todos", () => {
|
|
142
|
+
test.beforeEach(async ({page}) => {
|
|
143
|
+
await loginAs(page);
|
|
144
|
+
await page.getByTestId("todos-screen").waitFor({state: "visible"});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("user can create a todo", async ({page}) => {
|
|
148
|
+
await page.getByTestId("todos-title-input").fill("Buy milk");
|
|
149
|
+
await page.getByTestId("todos-create-button").click();
|
|
150
|
+
await expect(page.getByTestId("todos-list")).toContainText("Buy milk");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Rules:
|
|
156
|
+
|
|
157
|
+
- Select with `getByTestId()`; fall back to `getByRole()`; use `getByText()` sparingly (React
|
|
158
|
+
Native Web wraps text in extra elements). Never select by class name (`.css-abc123` changes
|
|
159
|
+
every build) or deep structure (`div > div > div`).
|
|
160
|
+
- React Native Web renders `View` as `div`, `Text` as `div`/`span`, `Pressable` as
|
|
161
|
+
`div[role="button"]`, `TextInput` as `input`/`textarea`, `Switch` as `div[role="switch"]`
|
|
162
|
+
with `aria-checked`.
|
|
163
|
+
- Never `page.waitForTimeout()`. After navigation, wait for the target screen's root testID to
|
|
164
|
+
be visible; after a fetch, wait for the content or for the spinner to be hidden; use
|
|
165
|
+
web-first assertions (`await expect(locator).toHaveText(...)`), which retry.
|
|
166
|
+
- Log in through `loginAs(page)` in `test.beforeEach`, never inside a test body.
|
|
167
|
+
- Group with `test.describe()`, navigate in `test.beforeEach()`, name tests as user behavior
|
|
168
|
+
(`"user can submit a valid form"`), and keep tests independent: each creates its own data
|
|
169
|
+
or uses its own fixture user from `e2e/fixtures/testUsers.ts`.
|
|
170
|
+
- Seed through the API (`request.post`) in `beforeEach`/`auth.setup.ts`, not through the UI,
|
|
171
|
+
unless the UI path is what is under test.
|
|
172
|
+
|
|
173
|
+
Run:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bunx playwright test e2e/todos.spec.ts --reporter=list
|
|
177
|
+
bunx playwright test --ui # debug interactively
|
|
178
|
+
bunx playwright show-trace test-results/<test>/trace.zip
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Checklist
|
|
182
|
+
|
|
183
|
+
- [ ] Every touched element has a `{screen}-{element}-{qualifier}` testID
|
|
184
|
+
- [ ] Each component state (loading, empty, error, populated) has a test
|
|
185
|
+
- [ ] Data comes from props or a preload SDK/syncdb-hook mock — no `mock.module` in test
|
|
186
|
+
files
|
|
187
|
+
- [ ] No sleeps, `waitForTimeout`, class selectors, or component snapshots
|
|
188
|
+
- [ ] E2E logs in with `loginAs` in `beforeEach` and waits on screen root testIDs
|
|
189
|
+
- [ ] `bun test` (and `bunx playwright test` for E2E changes) passes for the whole app
|
|
190
|
+
- [ ] UI changes also get runtime evidence via `verify-ui-changes`
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: terreno-mobile-tests
|
|
3
|
+
description: >-
|
|
4
|
+
Write and fix mobile tests for a Terreno Expo app on iOS and Android: Maestro flows that
|
|
5
|
+
drive the native app on a simulator or emulator, and Bun unit tests for Platform.OS-specific
|
|
6
|
+
code. Covers testID selectors on native, login subflows, waiting, platform branches,
|
|
7
|
+
keyboards and system dialogs, reaching the local backend from a device, and debugging flaky
|
|
8
|
+
flows. Use when a change affects native-only behavior (navigation gestures, push, deep
|
|
9
|
+
links, permissions, keyboard, platform branches) or a Maestro flow fails. If the app has no
|
|
10
|
+
Maestro setup, run terreno-test-setup first.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Writing Terreno mobile tests
|
|
14
|
+
|
|
15
|
+
## 1. Pick the layer
|
|
16
|
+
|
|
17
|
+
| Behavior | Layer |
|
|
18
|
+
|----------|-------|
|
|
19
|
+
| Logic that branches on `Platform.OS` / `Platform.select` | Bun unit test run per platform (`TEST_PLATFORM=android bun test`) |
|
|
20
|
+
| Component behavior identical on every platform | `terreno-frontend-tests` (Bun + RNTL), once |
|
|
21
|
+
| Flow on a real device: navigation, keyboard, deep link, permission prompt, push tap, app restart | Maestro flow |
|
|
22
|
+
| Same flow in a browser | Playwright (`terreno-frontend-tests`) |
|
|
23
|
+
|
|
24
|
+
Done when: you named the layer and, for Maestro, which platforms (iOS, Android, or both) the
|
|
25
|
+
flow must pass on.
|
|
26
|
+
|
|
27
|
+
## 2. Platform unit tests
|
|
28
|
+
|
|
29
|
+
The preload fixes `Platform.OS` per run (see `terreno-test-setup`). Write the test once and
|
|
30
|
+
assert the branch for the current platform:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import {describe, expect, it} from "bun:test";
|
|
34
|
+
import {Platform} from "react-native";
|
|
35
|
+
import {keyboardOffset} from "./layout";
|
|
36
|
+
|
|
37
|
+
describe("keyboardOffset", () => {
|
|
38
|
+
it("uses the platform's offset", () => {
|
|
39
|
+
expect(keyboardOffset()).toBe(Platform.OS === "ios" ? 64 : 0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Run `bun test` and `bun run test:android` (and `test:web` if the code has a web branch).
|
|
45
|
+
|
|
46
|
+
## 3. Maestro flows
|
|
47
|
+
|
|
48
|
+
Write a flow per user goal, under `maestro/`, reusing subflows for login and navigation:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
appId: ${APP_ID}
|
|
52
|
+
---
|
|
53
|
+
# Creates a todo and sees it in the list after an app restart.
|
|
54
|
+
- launchApp:
|
|
55
|
+
clearState: true
|
|
56
|
+
- runFlow: subflows/login.yaml
|
|
57
|
+
- extendedWaitUntil:
|
|
58
|
+
visible:
|
|
59
|
+
id: "todos-screen"
|
|
60
|
+
timeout: 30000
|
|
61
|
+
- tapOn:
|
|
62
|
+
id: "todos-title-input"
|
|
63
|
+
- inputText: "Buy milk"
|
|
64
|
+
- hideKeyboard
|
|
65
|
+
- tapOn:
|
|
66
|
+
id: "todos-create-button"
|
|
67
|
+
- assertVisible: "Buy milk"
|
|
68
|
+
- stopApp
|
|
69
|
+
- launchApp
|
|
70
|
+
- extendedWaitUntil:
|
|
71
|
+
visible: "Buy milk"
|
|
72
|
+
timeout: 30000
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Rules:
|
|
76
|
+
|
|
77
|
+
- **Select by `id:`** (the React Native `testID`). Fall back to visible text only for system
|
|
78
|
+
UI and text you are asserting. Never select by coordinates or index.
|
|
79
|
+
- **Wait for screens** with `extendedWaitUntil: {visible: {id: "<screen>-screen"}, timeout}`
|
|
80
|
+
after launch and every navigation. Never use fixed `waitForAnimationToEnd` chains or sleeps
|
|
81
|
+
as the main wait.
|
|
82
|
+
- **Keyboards**: `hideKeyboard` after `inputText` before tapping something the keyboard may
|
|
83
|
+
cover.
|
|
84
|
+
- **Fresh state**: `launchApp: {clearState: true}` at the start of a flow that must not depend
|
|
85
|
+
on a previous run; seed data through the backend (seed script or API) rather than the UI.
|
|
86
|
+
- **Per-platform steps** with `runFlow: {when: {platform: Android}, commands: [...]}`: Android
|
|
87
|
+
has a system `back`; iOS does not (navigate with the app's back button testID instead).
|
|
88
|
+
- **System dialogs** (permissions, iOS "Save Password?") are optional and platform-specific:
|
|
89
|
+
```yaml
|
|
90
|
+
- runFlow:
|
|
91
|
+
when:
|
|
92
|
+
visible: "Not Now"
|
|
93
|
+
commands:
|
|
94
|
+
- tapOn: "Not Now"
|
|
95
|
+
```
|
|
96
|
+
- **Secrets and IDs** come from `-e` env vars (`${APP_ID}`, `${EMAIL}`, `${PASSWORD}`), never
|
|
97
|
+
hard-coded.
|
|
98
|
+
- One goal per flow; `maestro/config.yaml` lists them in order for CI.
|
|
99
|
+
|
|
100
|
+
## 4. Run and debug
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
maestro test -e APP_ID=com.<org>.<app> -e EMAIL=... -e PASSWORD=... maestro/todos.yaml
|
|
104
|
+
maestro --device <udid|emulator-serial> test ... # pick a device when several are booted
|
|
105
|
+
maestro hierarchy # dump the screen: confirm a testID exists
|
|
106
|
+
maestro studio # build steps interactively
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Symptom | Cause and fix |
|
|
110
|
+
|---------|---------------|
|
|
111
|
+
| `Element not found` for an `id:` | The component does not forward `testID`, or the element is off-screen: check `maestro hierarchy`, add `testID`, or `scrollUntilVisible` |
|
|
112
|
+
| App shows a network error | Backend unreachable: Android needs `adb reverse tcp:4000 tcp:4000` (and `tcp:8081` for Metro) after every emulator start |
|
|
113
|
+
| Passes on iOS, fails on Android | Keyboard covering the target (`hideKeyboard`), or a step that relies on iOS-only UI |
|
|
114
|
+
| First run on a fresh emulator times out | Maestro's driver install is slow the first time; rerun once |
|
|
115
|
+
| Red screen / stale bundle | Metro not running or serving another app: restart `expo start` for this app |
|
|
116
|
+
|
|
117
|
+
Save a screenshot on failure for the PR with `takeScreenshot: <name>` before the failing step,
|
|
118
|
+
or capture with `xcrun simctl io booted screenshot` / `adb exec-out screencap -p > shot.png`.
|
|
119
|
+
|
|
120
|
+
## Checklist
|
|
121
|
+
|
|
122
|
+
- [ ] Platform branches have unit tests that run under each platform they branch on
|
|
123
|
+
- [ ] Each Maestro flow targets one user goal, starts from `clearState`, and seeds data via the
|
|
124
|
+
backend
|
|
125
|
+
- [ ] Every step selects by `id:` (testID) except system dialogs and asserted text
|
|
126
|
+
- [ ] Screens are awaited with `extendedWaitUntil`; no sleeps
|
|
127
|
+
- [ ] The flow passed on each platform the change affects, and you reported which devices ran
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: terreno-test-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Bootstrap or repair the test setup of a Terreno app: backend Bun tests with in-memory
|
|
5
|
+
Mongo, frontend Bun + React Native Testing Library unit tests and Playwright web E2E, and
|
|
6
|
+
mobile Maestro flows on iOS/Android. Use when an app has no tests yet, `bun test` finds
|
|
7
|
+
nothing or crashes on import, a new backend/frontend/mobile package needs tests, or CI needs
|
|
8
|
+
a test job or sharding. Not for writing individual tests — use terreno-backend-tests,
|
|
9
|
+
terreno-frontend-tests, or terreno-mobile-tests.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Terreno test setup
|
|
13
|
+
|
|
14
|
+
Bootstrap one layer at a time. Each layer is done only when its completion check passes.
|
|
15
|
+
Apps from `create-terreno-app` have `"test": "bun test"` scripts but no preload, helpers, or
|
|
16
|
+
example test, so start with the backend.
|
|
17
|
+
|
|
18
|
+
## 0. Inventory
|
|
19
|
+
|
|
20
|
+
1. Read `backend/package.json`, `frontend/package.json`, every `bunfig.toml`, and any
|
|
21
|
+
existing `*.test.ts(x)`, `e2e/`, or `.maestro/` / `maestro/` directory.
|
|
22
|
+
2. Note the installed `@terreno/*` version. All `@terreno/*` packages share one version, so
|
|
23
|
+
`@terreno/test` must match it.
|
|
24
|
+
3. Pick the layers the user asked for. "Set up tests" with no qualifier means backend +
|
|
25
|
+
frontend unit; add Playwright and Maestro only when asked or when CI already expects them.
|
|
26
|
+
|
|
27
|
+
Done when: you can name, per layer, what exists and what is missing.
|
|
28
|
+
|
|
29
|
+
## 1. Backend (Bun + in-memory Mongo)
|
|
30
|
+
|
|
31
|
+
Follow [references/backend.md](references/backend.md): add `@terreno/test`, a `bunfig.toml`
|
|
32
|
+
preload that calls `registerBackendPreload`, service mocks registered once in that preload,
|
|
33
|
+
and one route test.
|
|
34
|
+
|
|
35
|
+
Done when: `cd backend && bun test` passes with the example route test while the dev
|
|
36
|
+
`mongod` is stopped (proves the suite uses the in-memory server, not the dev database).
|
|
37
|
+
|
|
38
|
+
## 2. Frontend unit tests (Bun + React Native Testing Library)
|
|
39
|
+
|
|
40
|
+
Follow [references/frontend.md](references/frontend.md): add `@terreno/test`,
|
|
41
|
+
`@testing-library/react-native@^13`, and `react-test-renderer`, a preload that calls
|
|
42
|
+
`registerExpoTestEnvironment`, and one component test using `renderWithTerreno`.
|
|
43
|
+
|
|
44
|
+
Done when: `cd frontend && bun test` passes with the example component test.
|
|
45
|
+
|
|
46
|
+
## 3. Frontend web E2E (Playwright)
|
|
47
|
+
|
|
48
|
+
Follow [references/frontend.md#playwright-web-e2e](references/frontend.md#playwright-web-e2e):
|
|
49
|
+
`playwright.config.ts` with `webServer` entries for backend + web, `e2e/auth.setup.ts`,
|
|
50
|
+
`e2e/helpers/login.ts` (`loginAs`), and one spec selecting by `testID`.
|
|
51
|
+
|
|
52
|
+
Done when: `bunx playwright test` passes locally against a replica-set Mongo.
|
|
53
|
+
|
|
54
|
+
## 4. Mobile (Maestro on iOS / Android)
|
|
55
|
+
|
|
56
|
+
Follow [references/mobile.md](references/mobile.md): a dev-client build on a simulator or
|
|
57
|
+
emulator, `maestro/config.yaml` plus one login flow keyed by `testID`, and optional
|
|
58
|
+
per-platform unit runs.
|
|
59
|
+
|
|
60
|
+
Done when: `maestro test maestro/` passes on at least one platform the user targets, and you
|
|
61
|
+
recorded which platforms and devices were exercised.
|
|
62
|
+
|
|
63
|
+
## 5. Scripts and CI
|
|
64
|
+
|
|
65
|
+
1. Package scripts: `test` (local), `test:ci` (same suite, no watch), plus `test:e2e` and
|
|
66
|
+
`test:maestro` when those layers exist.
|
|
67
|
+
2. Large suites: shard with `terreno-test-shard --shard <i>/<n>` (ships with
|
|
68
|
+
`@terreno/test`); add `terreno-test-hygiene --update-baseline` once, then run
|
|
69
|
+
`terreno-test-hygiene` in CI so new flaky patterns fail. See
|
|
70
|
+
[references/backend.md#ci](references/backend.md#ci).
|
|
71
|
+
3. E2E and Maestro jobs need a replica-set `mongod`, the backend, and the web or native app
|
|
72
|
+
running; unit jobs need nothing but `bun install`.
|
|
73
|
+
|
|
74
|
+
Done when: every layer you set up has a script, and the CI config (if the repo has one)
|
|
75
|
+
runs each script.
|
|
76
|
+
|
|
77
|
+
## Finish
|
|
78
|
+
|
|
79
|
+
Report each layer as set up / skipped / blocked with the exact command that proves it, then
|
|
80
|
+
point the user at the matching writing skill: `terreno-backend-tests`,
|
|
81
|
+
`terreno-frontend-tests`, `terreno-mobile-tests`.
|