@unotest/mobile 0.8.1 → 0.8.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,78 @@ All notable changes to `@unotest/mobile` will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.8.3] — 2026-05-18
8
+
9
+ ### Added — README onboarding protocol for AI agents
10
+
11
+ - **New top-of-README section.** A developer dropping the npm page
12
+ link into their AI agent's context (Cursor, Claude Desktop, Claude
13
+ Code in another project, etc.) now gets a 5-step protocol explicitly
14
+ addressed to the agent: verify host → pick working directory → ask
15
+ for `.app` path → run interactive setup in user's terminal → hand off
16
+ to Claude Code in the same directory. Spells out where the agent's
17
+ responsibility ends (not test authoring; that's a separate Claude
18
+ Code session via MCP) and what NOT to run via its own `Bash`-style
19
+ shell tool (the install command is interactive — needs a real TTY
20
+ for the simulator picker; tool-spawned shells either hang or fall
21
+ back to a non-interactive code path that can confuse the user).
22
+ - The working-directory step is explicit because `init` drops
23
+ `unotest/`, `.mcp.json`, `.claude/skills/`, and `AGENTS.md` into
24
+ cwd, and `.mcp.json`'s location determines what Claude Code sees on
25
+ its next launch. Recommends the app repo root by default; monorepo
26
+ root for monorepos; explicit warning that a separate test repo
27
+ divorces tests from the app and should be chosen only when the user
28
+ asks for it.
29
+
30
+ ## [0.8.2] — 2026-05-18
31
+
32
+ ### Fixed — `type()` no longer drops 1-3 characters mid-string
33
+
34
+ - **What broke.** `WdaDriver.type` issued `XCUIElement.typeText` at WDA's
35
+ default typing frequency (~60 letters/sec) into the simulator's iOS
36
+ keyboard. Under host contention (Claude Code + WDA build + Simulator
37
+ all running on the same Mac), the iOS keyboard layer couldn't keep
38
+ up and randomly dropped 1-3 characters from the middle of the string
39
+ — observed live as `petr@volkov.io` → `pr@volkov.io`, `Qwerty34##`
40
+ → `Qwery34##`. P4 / S8 had only addressed the unrelated *wrong-
41
+ keyboard-layout* failure (Cyrillic keyboard eating Latin chars
42
+ wholesale); this race is a separate failure mode that survived.
43
+ - **Fix 1 — `ConnectHardwareKeyboard` pinned OFF on install.**
44
+ The `pinEnglishKeyboard` step (S8) now also writes
45
+ `ConnectHardwareKeyboard = false` into the Simulator.app preferences
46
+ on every install. When the toggle is ON (the default on a freshly
47
+ created sim, controlled by ⌘K inside the sim), iOS suppresses the
48
+ soft keyboard and routes keys through the host's physical keyboard
49
+ path — typeText then races against keyboard presentation. Pinning
50
+ OFF guarantees the soft keyboard renders and stays visible.
51
+ Idempotent. Reuses the existing `pinKeyboard: false` opt-out flag.
52
+ - **Fix 2 — `WDA_TYPING_FREQUENCY` env (default `8`).** `WdaDriver.type`
53
+ now passes `frequency: <env>` in the POST `/element/{id}/value`
54
+ body, which WDA forwards to `fb_typeText:withFrequency:`. 8 cps is
55
+ conservative-but-still-usable: typical credentials (`petr@volkov.io`,
56
+ `Qwerty34##`) type in ~2s instead of the unreliable ~0.3s. Bump
57
+ higher if you trust your host's headroom; lower if you're seeing
58
+ drops in CI under heavy load. Validated as a positive integer at
59
+ `loadEnv` time.
60
+ - **Why not pasteboard / per-char paste.** Researched approaches the
61
+ XCUITest community has tried (`UIPasteboard` + double-tap-menu paste,
62
+ per-char `typeKey:` calls, `setValue` direct-set): pasteboard paste
63
+ is **43% slower and flaky** (per iammike.org / 2022); per-char
64
+ `typeKey:` is only 3% faster than throttled `typeText`, complex to
65
+ wire reliably; `setValue` on native iOS text fields routes back to
66
+ `typeText` internally — there is no Apple-blessed "set text without
67
+ keyboard" API. WDA's own `maxTypingFrequency` is the documented knob.
68
+ - **Tests:** +0 net. Modified 1 existing assertion to verify
69
+ `frequency: 8` lands in the request body. The hardware-keyboard
70
+ flip rides on the same `pinEnglishKeyboard` call site, so existing
71
+ call-sequence assertions still hold. Total 570.
72
+
73
+ ### Added — `WDA_TYPING_FREQUENCY` env var
74
+
75
+ - Letters-per-second passed to WDA's `fb_typeText:withFrequency:` on
76
+ every `type(...)` call. Default `8`. See the fix entry above for
77
+ rationale on the conservative default.
78
+
7
79
  ## [0.8.1] — 2026-05-18
8
80
 
9
81
  ### Fixed — `init` pins the exact `@unotest/mobile` version in `.mcp.json`
package/README.md CHANGED
@@ -5,6 +5,74 @@ that lets Claude (or any MCP client) drive your iOS Simulator and write
5
5
  real test scenarios for your app — works with any backend stack
6
6
  (Node/Python/Rails/Go/Java), no JS expertise required from you.
7
7
 
8
+ ## If you're an AI agent helping a user set this up
9
+
10
+ *(Humans: skip this section, jump to [Quick start](#quick-start).)*
11
+
12
+ You were given this page because a developer wants `@unotest/mobile`
13
+ set up in their project. Follow this protocol exactly — don't improvise.
14
+
15
+ **1. Verify the host.** Run `xcrun simctl list devices` via your shell
16
+ tool. If it errors with "command not found" or similar, the user isn't
17
+ on macOS or is missing Xcode CLI tools. Stop and tell them: "This is
18
+ iOS-only — Apple licensing prevents iOS simulators from running on
19
+ Linux/Windows. You'll need macOS with Xcode installed." Also verify
20
+ `node --version` ≥ 20.
21
+
22
+ **2. Pick a working directory with the user.** Setup writes
23
+ `unotest/` (test scaffold), `.mcp.json` (Claude Code config),
24
+ `.claude/skills/` (skill the in-project agent reads), and an
25
+ `AGENTS.md` into the current directory, and updates `.gitignore` if
26
+ present. Where these land determines what Claude Code sees later, so
27
+ ask the user explicitly:
28
+
29
+ - **Default — the app's repo root.** Same directory as their
30
+ `package.json` (RN/Expo) or where their `.xcodeproj` lives (native).
31
+ Tests live next to app code, ship via the same git history, work
32
+ cleanly in CI.
33
+ - **Monorepo:** the monorepo root, not the app subfolder. Claude Code
34
+ is typically opened at the monorepo root, and `.mcp.json` must live
35
+ there to be auto-discovered.
36
+ - **Separate test repo:** any directory the user picks. Choose this
37
+ only if they explicitly want tests divorced from the app.
38
+
39
+ Once the user picks, have them `cd` there. All commands below run
40
+ from that directory.
41
+
42
+ **3. Ask the user for the `.app` path.** `@unotest/mobile` drives a
43
+ real installed app — it needs the path to a built `.app` bundle.
44
+ Ask: *"What's the absolute path to your built `.app`?"* Common
45
+ locations:
46
+ - React Native / Expo: `./ios/build/Build/Products/Debug-iphonesimulator/<App>.app`
47
+ after `npx expo run:ios` or `pod install && xcodebuild`.
48
+ - Native Swift: open Xcode → Product → Show Build Folder → `Products/Debug-iphonesimulator/`.
49
+
50
+ If they don't have one yet, tell them to build first; don't guess
51
+ paths.
52
+
53
+ **4. Tell the user to run the setup command in the chosen directory.**
54
+ **Do not run this yourself via Bash** — it's interactive (simulator
55
+ picker, permission prompts) and needs a real TTY. Your shell tool
56
+ will hang or get a non-interactive fallback. Give them this exact
57
+ line to paste (from the directory picked in step 2):
58
+
59
+ ```bash
60
+ npx @unotest/mobile@latest install <their-app-path> --update-env
61
+ ```
62
+
63
+ They'll see: env check → file scaffold → simulator picker (arrow keys)
64
+ → permission grant prompt → install logs. Wait for the line
65
+ `✓ Installed <bundle.id>`. That's the success signal.
66
+
67
+ **5. Hand off to Claude Code.** Once install finishes, instruct the
68
+ user: *"Open Claude Code in **the same directory you ran setup in**,
69
+ then ask it to write your first test."* A separate Claude Code session
70
+ (not you) connects to the MCP server bootstrapped in step 4 and takes
71
+ over from there. Your job ends here — you're setup, not test authoring.
72
+
73
+ If any step fails, surface the actual error verbatim to the user and
74
+ stop. Don't try alternative paths.
75
+
8
76
  ## Quick start
9
77
 
10
78
  ```bash
@@ -30,18 +98,18 @@ you review the resulting `.js` files in `unotest/e2e/`.
30
98
  ┌─────────────┐ MCP (stdio) ┌──────────────────┐
31
99
  │ Claude Code │ ◀────────────▶ │ @unotest/mobile │
32
100
  └─────────────┘ tool calls │ (this package) │
33
- └────────┬─────────┘
34
- │ HTTP
35
- ┌────────▼──────────┐
36
- │ WebDriverAgent │
37
- │ (Apple's XCUI │
38
- │ driver, on sim) │
39
- └────────┬──────────┘
40
-
41
- ┌────────▼──────────┐
42
- │ iOS Simulator │
43
- │ Your .app │
44
- └───────────────────┘
101
+ └────────┬─────────┘
102
+ │ HTTP
103
+ ┌────────▼──────────┐
104
+ │ WebDriverAgent │
105
+ │ (Apple's XCUI │
106
+ │ driver, on sim) │
107
+ └────────┬──────────┘
108
+
109
+ ┌────────▼──────────┐
110
+ │ iOS Simulator │
111
+ │ Your .app │
112
+ └───────────────────┘
45
113
  ```
46
114
 
47
115
  - **The agent sees the accessibility tree, not screenshots.** WDA
@@ -240,6 +240,15 @@ var EnvSchema = z.object({
240
240
  // Implicit auto-wait on selector-bearing actions (D-18).
241
241
  WDA_DEFAULT_ACTION_WAIT_MS: z.string().default("2000").transform((v) => Number.parseInt(v, 10)),
242
242
  WDA_DEFAULT_WAITFOR_TIMEOUT_MS: z.string().default("10000").transform((v) => Number.parseInt(v, 10)),
243
+ // Typing frequency (letters/sec) passed through to WDA's
244
+ // `fb_typeText:withFrequency:` on every `type(...)` call. WDA's own
245
+ // default is its `maxTypingFrequency` (typically 60 cps) which under
246
+ // contention from the host (Claude Code + WDA build + sim) races
247
+ // against the iOS keyboard presentation and randomly drops 1-3 chars
248
+ // mid-string. 8 cps is the conservative-but-still-usable value that
249
+ // empirically eliminates drops without making typing visibly slow
250
+ // (`petr@volkov.io` types in ~2s instead of ~0.3s).
251
+ WDA_TYPING_FREQUENCY: z.string().default("8").transform((v) => Number.parseInt(v, 10)),
243
252
  // TTL for paused-failed runtimes before auto-abort (D-17). Default 30 min.
244
253
  PAUSED_RUNTIME_TTL_MS: z.string().default("1800000").transform((v) => Number.parseInt(v, 10))
245
254
  });
@@ -287,6 +296,11 @@ ${issues}`
287
296
  );
288
297
  }
289
298
  }
299
+ if (!Number.isFinite(raw.WDA_TYPING_FREQUENCY) || raw.WDA_TYPING_FREQUENCY <= 0) {
300
+ throw new Error(
301
+ `WDA_TYPING_FREQUENCY must be a positive integer (letters/sec). Got "${process.env.WDA_TYPING_FREQUENCY}".`
302
+ );
303
+ }
290
304
  cached = {
291
305
  ...raw,
292
306
  simPool,
@@ -294,6 +308,7 @@ ${issues}`
294
308
  wdaPortBySlot,
295
309
  defaultActionWaitMs: raw.WDA_DEFAULT_ACTION_WAIT_MS,
296
310
  defaultWaitForTimeoutMs: raw.WDA_DEFAULT_WAITFOR_TIMEOUT_MS,
311
+ wdaTypingFrequency: raw.WDA_TYPING_FREQUENCY,
297
312
  pausedRuntimeTtlMs: raw.PAUSED_RUNTIME_TTL_MS,
298
313
  explorationsDir: raw.EXPLORATIONS_DIR ?? `${raw.ARTIFACTS_DIR}/explorations`
299
314
  };
@@ -636,6 +651,17 @@ async function pinEnglishKeyboardSim(udid) {
636
651
  "-string",
637
652
  "en_US"
638
653
  ]);
654
+ await exec2("xcrun", [
655
+ "simctl",
656
+ "spawn",
657
+ udid,
658
+ "defaults",
659
+ "write",
660
+ "com.apple.iphonesimulator.SimulatorApp",
661
+ "ConnectHardwareKeyboard",
662
+ "-bool",
663
+ "false"
664
+ ]);
639
665
  }
640
666
  __name(pinEnglishKeyboardSim, "pinEnglishKeyboardSim");
641
667
 
@@ -690,8 +716,11 @@ var SimctlAdapter = class {
690
716
  async privacyGrant(udid, service, bundleId) {
691
717
  return privacyGrantSim(udid, service, bundleId);
692
718
  }
693
- /** S8 — pin the sim's keyboard to en_US@QWERTY so WDA's typeText
694
- * doesn't drop Latin chars when the sim was last on a Cyrillic layout. */
719
+ /** S8 — prepare the sim's keyboard for reliable `typeText`. Pins
720
+ * layout to en_US@QWERTY (otherwise a leftover Cyrillic layout
721
+ * drops Latin chars) and forces "Connect Hardware Keyboard" OFF
722
+ * (otherwise the soft keyboard is suppressed and `typeText` races
723
+ * against keyboard presentation, dropping 1-3 chars mid-string). */
695
724
  async pinEnglishKeyboard(udid) {
696
725
  return pinEnglishKeyboardSim(udid);
697
726
  }
@@ -1277,7 +1306,8 @@ Install it first: \`npx unotest-mobile install <path-to-.app>\` (or set APP_PATH
1277
1306
  await this.withFreshSession(slot, async (session) => {
1278
1307
  const elementId = await this.resolveElementId(slot, selector);
1279
1308
  await session.client.setElementValue(session.getSessionId(), elementId, {
1280
- value: Array.from(text)
1309
+ value: Array.from(text),
1310
+ frequency: this.deps.typingFrequency
1281
1311
  });
1282
1312
  });
1283
1313
  }
@@ -1578,6 +1608,7 @@ function createDriver(cfg) {
1578
1608
  simBySlot: cfg.env.simBySlot,
1579
1609
  wdaPortBySlot: cfg.env.wdaPortBySlot,
1580
1610
  appBundleId: cfg.env.APP_BUNDLE_ID,
1611
+ typingFrequency: cfg.env.wdaTypingFrequency,
1581
1612
  binaryProvider
1582
1613
  });
1583
1614
  }
@@ -6819,7 +6850,7 @@ async function installApp2(opts, deps) {
6819
6850
  }
6820
6851
  }
6821
6852
  if (opts.pinKeyboard !== false) {
6822
- deps.logger.info(`[${slot}] pinning keyboard to en_US@QWERTY`);
6853
+ deps.logger.info(`[${slot}] pinning keyboard (en_US@QWERTY, soft only)`);
6823
6854
  await deps.simctl.pinEnglishKeyboard(sim.udid);
6824
6855
  }
6825
6856
  let launched = false;
@@ -236,6 +236,15 @@ var EnvSchema = z.object({
236
236
  // Implicit auto-wait on selector-bearing actions (D-18).
237
237
  WDA_DEFAULT_ACTION_WAIT_MS: z.string().default("2000").transform((v) => Number.parseInt(v, 10)),
238
238
  WDA_DEFAULT_WAITFOR_TIMEOUT_MS: z.string().default("10000").transform((v) => Number.parseInt(v, 10)),
239
+ // Typing frequency (letters/sec) passed through to WDA's
240
+ // `fb_typeText:withFrequency:` on every `type(...)` call. WDA's own
241
+ // default is its `maxTypingFrequency` (typically 60 cps) which under
242
+ // contention from the host (Claude Code + WDA build + sim) races
243
+ // against the iOS keyboard presentation and randomly drops 1-3 chars
244
+ // mid-string. 8 cps is the conservative-but-still-usable value that
245
+ // empirically eliminates drops without making typing visibly slow
246
+ // (`petr@volkov.io` types in ~2s instead of ~0.3s).
247
+ WDA_TYPING_FREQUENCY: z.string().default("8").transform((v) => Number.parseInt(v, 10)),
239
248
  // TTL for paused-failed runtimes before auto-abort (D-17). Default 30 min.
240
249
  PAUSED_RUNTIME_TTL_MS: z.string().default("1800000").transform((v) => Number.parseInt(v, 10))
241
250
  });
@@ -283,6 +292,11 @@ ${issues}`
283
292
  );
284
293
  }
285
294
  }
295
+ if (!Number.isFinite(raw.WDA_TYPING_FREQUENCY) || raw.WDA_TYPING_FREQUENCY <= 0) {
296
+ throw new Error(
297
+ `WDA_TYPING_FREQUENCY must be a positive integer (letters/sec). Got "${process.env.WDA_TYPING_FREQUENCY}".`
298
+ );
299
+ }
286
300
  cached = {
287
301
  ...raw,
288
302
  simPool,
@@ -290,6 +304,7 @@ ${issues}`
290
304
  wdaPortBySlot,
291
305
  defaultActionWaitMs: raw.WDA_DEFAULT_ACTION_WAIT_MS,
292
306
  defaultWaitForTimeoutMs: raw.WDA_DEFAULT_WAITFOR_TIMEOUT_MS,
307
+ wdaTypingFrequency: raw.WDA_TYPING_FREQUENCY,
293
308
  pausedRuntimeTtlMs: raw.PAUSED_RUNTIME_TTL_MS,
294
309
  explorationsDir: raw.EXPLORATIONS_DIR ?? `${raw.ARTIFACTS_DIR}/explorations`
295
310
  };
@@ -632,6 +647,17 @@ async function pinEnglishKeyboardSim(udid) {
632
647
  "-string",
633
648
  "en_US"
634
649
  ]);
650
+ await exec2("xcrun", [
651
+ "simctl",
652
+ "spawn",
653
+ udid,
654
+ "defaults",
655
+ "write",
656
+ "com.apple.iphonesimulator.SimulatorApp",
657
+ "ConnectHardwareKeyboard",
658
+ "-bool",
659
+ "false"
660
+ ]);
635
661
  }
636
662
  __name(pinEnglishKeyboardSim, "pinEnglishKeyboardSim");
637
663
 
@@ -686,8 +712,11 @@ var SimctlAdapter = class {
686
712
  async privacyGrant(udid, service, bundleId) {
687
713
  return privacyGrantSim(udid, service, bundleId);
688
714
  }
689
- /** S8 — pin the sim's keyboard to en_US@QWERTY so WDA's typeText
690
- * doesn't drop Latin chars when the sim was last on a Cyrillic layout. */
715
+ /** S8 — prepare the sim's keyboard for reliable `typeText`. Pins
716
+ * layout to en_US@QWERTY (otherwise a leftover Cyrillic layout
717
+ * drops Latin chars) and forces "Connect Hardware Keyboard" OFF
718
+ * (otherwise the soft keyboard is suppressed and `typeText` races
719
+ * against keyboard presentation, dropping 1-3 chars mid-string). */
691
720
  async pinEnglishKeyboard(udid) {
692
721
  return pinEnglishKeyboardSim(udid);
693
722
  }
@@ -1273,7 +1302,8 @@ Install it first: \`npx unotest-mobile install <path-to-.app>\` (or set APP_PATH
1273
1302
  await this.withFreshSession(slot, async (session) => {
1274
1303
  const elementId = await this.resolveElementId(slot, selector);
1275
1304
  await session.client.setElementValue(session.getSessionId(), elementId, {
1276
- value: Array.from(text)
1305
+ value: Array.from(text),
1306
+ frequency: this.deps.typingFrequency
1277
1307
  });
1278
1308
  });
1279
1309
  }
@@ -1574,6 +1604,7 @@ function createDriver(cfg) {
1574
1604
  simBySlot: cfg.env.simBySlot,
1575
1605
  wdaPortBySlot: cfg.env.wdaPortBySlot,
1576
1606
  appBundleId: cfg.env.APP_BUNDLE_ID,
1607
+ typingFrequency: cfg.env.wdaTypingFrequency,
1577
1608
  binaryProvider
1578
1609
  });
1579
1610
  }
@@ -272,6 +272,11 @@ APP_BUNDLE_ID=com.example.myapp
272
272
  WDA_PORTS=A=8100,B=8101
273
273
  # WDA_DEFAULT_ACTION_WAIT_MS=2000
274
274
  # WDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000
275
+ # Letters/sec for type() \u2014 WDA's fb_typeText:withFrequency:. Conservative
276
+ # default avoids char-drop races against the iOS keyboard layer when the
277
+ # host is under load. Bump higher (e.g. 20) on a quiet machine; lower
278
+ # (e.g. 4) if you still see drops in CI.
279
+ # WDA_TYPING_FREQUENCY=8
275
280
 
276
281
  # --- Artifacts / sessions / paused-runtime TTL -----------------------------
277
282
  # Defaults are sensible \u2014 uncomment only to override.
@@ -84,6 +84,15 @@ var EnvSchema = z.object({
84
84
  // Implicit auto-wait on selector-bearing actions (D-18).
85
85
  WDA_DEFAULT_ACTION_WAIT_MS: z.string().default("2000").transform((v) => Number.parseInt(v, 10)),
86
86
  WDA_DEFAULT_WAITFOR_TIMEOUT_MS: z.string().default("10000").transform((v) => Number.parseInt(v, 10)),
87
+ // Typing frequency (letters/sec) passed through to WDA's
88
+ // `fb_typeText:withFrequency:` on every `type(...)` call. WDA's own
89
+ // default is its `maxTypingFrequency` (typically 60 cps) which under
90
+ // contention from the host (Claude Code + WDA build + sim) races
91
+ // against the iOS keyboard presentation and randomly drops 1-3 chars
92
+ // mid-string. 8 cps is the conservative-but-still-usable value that
93
+ // empirically eliminates drops without making typing visibly slow
94
+ // (`petr@volkov.io` types in ~2s instead of ~0.3s).
95
+ WDA_TYPING_FREQUENCY: z.string().default("8").transform((v) => Number.parseInt(v, 10)),
87
96
  // TTL for paused-failed runtimes before auto-abort (D-17). Default 30 min.
88
97
  PAUSED_RUNTIME_TTL_MS: z.string().default("1800000").transform((v) => Number.parseInt(v, 10))
89
98
  });
@@ -131,6 +140,11 @@ ${issues}`
131
140
  );
132
141
  }
133
142
  }
143
+ if (!Number.isFinite(raw.WDA_TYPING_FREQUENCY) || raw.WDA_TYPING_FREQUENCY <= 0) {
144
+ throw new Error(
145
+ `WDA_TYPING_FREQUENCY must be a positive integer (letters/sec). Got "${process.env.WDA_TYPING_FREQUENCY}".`
146
+ );
147
+ }
134
148
  cached = {
135
149
  ...raw,
136
150
  simPool,
@@ -138,6 +152,7 @@ ${issues}`
138
152
  wdaPortBySlot,
139
153
  defaultActionWaitMs: raw.WDA_DEFAULT_ACTION_WAIT_MS,
140
154
  defaultWaitForTimeoutMs: raw.WDA_DEFAULT_WAITFOR_TIMEOUT_MS,
155
+ wdaTypingFrequency: raw.WDA_TYPING_FREQUENCY,
141
156
  pausedRuntimeTtlMs: raw.PAUSED_RUNTIME_TTL_MS,
142
157
  explorationsDir: raw.EXPLORATIONS_DIR ?? `${raw.ARTIFACTS_DIR}/explorations`
143
158
  };
@@ -364,6 +379,17 @@ async function pinEnglishKeyboardSim(udid) {
364
379
  "-string",
365
380
  "en_US"
366
381
  ]);
382
+ await exec("xcrun", [
383
+ "simctl",
384
+ "spawn",
385
+ udid,
386
+ "defaults",
387
+ "write",
388
+ "com.apple.iphonesimulator.SimulatorApp",
389
+ "ConnectHardwareKeyboard",
390
+ "-bool",
391
+ "false"
392
+ ]);
367
393
  }
368
394
  __name(pinEnglishKeyboardSim, "pinEnglishKeyboardSim");
369
395
 
@@ -418,8 +444,11 @@ var SimctlAdapter = class {
418
444
  async privacyGrant(udid, service, bundleId) {
419
445
  return privacyGrantSim(udid, service, bundleId);
420
446
  }
421
- /** S8 — pin the sim's keyboard to en_US@QWERTY so WDA's typeText
422
- * doesn't drop Latin chars when the sim was last on a Cyrillic layout. */
447
+ /** S8 — prepare the sim's keyboard for reliable `typeText`. Pins
448
+ * layout to en_US@QWERTY (otherwise a leftover Cyrillic layout
449
+ * drops Latin chars) and forces "Connect Hardware Keyboard" OFF
450
+ * (otherwise the soft keyboard is suppressed and `typeText` races
451
+ * against keyboard presentation, dropping 1-3 chars mid-string). */
423
452
  async pinEnglishKeyboard(udid) {
424
453
  return pinEnglishKeyboardSim(udid);
425
454
  }
@@ -912,6 +941,11 @@ APP_BUNDLE_ID=com.example.myapp
912
941
  WDA_PORTS=A=8100,B=8101
913
942
  # WDA_DEFAULT_ACTION_WAIT_MS=2000
914
943
  # WDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000
944
+ # Letters/sec for type() \u2014 WDA's fb_typeText:withFrequency:. Conservative
945
+ # default avoids char-drop races against the iOS keyboard layer when the
946
+ # host is under load. Bump higher (e.g. 20) on a quiet machine; lower
947
+ # (e.g. 4) if you still see drops in CI.
948
+ # WDA_TYPING_FREQUENCY=8
915
949
 
916
950
  # --- Artifacts / sessions / paused-runtime TTL -----------------------------
917
951
  # Defaults are sensible \u2014 uncomment only to override.
@@ -1324,7 +1358,7 @@ async function installApp2(opts, deps) {
1324
1358
  }
1325
1359
  }
1326
1360
  if (opts.pinKeyboard !== false) {
1327
- deps.logger.info(`[${slot}] pinning keyboard to en_US@QWERTY`);
1361
+ deps.logger.info(`[${slot}] pinning keyboard (en_US@QWERTY, soft only)`);
1328
1362
  await deps.simctl.pinEnglishKeyboard(sim.udid);
1329
1363
  }
1330
1364
  let launched = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unotest/mobile",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "AI-native E2E testing for iOS React Native apps. MCP server + CLI runner + JS-DSL scenarios.",
5
5
  "license": "MIT",
6
6
  "type": "module",