@terreno/agent-plugin 0.0.0-stage → 57.21.0
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 +2124 -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 +83 -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 +73 -0
- package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
- package/skills/building-terreno-apps/SKILL.md +280 -0
- package/skills/building-terreno-apps/agents/openai.yaml +4 -0
- package/skills/building-terreno-apps/references/navigation.md +102 -0
- package/skills/building-terreno-apps/references/route-structure.md +91 -0
- package/skills/building-terreno-apps/references/screen-patterns.md +191 -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 +156 -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 +116 -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 +276 -0
- package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
- package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
- package/skills/terreno-data-fetching/references/realtime.md +45 -0
- package/skills/terreno-data-fetching/references/sdk-customization.md +84 -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 +85 -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 +86 -0
- package/skills/upgrading-terreno/references/ordering.md +40 -0
- package/skills/verify-ui-changes/SKILL.md +605 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Backend test setup
|
|
2
|
+
|
|
3
|
+
Bun's test runner plus `@terreno/test`: in-memory MongoDB, canonical auth env, quiet logs, a
|
|
4
|
+
Sentry mock, and supertest helpers.
|
|
5
|
+
|
|
6
|
+
## 1. Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
cd backend
|
|
10
|
+
bun add -d @terreno/test@<same version as @terreno/api> supertest @types/supertest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`mongoose` is a peer dependency and is already in a Terreno backend. `mongodb-memory-server`
|
|
14
|
+
downloads a `mongod` binary on first use (Bun blocks its postinstall by default; the first test
|
|
15
|
+
run downloads it instead, which can take a minute).
|
|
16
|
+
|
|
17
|
+
## 2. Preload
|
|
18
|
+
|
|
19
|
+
`backend/bunfig.toml`:
|
|
20
|
+
|
|
21
|
+
```toml
|
|
22
|
+
[test]
|
|
23
|
+
preload = ["./src/tests/bunSetup.ts"]
|
|
24
|
+
root = "./src"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`backend/src/tests/bunSetup.ts` — one file: SDK mocks first (if any), then the Mongo preload:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import {registerBackendPreload, registerSendGridMock} from "@terreno/test";
|
|
31
|
+
|
|
32
|
+
// Only for SDKs the app imports (a fresh create-terreno-app backend imports none; skip these).
|
|
33
|
+
export const sendGrid = registerSendGridMock();
|
|
34
|
+
|
|
35
|
+
registerBackendPreload({
|
|
36
|
+
onBeforeEach: () => {
|
|
37
|
+
sendGrid.reset();
|
|
38
|
+
// Keys the app reads that must start every test unset.
|
|
39
|
+
Reflect.deleteProperty(process.env, "SENDGRID_API_KEY");
|
|
40
|
+
},
|
|
41
|
+
testEnv: {
|
|
42
|
+
extra: {APP_URL: "http://localhost:8082"},
|
|
43
|
+
tokenIssuer: "<app>.test",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
What the preload does:
|
|
49
|
+
|
|
50
|
+
| When | Effect |
|
|
51
|
+
|------|--------|
|
|
52
|
+
| once per run | starts one `mongodb-memory-server` `mongod` (or connects to `TERRENO_TEST_MONGODB_URI`), mocks `@sentry/bun`, silences Winston/console |
|
|
53
|
+
| `beforeEach` | resets `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRET`, `NODE_ENV=test`, `TZ=UTC`, plus `testEnv.extra`; runs `onBeforeEach` |
|
|
54
|
+
| `afterEach` | resets the system clock, clears mock calls, restores `spyOn` spies, re-applies the env |
|
|
55
|
+
|
|
56
|
+
- It does **not** wipe collections. Tests delete what they read (see `terreno-backend-tests`).
|
|
57
|
+
- A cold `mongod` start can take 20s+; the launch timeout defaults to 60s
|
|
58
|
+
(`TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` to change it). The first run also downloads the
|
|
59
|
+
`mongod` binary.
|
|
60
|
+
- Some lines still print (`debug: No options.openApi provided...` when a test builds a
|
|
61
|
+
`TerrenoApp`, request logs). They are harmless.
|
|
62
|
+
- Change streams, transactions, or `SyncApp` under test need a replica set: set
|
|
63
|
+
`process.env.TERRENO_TEST_USE_REPLSET = "true"` at the top of the preload.
|
|
64
|
+
`registerBackendPreload({useTransactions: true})` also wraps each test in a transaction
|
|
65
|
+
that is aborted afterwards (no cleanup code, but every DB call is serialized through one
|
|
66
|
+
session); use it only when cleanup cost dominates the suite.
|
|
67
|
+
- `registerSimpleMongoPreload` is the lighter variant the Terreno packages use; prefer
|
|
68
|
+
`registerBackendPreload` in apps.
|
|
69
|
+
|
|
70
|
+
## 3. Third-party services
|
|
71
|
+
|
|
72
|
+
`mock.module` replaces a module for the whole `bun test` process, so a mock registered inside
|
|
73
|
+
one test file leaks into every later file. Register SDK mocks **only in the preload**, export
|
|
74
|
+
the handles so tests can assert on them, and `reset()` each in `onBeforeEach` (the preload's
|
|
75
|
+
`afterEach` clears call history but not stored state such as GCS objects or queued tasks).
|
|
76
|
+
|
|
77
|
+
| Registrar | Module |
|
|
78
|
+
|-----------|--------|
|
|
79
|
+
| `registerSendGridMock` | `@sendgrid/mail` |
|
|
80
|
+
| `registerTwilioMock` | `twilio` |
|
|
81
|
+
| `registerExpoPushMock` | `expo-server-sdk` |
|
|
82
|
+
| `registerCloudTasksMock` | `@google-cloud/tasks` |
|
|
83
|
+
| `registerGcsMock` | `@google-cloud/storage` |
|
|
84
|
+
| `registerGoogleAuthMock` | `google-auth-library` |
|
|
85
|
+
| `registerTerrenoNotifierMocks` | `sendToSlack` / `sendToZoom` / `sendToGoogleChat` from `@terreno/api` |
|
|
86
|
+
|
|
87
|
+
The AI SDK needs no module mock: pass `createMockLanguageModel()` to `AIService` (see
|
|
88
|
+
`terreno-backend-tests`). Prefer constructor injection over a module mock when the app already
|
|
89
|
+
accepts a client (for example `@terreno/comms` providers).
|
|
90
|
+
|
|
91
|
+
## 4. First test
|
|
92
|
+
|
|
93
|
+
`backend/src/api/users.test.ts` — proves the preload, Mongo, and supertest work:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import {describe, expect, it} from "bun:test";
|
|
97
|
+
import {TerrenoApp} from "@terreno/api";
|
|
98
|
+
import supertest from "supertest";
|
|
99
|
+
import {User} from "../models/user";
|
|
100
|
+
import {userRouter} from "./users";
|
|
101
|
+
|
|
102
|
+
const buildApp = () =>
|
|
103
|
+
new TerrenoApp({skipListen: true, userModel: User as never}).register(userRouter).build();
|
|
104
|
+
|
|
105
|
+
describe("users", () => {
|
|
106
|
+
it("rejects anonymous list requests", async () => {
|
|
107
|
+
const res = await supertest(buildApp()).get("/users");
|
|
108
|
+
expect(res.status).toBe(401);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Build a `TerrenoApp` with only the routers under test instead of calling the app's `start()`:
|
|
114
|
+
`start()` connects to the dev database and registers every plugin.
|
|
115
|
+
|
|
116
|
+
## CI
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
"scripts": {
|
|
120
|
+
"test": "bun test",
|
|
121
|
+
"test:ci": "bun test",
|
|
122
|
+
"test:shard": "terreno-test-shard",
|
|
123
|
+
"test:hygiene": "terreno-test-hygiene --root src"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- Sharding: `bun run test:shard --shard $INDEX/$TOTAL` in a CI matrix; `--list` prints a
|
|
128
|
+
shard's files; flags after `--` go to `bun test` (e.g. `-- --timeout 30000`).
|
|
129
|
+
- Hygiene: run `bun run test:hygiene --update-baseline` once to record existing violations,
|
|
130
|
+
commit the baseline, then run `bun run test:hygiene` in CI so only new ones fail.
|
|
131
|
+
- Unit jobs need no services: the in-memory `mongod` runs inside the job.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Frontend test setup
|
|
2
|
+
|
|
3
|
+
Two layers: **unit/component tests** run in Bun with React Native mocked (fast, no device), and
|
|
4
|
+
**web E2E** runs the real app in Chromium with Playwright.
|
|
5
|
+
|
|
6
|
+
## Unit tests (Bun + React Native Testing Library)
|
|
7
|
+
|
|
8
|
+
### 1. Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd frontend
|
|
12
|
+
bun add -d @terreno/test@<same version as @terreno/ui> @testing-library/react-native@^13 \
|
|
13
|
+
react-test-renderer@<exact react version>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Pin React Native Testing Library to **13**. Version 14 makes `render` async and drops
|
|
17
|
+
`react-test-renderer`; `renderWithTerreno` targets 13, and unpinned installs get 14 (tests
|
|
18
|
+
then fail with `findByText is not a function`).
|
|
19
|
+
- `react-test-renderer` must match `react` exactly (both `19.2.3` today). Each run prints
|
|
20
|
+
`react-test-renderer is deprecated`; that warning is expected with RNTL 13.
|
|
21
|
+
- `@terreno/test` also brings its backend dependencies (`mongodb-memory-server`, `express`);
|
|
22
|
+
they are only loaded by backend tests.
|
|
23
|
+
|
|
24
|
+
### 2. Preload
|
|
25
|
+
|
|
26
|
+
`frontend/bunfig.toml`:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[test]
|
|
30
|
+
preload = ["./tests/testPreload.ts"]
|
|
31
|
+
coveragePathIgnorePatterns = ["**/*.test.ts", "**/*.test.tsx", "tests/**", "store/openApiSdk.ts"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`frontend/tests/testPreload.ts`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import {registerExpoTestEnvironment} from "@terreno/test/expo/registerExpoTestEnvironment";
|
|
38
|
+
|
|
39
|
+
registerExpoTestEnvironment({
|
|
40
|
+
expoConfig: {extra: {}, scheme: "<app scheme from app.json>"},
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
It mocks `react-native` (Bun cannot parse its Flow sources), the common `expo-*` modules,
|
|
45
|
+
`expo-router`, `@expo/vector-icons`, reanimated, gesture handler, SVG, AsyncStorage, NetInfo,
|
|
46
|
+
and SecureStore; serves images and fonts as asset stubs; sets `__DEV__`,
|
|
47
|
+
`IS_REACT_ACT_ENVIRONMENT`, `TZ`, and `EXPO_OS`. After each test it restores spies, empties
|
|
48
|
+
stored tokens, and resets `__DEV__`.
|
|
49
|
+
|
|
50
|
+
Options when the defaults do not fit:
|
|
51
|
+
|
|
52
|
+
| Need | Option |
|
|
53
|
+
|------|--------|
|
|
54
|
+
| A module should load for real | `skip: ["expo-sqlite"]` |
|
|
55
|
+
| App-specific native module | `overrides: {"react-native-maps": () => ({MapView: () => null})}` |
|
|
56
|
+
| Android / web `Platform.OS` | `platform: "android"` (default `"ios"`) |
|
|
57
|
+
| Fixed clock in every test | `fixedNow: "2024-06-01T10:30:00.000Z"` |
|
|
58
|
+
| A package ships its own nested `react-native` | `nestedPackagePrefixes: ["some-lib/node_modules/"]` |
|
|
59
|
+
|
|
60
|
+
Put any further app-wide `mock.module` calls (e.g. a partial `@/store/sdk` mock) in a second
|
|
61
|
+
preload file listed **after** `testPreload.ts`, and load the real module there with
|
|
62
|
+
`await import(...)` — a static import would evaluate before React Native is mocked.
|
|
63
|
+
|
|
64
|
+
### 3. First test
|
|
65
|
+
|
|
66
|
+
`frontend/components/Greeting.test.tsx`:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import {describe, expect, it} from "bun:test";
|
|
70
|
+
import {renderWithTerreno} from "@terreno/test/expo/renderWithTerreno";
|
|
71
|
+
import {Greeting} from "./Greeting";
|
|
72
|
+
|
|
73
|
+
describe("Greeting", () => {
|
|
74
|
+
it("shows the user's name", async () => {
|
|
75
|
+
const {findByText} = renderWithTerreno(<Greeting name="Ada" />);
|
|
76
|
+
expect(await findByText("Hello, Ada")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`renderWithTerreno(ui, {store?, theme?})` wraps the tree in `TerrenoProvider` (and a Redux
|
|
82
|
+
`Provider` when `store` is passed; needs `react-redux`).
|
|
83
|
+
|
|
84
|
+
Done when `cd frontend && bun test` passes.
|
|
85
|
+
|
|
86
|
+
## Playwright web E2E
|
|
87
|
+
|
|
88
|
+
### 1. Install
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd frontend
|
|
92
|
+
bun add -d @playwright/test
|
|
93
|
+
bunx playwright install --with-deps chromium
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
E2E needs a **replica-set** `mongod` (the backend's sync and realtime use change streams):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
mongod --replSet rs0 --port 27017 --bind_ip 127.0.0.1 --dbpath ./.devdata/mongo &
|
|
100
|
+
mongosh --quiet --eval 'try { rs.status() } catch { rs.initiate({_id: "rs0", members: [{_id: 0, host: "127.0.0.1:27017"}]}) }'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Config
|
|
104
|
+
|
|
105
|
+
`frontend/playwright.config.ts`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import {defineConfig, devices} from "@playwright/test";
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
projects: [
|
|
112
|
+
{name: "setup", testMatch: /auth\.setup\.ts/},
|
|
113
|
+
{dependencies: ["setup"], name: "chromium", use: {...devices["Desktop Chrome"]}},
|
|
114
|
+
],
|
|
115
|
+
testDir: "./e2e",
|
|
116
|
+
use: {baseURL: "http://localhost:8082", trace: "retain-on-failure"},
|
|
117
|
+
webServer: [
|
|
118
|
+
{
|
|
119
|
+
command: "bun run --cwd ../backend src/index.ts",
|
|
120
|
+
env: {MONGO_URI: process.env.MONGO_URI ?? "mongodb://127.0.0.1/<app>-e2e?replicaSet=rs0", PORT: "4000"},
|
|
121
|
+
reuseExistingServer: !process.env.CI,
|
|
122
|
+
timeout: 120_000,
|
|
123
|
+
url: "http://localhost:4000/health",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
command: "bunx expo start --web --port 8082",
|
|
127
|
+
env: {EXPO_PUBLIC_API_URL: "http://localhost:4000"},
|
|
128
|
+
reuseExistingServer: !process.env.CI,
|
|
129
|
+
timeout: 180_000,
|
|
130
|
+
url: "http://localhost:8082",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Layout
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
e2e/
|
|
140
|
+
fixtures/testUsers.ts # TEST_USER, ADMIN_USER: {email, name, password}
|
|
141
|
+
helpers/login.ts # loginAs(page, user = TEST_USER)
|
|
142
|
+
auth.setup.ts # creates the users through the API before any spec runs
|
|
143
|
+
<feature>.spec.ts
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`e2e/helpers/login.ts`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import type {Page} from "@playwright/test";
|
|
150
|
+
import {TEST_USER} from "../fixtures/testUsers";
|
|
151
|
+
|
|
152
|
+
export const loginAs = async (page: Page, user = TEST_USER): Promise<void> => {
|
|
153
|
+
await page.goto("/login");
|
|
154
|
+
await page.getByTestId("login-screen").waitFor({state: "visible"});
|
|
155
|
+
await page.getByTestId("login-screen-email-input").fill(user.email);
|
|
156
|
+
await page.getByTestId("login-screen-password-input").fill(user.password);
|
|
157
|
+
await page.getByTestId("login-screen-submit-button").click();
|
|
158
|
+
await page.getByTestId("login-screen").waitFor({state: "hidden"});
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`e2e/auth.setup.ts` signs each fixture user up through the backend's auth API and ignores
|
|
163
|
+
"already exists", so reruns are idempotent. With Better Auth (the `create-terreno-app`
|
|
164
|
+
default) that is `request.post("http://localhost:4000/api/auth/sign-up/email", {data: {email,
|
|
165
|
+
name, password}})`; with JWT auth it is `POST /auth/signup`. `bun run --cwd ../backend` loads
|
|
166
|
+
`backend/.env`, so the backend gets its auth secrets. The `@terreno/ui` login screen already
|
|
167
|
+
carries the `login-screen*` testIDs; give a custom form the same IDs.
|
|
168
|
+
|
|
169
|
+
Done when `bunx playwright test` passes. Add `"test:e2e": "playwright test"` to the scripts.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Mobile test setup (iOS / Android)
|
|
2
|
+
|
|
3
|
+
Two layers:
|
|
4
|
+
|
|
5
|
+
- **Platform unit tests** — the same Bun suite with `Platform.OS` set to `ios` or `android`.
|
|
6
|
+
Fast; no device.
|
|
7
|
+
- **Maestro flows** — drive the real native app on a simulator or emulator.
|
|
8
|
+
|
|
9
|
+
## Platform unit tests
|
|
10
|
+
|
|
11
|
+
`registerExpoTestEnvironment` fixes `Platform.OS` for the whole run, so pick it from an env var
|
|
12
|
+
in the preload and run the suite once per platform:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// frontend/tests/testPreload.ts
|
|
16
|
+
import {registerExpoTestEnvironment} from "@terreno/test/expo/registerExpoTestEnvironment";
|
|
17
|
+
|
|
18
|
+
const platform = (process.env.TEST_PLATFORM ?? "ios") as "android" | "ios" | "web";
|
|
19
|
+
registerExpoTestEnvironment({expoConfig: {extra: {}, scheme: "<scheme>"}, platform});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"test:android": "TEST_PLATFORM=android bun test",
|
|
26
|
+
"test:web": "TEST_PLATFORM=web bun test"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run the extra platforms in CI only when the app has `Platform.OS` / `Platform.select` branches.
|
|
31
|
+
|
|
32
|
+
## Maestro
|
|
33
|
+
|
|
34
|
+
### 1. Native identifiers
|
|
35
|
+
|
|
36
|
+
Maestro targets the app by bundle ID. Set both in `frontend/app.json` if missing
|
|
37
|
+
(`create-terreno-app` only sets `scheme`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
"ios": {"bundleIdentifier": "com.<org>.<app>"},
|
|
41
|
+
"android": {"package": "com.<org>.<app>"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. A build on a device
|
|
45
|
+
|
|
46
|
+
Use a **development build**, not Expo Go (Expo Go's app ID is Expo's, not yours):
|
|
47
|
+
|
|
48
|
+
| Platform | Command |
|
|
49
|
+
|----------|---------|
|
|
50
|
+
| iOS simulator (macOS + Xcode) | `bunx expo run:ios` |
|
|
51
|
+
| Android emulator | `bunx expo run:android` |
|
|
52
|
+
| Either, prebuilt in the cloud | `bunx eas-cli build --profile development --platform <ios\|android>` then `bunx eas-cli build:run -p <platform> --latest` |
|
|
53
|
+
|
|
54
|
+
The `store-assets` skill's `devclient:ensure` script keeps a dev client in sync with the
|
|
55
|
+
native fingerprint if the app already uses it.
|
|
56
|
+
|
|
57
|
+
Reach the local backend from the device:
|
|
58
|
+
|
|
59
|
+
- iOS simulator shares the Mac's `localhost`; nothing to do.
|
|
60
|
+
- Android emulator: `adb reverse tcp:8081 tcp:8081 && adb reverse tcp:4000 tcp:4000` (Metro and
|
|
61
|
+
the API). Repeat after every emulator restart.
|
|
62
|
+
|
|
63
|
+
### 3. Install Maestro
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
curl -fsSL "https://get.maestro.mobile.dev" | bash # needs Java 17
|
|
67
|
+
maestro --version
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Flows
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
frontend/maestro/
|
|
74
|
+
config.yaml # flow list and order
|
|
75
|
+
subflows/login.yaml # reused by other flows
|
|
76
|
+
login.yaml
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`frontend/maestro/subflows/login.yaml`:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
appId: ${APP_ID}
|
|
83
|
+
---
|
|
84
|
+
- extendedWaitUntil:
|
|
85
|
+
visible:
|
|
86
|
+
id: "login-screen-email-input"
|
|
87
|
+
timeout: 60000
|
|
88
|
+
- tapOn:
|
|
89
|
+
id: "login-screen-email-input"
|
|
90
|
+
- inputText: ${EMAIL}
|
|
91
|
+
- tapOn:
|
|
92
|
+
id: "login-screen-password-input"
|
|
93
|
+
- inputText: ${PASSWORD}
|
|
94
|
+
- hideKeyboard
|
|
95
|
+
- tapOn:
|
|
96
|
+
id: "login-screen-submit-button"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`frontend/maestro/login.yaml`:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
appId: ${APP_ID}
|
|
103
|
+
---
|
|
104
|
+
- launchApp:
|
|
105
|
+
clearState: true
|
|
106
|
+
- runFlow: subflows/login.yaml
|
|
107
|
+
- extendedWaitUntil:
|
|
108
|
+
visible:
|
|
109
|
+
id: "home-screen"
|
|
110
|
+
timeout: 30000
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
React Native `testID` becomes the iOS accessibility identifier and the Android resource ID, so
|
|
114
|
+
Maestro's `id:` selector works on both. `@terreno/ui` components accept a `testID` prop; check a custom component passes it to the underlying native view.
|
|
115
|
+
|
|
116
|
+
### 5. Run
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
maestro test -e APP_ID=com.<org>.<app> -e EMAIL=test@example.com -e PASSWORD=<password> maestro/
|
|
120
|
+
maestro --device <udid|emulator-serial> test ... # when several devices are booted
|
|
121
|
+
maestro hierarchy # list the testIDs on screen
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Seed the user first (`bun run seed` in the backend, or the E2E auth setup).
|
|
125
|
+
|
|
126
|
+
Add `"test:maestro": "maestro test maestro/"` to the scripts.
|
|
127
|
+
|
|
128
|
+
### CI
|
|
129
|
+
|
|
130
|
+
- Android: a Linux runner with an emulator (e.g. `reactivecircus/android-emulator-runner`),
|
|
131
|
+
then build, install, `adb reverse`, and `maestro test`.
|
|
132
|
+
- iOS: needs a macOS runner and a simulator build (`eas build --profile development:simulator`
|
|
133
|
+
or `expo run:ios`).
|
|
134
|
+
- Maestro Cloud runs both from an uploaded build if the project has an account.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: terreno-ui
|
|
3
|
+
description: >-
|
|
4
|
+
Build UI with @terreno/ui — Terreno's React Native component library with a
|
|
5
|
+
large component set and a three-layer theming system. Use when adding or reviewing
|
|
6
|
+
layout, forms, tables, modals, feedback, or navigation chrome. Covers
|
|
7
|
+
component selection, Box/Page patterns, theme props, and when NOT to use raw
|
|
8
|
+
React Native primitives. Lifecycle composition: Grow for UI shape, Pick for
|
|
9
|
+
implementation, Roast for UI conformance review. Not for backend APIs, RTK
|
|
10
|
+
Query, or Expo Router setup.
|
|
11
|
+
---
|
|
12
|
+
# @terreno/ui
|
|
13
|
+
|
|
14
|
+
`@terreno/ui` is Terreno's universal (iOS, Android, web) component library. Start with existing components before creating new ones. For full screen structure and navigation, see `building-terreno-apps`.
|
|
15
|
+
|
|
16
|
+
## Choosing a component (read this first)
|
|
17
|
+
|
|
18
|
+
Work down this list and stop at the first layer that meets the need:
|
|
19
|
+
|
|
20
|
+
1. **Layout primitives — start here.** `Box` for flex layout, `Page` for full screens, `Card` for grouped content with shadow.
|
|
21
|
+
2. **Form fields.** `TextField`, `SelectField`, `DateTimeField`, `CheckBox`, etc. for user input.
|
|
22
|
+
3. **Data display.** `DataTable`, `Badge`, `Text`, `Heading` for content.
|
|
23
|
+
4. **Feedback.** `Modal`, `Toast`, `Spinner`, `Banner`, `ErrorPage` for states.
|
|
24
|
+
5. **Admin screens.** Skill `building-admin-interfaces` (`AdminScreenRouter`,
|
|
25
|
+
`AdminModelTable`, `AdminModelForm`, `AdminShellLayout`) before hand-rolling
|
|
26
|
+
tables/forms.
|
|
27
|
+
6. **New component — last resort.** Only when a concrete composition gap is proven. Add to `ui/` package and a demo story in `demo/`.
|
|
28
|
+
|
|
29
|
+
**Do not use `@expo/ui`** in Terreno apps unless there is an explicit requirement for native SwiftUI/Compose that `@terreno/ui` cannot satisfy.
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
1. Read `docs/reference/ui.md`, `docs/explanation/responsive-design.md`, `docs/reference/components/`, and the relevant explanation pages before changing components, width-dependent layout, or theme contracts.
|
|
34
|
+
2. Implement against that design.
|
|
35
|
+
3. Update those pages (and a demo story) in the same slice with `update-docs`.
|
|
36
|
+
4. Ship without matching docs is a failed slice.
|
|
37
|
+
|
|
38
|
+
## References
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
references/
|
|
42
|
+
layout.md Box, Page, Card, ListDetailLayout, SplitPage — spacing and flex
|
|
43
|
+
forms.md TextField, SelectField, DateTimeField, validation
|
|
44
|
+
data-display.md DataTable, Badge, Text, Heading
|
|
45
|
+
feedback.md Modal, Toast, Spinner, Banner
|
|
46
|
+
theming.md TerrenoProvider, useTheme, color tokens
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
In Terreno monorepo apps, `@terreno/ui` is a workspace dependency:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
"@terreno/ui": "workspace:*"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Wrap the app root in `TerrenoProvider`:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import {TerrenoProvider} from "@terreno/ui";
|
|
61
|
+
|
|
62
|
+
<TerrenoProvider>
|
|
63
|
+
{children}
|
|
64
|
+
</TerrenoProvider>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick examples
|
|
68
|
+
|
|
69
|
+
### Layout
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import {Box, Card, Heading, Page, Text} from "@terreno/ui";
|
|
73
|
+
|
|
74
|
+
const ProfileScreen: React.FC = () => (
|
|
75
|
+
<Page title="Profile" scroll>
|
|
76
|
+
<Box gap={4} padding={4}>
|
|
77
|
+
<Heading size="lg">Account</Heading>
|
|
78
|
+
<Card padding={4}>
|
|
79
|
+
<Text>user@example.com</Text>
|
|
80
|
+
</Card>
|
|
81
|
+
</Box>
|
|
82
|
+
</Page>
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Button
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import {Button} from "@terreno/ui";
|
|
90
|
+
|
|
91
|
+
<Button
|
|
92
|
+
text="Save"
|
|
93
|
+
variant="primary"
|
|
94
|
+
onClick={handleSave}
|
|
95
|
+
loading={isSaving}
|
|
96
|
+
iconName="check"
|
|
97
|
+
fullWidth
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Variants: `primary` | `secondary` | `muted` | `outline` | `destructive`
|
|
102
|
+
|
|
103
|
+
### Form field
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import {TextField} from "@terreno/ui";
|
|
107
|
+
|
|
108
|
+
<TextField
|
|
109
|
+
title="Email"
|
|
110
|
+
value={email}
|
|
111
|
+
onChange={setEmail}
|
|
112
|
+
type="email"
|
|
113
|
+
errorText={emailError}
|
|
114
|
+
helperText="We'll never share your email"
|
|
115
|
+
/>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Modal
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import {Modal, Text} from "@terreno/ui";
|
|
122
|
+
|
|
123
|
+
<Modal
|
|
124
|
+
title="Confirm Delete"
|
|
125
|
+
visible={isVisible}
|
|
126
|
+
primaryButtonText="Delete"
|
|
127
|
+
secondaryButtonText="Cancel"
|
|
128
|
+
onDismiss={() => setIsVisible(false)}
|
|
129
|
+
onPrimaryAction={handleDelete}
|
|
130
|
+
>
|
|
131
|
+
<Text>Are you sure?</Text>
|
|
132
|
+
</Modal>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Rules
|
|
136
|
+
|
|
137
|
+
- Use `Box` for layout — never raw `View` (unless inside a platform-specific workaround).
|
|
138
|
+
- Use `Text` / `Heading` from `@terreno/ui` — never raw React Native `Text`.
|
|
139
|
+
- Use theme props (`padding`, `gap`, `color`, `border`, `rounding`) — not inline hex when a token exists.
|
|
140
|
+
- Use `Page` for screen-level layout — not bare `ScrollView`.
|
|
141
|
+
- Icons use FontAwesome 6 via `iconName` props.
|
|
142
|
+
- Use Luxon for dates in display logic; `DateTimeField` handles input.
|
|
143
|
+
- Always support React Native Web.
|
|
144
|
+
- After component changes, verify in the demo app (`verify-ui-changes` skill).
|
|
145
|
+
|
|
146
|
+
## Component map
|
|
147
|
+
|
|
148
|
+
| Need | Component |
|
|
149
|
+
|------|-----------|
|
|
150
|
+
| Flex layout | `Box` |
|
|
151
|
+
| Full screen | `Page` |
|
|
152
|
+
| Grouped content | `Card` |
|
|
153
|
+
| List/detail with routes (mail, inbox, records) | `ListDetailLayout` in the group's `_layout.tsx` |
|
|
154
|
+
| List/detail without routes (inside a modal) | `SplitPage` |
|
|
155
|
+
| Text input | `TextField`, `TextArea`, `EmailField`, `PasswordField` |
|
|
156
|
+
| Dropdown | `SelectField`, `MultiselectField` |
|
|
157
|
+
| Date/time | `DateTimeField` |
|
|
158
|
+
| Boolean | `CheckBox`, `BooleanField`, `RadioField` |
|
|
159
|
+
| Table | `DataTable` |
|
|
160
|
+
| Status chip | `Badge` |
|
|
161
|
+
| Action | `Button`, `IconButton`, `Link` |
|
|
162
|
+
| Dialog | `Modal`, `ActionSheet`, `ModalSheet` |
|
|
163
|
+
| Loading | `Spinner` |
|
|
164
|
+
| Notification | `Toast` (via TerrenoProvider), `Banner` |
|
|
165
|
+
| Error state | `ErrorPage`, `ErrorBoundary` |
|
|
166
|
+
| OAuth login | `SocialLoginButton` |
|
|
167
|
+
| Signature | `SignatureField` |
|
|
168
|
+
| Markdown | `MarkdownView` |
|
|
169
|
+
|
|
170
|
+
See `./references/` for detailed props and patterns.
|
|
171
|
+
|
|
172
|
+
## vs Expo UI (`@expo/ui`)
|
|
173
|
+
|
|
174
|
+
| | @terreno/ui | @expo/ui |
|
|
175
|
+
|---|-------------|----------|
|
|
176
|
+
| Platform | RN + web (unified) | Native SwiftUI/Compose |
|
|
177
|
+
| Theming | Three-layer design tokens | Platform-native |
|
|
178
|
+
| Forms | Full field library | Limited universal set |
|
|
179
|
+
| Tables | `DataTable` with sort/pagination | List components |
|
|
180
|
+
| Admin | `@terreno/admin-frontend` | None |
|
|
181
|
+
| Terreno integration | First-class | Not integrated |
|
|
182
|
+
|
|
183
|
+
Default to `@terreno/ui` in all Terreno apps.
|
|
184
|
+
|
|
185
|
+
## Decision Tree
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Need UI in a Terreno app?
|
|
189
|
+
|-- Screen structure?
|
|
190
|
+
| \-- Page + Box (references/layout.md)
|
|
191
|
+
|
|
|
192
|
+
|-- User input?
|
|
193
|
+
| \-- references/forms.md
|
|
194
|
+
|
|
|
195
|
+
|-- Show data (table, badge, text)?
|
|
196
|
+
| \-- references/data-display.md
|
|
197
|
+
|
|
|
198
|
+
|-- Loading/error/modal/toast?
|
|
199
|
+
| \-- references/feedback.md
|
|
200
|
+
|
|
|
201
|
+
|-- Colors/spacing/fonts?
|
|
202
|
+
| \-- references/theming.md
|
|
203
|
+
|
|
|
204
|
+
|-- Admin CRUD table/form, sidebar, or custom operator screen?
|
|
205
|
+
| \-- skill building-admin-interfaces
|
|
206
|
+
|
|
|
207
|
+
|-- Component doesn't exist?
|
|
208
|
+
| \-- Compose with Box + existing fields; add to ui/ only if gap is proven
|
|
209
|
+
|
|
|
210
|
+
\-- Changed a component?
|
|
211
|
+
\-- verify-ui-changes (demo app /dev route)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Common Mistakes
|
|
215
|
+
|
|
216
|
+
**Wrong: inline styles for spacing**
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
<View style={{padding: 16, marginBottom: 8}}>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Right: theme props**
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<Box padding={4} marginBottom={2}>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Wrong: raw Text**
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import {Text} from "react-native";
|
|
232
|
+
<Text style={{fontSize: 24}}>Title</Text>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Right: Heading**
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import {Heading} from "@terreno/ui";
|
|
239
|
+
<Heading size="lg">Title</Heading>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Wrong: StyleSheet.create for one-off styles**
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
const styles = StyleSheet.create({container: {flex: 1}});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Right: inline styles or Box props (Terreno convention)**
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
<Box flex="grow" padding={4}>
|
|
252
|
+
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Terreno UI"
|
|
3
|
+
short_description: "Choose and compose @terreno/ui components — Box, Page, forms, tables, modals, and theming for cross-platform apps"
|
|
4
|
+
default_prompt: "Use $terreno-ui to pick the right @terreno/ui components and theme props for a layout, form, or feedback pattern."
|