@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.
Files changed (98) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/CHANGELOG.md +2213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -2
  5. package/agents/pre-commit.md +65 -0
  6. package/agents/ui-verifier.md +93 -0
  7. package/package.json +32 -4
  8. package/references/async-review-bots.md +76 -0
  9. package/references/documentation-contract.md +61 -0
  10. package/references/execution-state.schema.json +77 -0
  11. package/references/github-attention-contract.md +165 -0
  12. package/references/independent-review.md +31 -0
  13. package/references/lifecycle-contract.md +211 -0
  14. package/references/loop-engineering.md +125 -0
  15. package/references/pick-roast-loop.md +112 -0
  16. package/references/pr-deployments.md +69 -0
  17. package/references/product-ci.md +164 -0
  18. package/references/push-ownership.md +91 -0
  19. package/references/stage-result.schema.json +82 -0
  20. package/references/subagent-briefing.md +43 -0
  21. package/references/testing-plan.md +163 -0
  22. package/references/user-guide.md +171 -0
  23. package/skills/1-grow/SKILL.md +145 -0
  24. package/skills/1-grow/references/grilling.md +243 -0
  25. package/skills/2-pick/SKILL.md +170 -0
  26. package/skills/2-pick/references/mocking.md +34 -0
  27. package/skills/2-pick/references/testing.md +43 -0
  28. package/skills/3-roast/SKILL.md +168 -0
  29. package/skills/4-brew/SKILL.md +201 -0
  30. package/skills/5-taste/SKILL.md +311 -0
  31. package/skills/ai-prompt-governance/SKILL.md +100 -0
  32. package/skills/attach-pr-media/SKILL.md +89 -0
  33. package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
  34. package/skills/backend-test-env/SKILL.md +64 -0
  35. package/skills/backfill-regression/SKILL.md +101 -0
  36. package/skills/backfill-user-guide/SKILL.md +96 -0
  37. package/skills/bootstrap-mcp-server/SKILL.md +244 -0
  38. package/skills/building-admin-interfaces/SKILL.md +86 -0
  39. package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
  40. package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
  41. package/skills/building-admin-interfaces/references/patterns-and-tools.md +74 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +294 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +108 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +104 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +174 -0
  48. package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
  49. package/skills/deploy-gcp/SKILL.md +73 -0
  50. package/skills/deploy-gcp/references/troubleshooting.md +29 -0
  51. package/skills/generate-release-announcements/SKILL.md +145 -0
  52. package/skills/generate-sdk/SKILL.md +165 -0
  53. package/skills/mongoose-schema-safety/SKILL.md +164 -0
  54. package/skills/pick-roast-loop/SKILL.md +152 -0
  55. package/skills/planning-loop/SKILL.md +161 -0
  56. package/skills/prd/SKILL.md +138 -0
  57. package/skills/set-up-social-login/SKILL.md +76 -0
  58. package/skills/store-assets/SKILL.md +221 -0
  59. package/skills/store-assets/examples/screenshots.yaml +31 -0
  60. package/skills/store-assets/examples/storeAssets.config.json +44 -0
  61. package/skills/store-assets/scripts/banner.ts +165 -0
  62. package/skills/store-assets/scripts/config.ts +255 -0
  63. package/skills/store-assets/scripts/devClient.ts +635 -0
  64. package/skills/store-assets/scripts/devices.ts +286 -0
  65. package/skills/store-assets/scripts/icon.ts +81 -0
  66. package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
  67. package/skills/store-assets/scripts/maestro/login.yaml +27 -0
  68. package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
  69. package/skills/store-assets/scripts/screenshots.ts +848 -0
  70. package/skills/taste-sweep/SKILL.md +172 -0
  71. package/skills/terreno-backend-api/SKILL.md +355 -0
  72. package/skills/terreno-backend-api/agents/openai.yaml +4 -0
  73. package/skills/terreno-backend-api/references/custom-routes.md +132 -0
  74. package/skills/terreno-backend-api/references/model-router.md +112 -0
  75. package/skills/terreno-backend-api/references/server-setup.md +116 -0
  76. package/skills/terreno-backend-tests/SKILL.md +176 -0
  77. package/skills/terreno-data-fetching/SKILL.md +288 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +89 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +50 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +55 -0
  82. package/skills/terreno-frontend-tests/SKILL.md +190 -0
  83. package/skills/terreno-mobile-tests/SKILL.md +127 -0
  84. package/skills/terreno-test-setup/SKILL.md +81 -0
  85. package/skills/terreno-test-setup/references/backend.md +131 -0
  86. package/skills/terreno-test-setup/references/frontend.md +169 -0
  87. package/skills/terreno-test-setup/references/mobile.md +134 -0
  88. package/skills/terreno-ui/SKILL.md +252 -0
  89. package/skills/terreno-ui/agents/openai.yaml +4 -0
  90. package/skills/terreno-ui/references/data-display.md +87 -0
  91. package/skills/terreno-ui/references/feedback.md +122 -0
  92. package/skills/terreno-ui/references/forms.md +119 -0
  93. package/skills/terreno-ui/references/layout.md +124 -0
  94. package/skills/terreno-ui/references/theming.md +87 -0
  95. package/skills/update-docs/SKILL.md +82 -0
  96. package/skills/upgrading-terreno/SKILL.md +93 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,286 @@
1
+ // Simulator, emulator, and dev-client launch helpers shared by devClient.ts and screenshots.ts.
2
+
3
+ import {join} from "node:path";
4
+ import {type ExpoInfo, fail, getAndroidHome, run, runOrFail, warn} from "./config";
5
+
6
+ // Booting a cold emulator can take several minutes on a loaded host.
7
+ const BOOT_POLL_ATTEMPTS = 150;
8
+ const BOOT_POLL_INTERVAL_MS = 2_000;
9
+ const DEV_MENU_PREFS = "expo.modules.devmenu.sharedpreferences";
10
+
11
+ export interface SimDevice {
12
+ name: string;
13
+ udid: string;
14
+ state: string;
15
+ }
16
+
17
+ export interface RunningEmulator {
18
+ avd: string;
19
+ serial: string;
20
+ }
21
+
22
+ // "skipIntro" only marks the dev menu intro as seen. "hideAll" also hides the floating gear
23
+ // button and the auto-open sheet, for clean screenshots.
24
+ export type DevMenuMode = "skipIntro" | "hideAll";
25
+
26
+ export const listIosSimulators = async ({
27
+ filter,
28
+ tag,
29
+ }: {
30
+ filter: "booted" | "available";
31
+ tag: string;
32
+ }): Promise<SimDevice[]> => {
33
+ const output = await runOrFail({
34
+ cmd: ["xcrun", "simctl", "list", "devices", filter, "-j"],
35
+ isQuiet: true,
36
+ tag,
37
+ });
38
+ // Newest runtime first, so a fresh simulator lands on the latest iOS.
39
+ return Object.entries(JSON.parse(output).devices as Record<string, SimDevice[]>)
40
+ .filter(([runtime]) => runtime.includes("iOS"))
41
+ .sort(([a], [b]) => b.localeCompare(a, undefined, {numeric: true}))
42
+ .flatMap(([, devices]) => devices);
43
+ };
44
+
45
+ export const listIosRuntimes = async ({tag}: {tag: string}): Promise<string[]> => {
46
+ const output = await runOrFail({
47
+ cmd: ["xcrun", "simctl", "list", "devices", "available", "-j"],
48
+ isQuiet: true,
49
+ tag,
50
+ });
51
+ return Object.keys(JSON.parse(output).devices as Record<string, SimDevice[]>)
52
+ .filter((runtime) => runtime.includes("iOS"))
53
+ .sort((a, b) => b.localeCompare(a, undefined, {numeric: true}));
54
+ };
55
+
56
+ // Best-effort: the simulator keeps running headless when Simulator.app isn't installed.
57
+ const openSimulatorApp = async ({tag}: {tag: string}): Promise<void> => {
58
+ const proc = Bun.spawn(["open", "-a", "Simulator"], {stderr: "ignore", stdout: "ignore"});
59
+ if ((await proc.exited) !== 0) {
60
+ warn({
61
+ message:
62
+ "Simulator.app not found; simulator is running headless. Install it from Xcode > Settings > Components.",
63
+ tag,
64
+ });
65
+ }
66
+ };
67
+
68
+ export const bootIosSimulator = async ({udid, tag}: {udid: string; tag: string}): Promise<void> => {
69
+ const booted = await listIosSimulators({filter: "booted", tag});
70
+ if (!booted.some((sim) => sim.udid === udid)) {
71
+ await run({cmd: ["xcrun", "simctl", "boot", udid], isQuiet: true});
72
+ await runOrFail({cmd: ["xcrun", "simctl", "bootstatus", udid, "-b"], isQuiet: true, tag});
73
+ }
74
+ await openSimulatorApp({tag});
75
+ };
76
+
77
+ export const listRunningEmulators = async ({tag}: {tag: string}): Promise<RunningEmulator[]> => {
78
+ const output = await runOrFail({cmd: ["adb", "devices"], isQuiet: true, tag});
79
+ const serials = output
80
+ .split("\n")
81
+ .map((line) => line.split("\t"))
82
+ .filter(([serial, state]) => serial?.startsWith("emulator-") && state === "device")
83
+ .map(([serial]) => serial as string);
84
+ return Promise.all(
85
+ serials.map(async (serial) => {
86
+ const {stdout} = await run({cmd: ["adb", "-s", serial, "emu", "avd", "name"], isQuiet: true});
87
+ return {avd: stdout.split("\n")[0]?.trim() ?? "", serial};
88
+ })
89
+ );
90
+ };
91
+
92
+ export const findEmulatorSerial = async ({
93
+ avd,
94
+ tag,
95
+ }: {
96
+ avd?: string;
97
+ tag: string;
98
+ }): Promise<string | undefined> => {
99
+ const emulators = await listRunningEmulators({tag});
100
+ return (avd ? emulators.find((emulator) => emulator.avd === avd) : emulators[0])?.serial;
101
+ };
102
+
103
+ export const listAvds = async ({tag}: {tag: string}): Promise<string[]> => {
104
+ const emulatorBin = join(getAndroidHome({tag}), "emulator", "emulator");
105
+ const output = await runOrFail({cmd: [emulatorBin, "-list-avds"], isQuiet: true, tag});
106
+ return output.split("\n").filter(Boolean);
107
+ };
108
+
109
+ // Starts the AVD (if needed) and returns its adb serial once Android reports boot complete.
110
+ export const bootEmulator = async ({avd, tag}: {avd: string; tag: string}): Promise<string> => {
111
+ const running = await findEmulatorSerial({avd, tag});
112
+ if (running) {
113
+ return running;
114
+ }
115
+ const emulatorBin = join(getAndroidHome({tag}), "emulator", "emulator");
116
+ // nohup without a shell, so the emulator outlives this script and no value is shell-parsed.
117
+ Bun.spawn(["nohup", emulatorBin, "-avd", avd], {
118
+ stderr: "ignore",
119
+ stdin: "ignore",
120
+ stdout: "ignore",
121
+ }).unref();
122
+ for (let attempt = 0; attempt < BOOT_POLL_ATTEMPTS; attempt++) {
123
+ const serial = await findEmulatorSerial({avd, tag});
124
+ if (serial) {
125
+ const {stdout} = await run({
126
+ cmd: ["adb", "-s", serial, "shell", "getprop", "sys.boot_completed"],
127
+ isQuiet: true,
128
+ });
129
+ if (stdout.trim() === "1") {
130
+ return serial;
131
+ }
132
+ }
133
+ await Bun.sleep(BOOT_POLL_INTERVAL_MS);
134
+ }
135
+ return fail({message: `Emulator ${avd} did not finish booting`, tag});
136
+ };
137
+
138
+ // The emulator reaches the host's API and Metro over localhost via adb reverse.
139
+ export const reversePorts = async ({
140
+ serial,
141
+ ports,
142
+ tag,
143
+ }: {
144
+ serial: string;
145
+ ports: number[];
146
+ tag: string;
147
+ }): Promise<void> => {
148
+ for (const port of ports) {
149
+ await runOrFail({
150
+ cmd: ["adb", "-s", serial, "reverse", `tcp:${port}`, `tcp:${port}`],
151
+ isQuiet: true,
152
+ tag,
153
+ });
154
+ }
155
+ };
156
+
157
+ const writeAndroidDevMenuPrefs = async ({
158
+ serial,
159
+ androidPackage,
160
+ devMenu,
161
+ tag,
162
+ }: {
163
+ serial: string;
164
+ androidPackage: string;
165
+ devMenu: DevMenuMode;
166
+ tag: string;
167
+ }): Promise<void> => {
168
+ const prefs =
169
+ devMenu === "hideAll"
170
+ ? {isOnboardingFinished: true, showFab: false, showsAtLaunch: false}
171
+ : {isOnboardingFinished: true};
172
+ const xml = [
173
+ "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>",
174
+ "<map>",
175
+ ...Object.entries(prefs).map(([name, value]) => `<boolean name="${name}" value="${value}" />`),
176
+ "</map>",
177
+ ].join("");
178
+ // Dev client builds are debuggable, so run-as can write the app's private prefs. Base64
179
+ // avoids shell-quoting the XML; androidPackage is validated by getExpoInfo.
180
+ const encoded = Buffer.from(xml).toString("base64");
181
+ await runOrFail({
182
+ cmd: [
183
+ "adb",
184
+ "-s",
185
+ serial,
186
+ "shell",
187
+ `run-as ${androidPackage} sh -c 'mkdir -p shared_prefs && echo ${encoded} | base64 -d > shared_prefs/${DEV_MENU_PREFS}.xml'`,
188
+ ],
189
+ isQuiet: true,
190
+ tag,
191
+ });
192
+ };
193
+
194
+ // Launches the dev client straight into a Metro server. iOS passes the launcher's
195
+ // --initialUrl argument because opening an exp+ URL shows an "Open in ...?" system prompt
196
+ // that automation cannot dismiss.
197
+ export const launchDevClientIntoMetro = async ({
198
+ platform,
199
+ deviceId,
200
+ expo,
201
+ metroPort,
202
+ devMenu,
203
+ tag,
204
+ }: {
205
+ platform: "ios" | "android";
206
+ deviceId: string;
207
+ expo: ExpoInfo;
208
+ metroPort: number;
209
+ devMenu: DevMenuMode;
210
+ tag: string;
211
+ }): Promise<void> => {
212
+ const metroUrl = `http://localhost:${metroPort}`;
213
+ if (platform === "ios") {
214
+ const prefs: [string, string][] =
215
+ devMenu === "hideAll"
216
+ ? [
217
+ ["EXDevMenuIsOnboardingFinished", "YES"],
218
+ ["EXDevMenuShowFloatingActionButton", "NO"],
219
+ ["EXDevMenuShowsAtLaunch", "NO"],
220
+ ]
221
+ : [["EXDevMenuIsOnboardingFinished", "YES"]];
222
+ for (const [key, value] of prefs) {
223
+ await runOrFail({
224
+ cmd: [
225
+ "xcrun",
226
+ "simctl",
227
+ "spawn",
228
+ deviceId,
229
+ "defaults",
230
+ "write",
231
+ expo.bundleId,
232
+ key,
233
+ "-bool",
234
+ value,
235
+ ],
236
+ isQuiet: true,
237
+ tag,
238
+ });
239
+ }
240
+ await runOrFail({
241
+ cmd: [
242
+ "xcrun",
243
+ "simctl",
244
+ "launch",
245
+ "--terminate-running-process",
246
+ deviceId,
247
+ expo.bundleId,
248
+ "--initialUrl",
249
+ metroUrl,
250
+ ],
251
+ isQuiet: true,
252
+ tag,
253
+ });
254
+ return;
255
+ }
256
+
257
+ // The app caches its prefs in memory, so stop it before rewriting them.
258
+ await runOrFail({
259
+ cmd: ["adb", "-s", deviceId, "shell", "am", "force-stop", expo.androidPackage],
260
+ isQuiet: true,
261
+ tag,
262
+ });
263
+ await writeAndroidDevMenuPrefs({
264
+ androidPackage: expo.androidPackage,
265
+ devMenu,
266
+ serial: deviceId,
267
+ tag,
268
+ });
269
+ const url = `exp+${expo.slug}://expo-development-client/?url=${encodeURIComponent(metroUrl)}`;
270
+ await runOrFail({
271
+ cmd: [
272
+ "adb",
273
+ "-s",
274
+ deviceId,
275
+ "shell",
276
+ "am",
277
+ "start",
278
+ "-a",
279
+ "android.intent.action.VIEW",
280
+ "-d",
281
+ `'${url}'`,
282
+ ],
283
+ isQuiet: true,
284
+ tag,
285
+ });
286
+ };
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bun
2
+ //
3
+ // Render the Google Play high-res store icon (512x512, full-bleed square): the brand mark in
4
+ // `onSurface` on the brand `surface`. Play applies its own corner mask and shadow, so the image
5
+ // has square corners and no shadow.
6
+ //
7
+ // Usage:
8
+ // bun run store:icon <app>
9
+ //
10
+ // Needs `brand.iconSvg` in storeAssets.config.json.
11
+ // Output: <appDir>/storeScreenshots/playIcon.png
12
+
13
+ import {existsSync, mkdirSync} from "node:fs";
14
+ import {join, resolve} from "node:path";
15
+ import {chromium} from "@playwright/test";
16
+ import {fail as failTagged, getApp, loadConfig, REPO_ROOT, say as sayTagged} from "./config";
17
+
18
+ const TAG = "store:icon";
19
+ const SIZE = 512;
20
+ // Keeps the mark inside Play's masked safe area.
21
+ const MARK_SIZE = 320;
22
+ const DEFAULT_MARK_VIEWBOX = "0 0 64 64";
23
+
24
+ const say = (message: string): void => sayTagged({message, tag: TAG});
25
+ const fail = (message: string): never => failTagged({message, tag: TAG});
26
+
27
+ const main = async (): Promise<void> => {
28
+ const config = await loadConfig({tag: TAG});
29
+ const app = getApp({appKey: process.argv[2], config, tag: TAG});
30
+ const brand = config.brand;
31
+ if (!brand) {
32
+ fail("storeAssets.config.json has no `brand` section");
33
+ return;
34
+ }
35
+ const svgPath = resolve(REPO_ROOT, brand.iconSvg);
36
+ if (!existsSync(svgPath)) {
37
+ fail(`Missing ${svgPath}`);
38
+ return;
39
+ }
40
+
41
+ let svg = await Bun.file(svgPath).text();
42
+ // A logo SVG can supply the mark: its first N <path>s, re-wrapped in the mark's viewBox.
43
+ // Root fill="none" matters because stroke-only paths would otherwise fill black.
44
+ if (brand.iconPathCount) {
45
+ // Matches both self-closing <path .../> and <path ...></path> elements.
46
+ const paths = (svg.match(/<path\b[^>]*?(?:\/>|>[\s\S]*?<\/path>)/g) ?? []).slice(
47
+ 0,
48
+ brand.iconPathCount
49
+ );
50
+ if (paths.length !== brand.iconPathCount) {
51
+ fail(`Expected ${brand.iconPathCount} <path> elements in ${brand.iconSvg}`);
52
+ }
53
+ const viewBox = brand.iconViewBox ?? DEFAULT_MARK_VIEWBOX;
54
+ svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none">${paths.join("")}</svg>`;
55
+ }
56
+ for (const color of brand.iconRecolor ?? []) {
57
+ svg = svg.replaceAll(`"${color}"`, `"${brand.onSurface}"`);
58
+ }
59
+ const mark = Buffer.from(svg).toString("base64");
60
+
61
+ const outDir = join(REPO_ROOT, app.appDir, "storeScreenshots");
62
+ mkdirSync(outDir, {recursive: true});
63
+ const outPath = join(outDir, "playIcon.png");
64
+ const browser = await chromium.launch();
65
+ try {
66
+ const page = await browser.newPage({
67
+ deviceScaleFactor: 1,
68
+ viewport: {height: SIZE, width: SIZE},
69
+ });
70
+ await page.setContent(`<!doctype html>
71
+ <body style="margin:0;width:${SIZE}px;height:${SIZE}px;background:${brand.surface};display:flex;align-items:center;justify-content:center">
72
+ <img style="width:${MARK_SIZE}px;height:${MARK_SIZE}px" src="data:image/svg+xml;base64,${mark}" alt="">
73
+ </body>`);
74
+ await page.screenshot({path: outPath});
75
+ } finally {
76
+ await browser.close();
77
+ }
78
+ say(`Saved ${outPath} (${SIZE}x${SIZE})`);
79
+ };
80
+
81
+ await main();
@@ -0,0 +1,21 @@
1
+ appId: ${APP_ID}
2
+ ---
3
+ # Returns to the home screen from a stack screen opened off home.
4
+ # Android: system back. iOS has no system back, so restart the app; the dev client reloads the
5
+ # last Metro bundle and the session persists. (A cold relaunch on Android leaves the dev client
6
+ # on a blank screen.)
7
+ - runFlow:
8
+ when:
9
+ platform: Android
10
+ commands:
11
+ - back
12
+ - runFlow:
13
+ when:
14
+ platform: iOS
15
+ commands:
16
+ - stopApp
17
+ - launchApp
18
+ - extendedWaitUntil:
19
+ visible:
20
+ id: ${HOME_SCREEN_ID}
21
+ timeout: 60000
@@ -0,0 +1,27 @@
1
+ appId: ${APP_ID}
2
+ ---
3
+ # Uses the @terreno/ui LoginScreen testIDs (login-screen-email-input, -password-input,
4
+ # -submit-button). Give a custom login form the same testIDs.
5
+ - tapOn:
6
+ id: "login-screen-email-input"
7
+ - inputText: ${EMAIL}
8
+ - tapOn:
9
+ id: "login-screen-password-input"
10
+ - inputText: ${PASSWORD}
11
+ - hideKeyboard
12
+ - tapOn:
13
+ id: "login-screen-submit-button"
14
+ - extendedWaitUntil:
15
+ visible:
16
+ id: ${HOME_SCREEN_ID}
17
+ timeout: 30000
18
+ # iOS offers to save the password a moment after login; give it time to appear.
19
+ - extendedWaitUntil:
20
+ visible: "Not Now"
21
+ timeout: 5000
22
+ optional: true
23
+ - runFlow:
24
+ when:
25
+ visible: "Not Now"
26
+ commands:
27
+ - tapOn: "Not Now"
@@ -0,0 +1,26 @@
1
+ appId: ${APP_ID}
2
+ ---
3
+ # Waits for the app and signs in if needed. scripts/screenshots.ts has already launched the
4
+ # dev client into the screenshot Metro server with the dev menu overlays disabled, and supplies
5
+ # APP_ID, EMAIL, PASSWORD, and HOME_SCREEN_ID.
6
+ # The first production bundle can take a minute to build.
7
+ - extendedWaitUntil:
8
+ visible:
9
+ id: "login-screen|${HOME_SCREEN_ID}"
10
+ timeout: 180000
11
+ # The app can re-render while it boots; let it settle before branching on login.
12
+ - waitForAnimationToEnd:
13
+ timeout: 5000
14
+ - extendedWaitUntil:
15
+ visible:
16
+ id: "login-screen-email-input|${HOME_SCREEN_ID}"
17
+ timeout: 30000
18
+ - runFlow:
19
+ when:
20
+ visible:
21
+ id: "login-screen-email-input"
22
+ file: login.yaml
23
+ - extendedWaitUntil:
24
+ visible:
25
+ id: ${HOME_SCREEN_ID}
26
+ timeout: 30000