craft-native 0.0.217 → 0.0.219

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/README.md CHANGED
@@ -522,6 +522,16 @@ app's UI checks for persisted progress and offline content. Neither operation
522
522
  uninstalls an app or clears its data. The controls deliberately refuse physical
523
523
  devices and implicit device selectors.
524
524
 
525
+ For guest-frame evidence on a headless iOS runner, use
526
+ `recordIosSimulatorVideo({ device: simulatorUuid, path: '/absolute/path/video.mp4' })`
527
+ from the same entrypoint. It resolves after Apple's first-frame acknowledgement.
528
+ Call `await recording.stop()` in `finally` before restarting the simulator or
529
+ installing an update; repeated stops share the same finalization. Startup and
530
+ stop are bounded (30 seconds by default, `timeoutMs` to change it), and an empty
531
+ or failed recording rejects. Existing files are preserved unless `overwrite: true`
532
+ is set. Capture retains a display-frame sink independently of the Device Hub viewer;
533
+ it does not change the application's animations or test assertions.
534
+
525
535
  ## Contributing
526
536
 
527
537
  We love contributions! Please see our [contributing guide](../../CONTRIBUTING.md).
package/dist/cli.js CHANGED
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3424
3424
  `);
3425
3425
  }
3426
3426
  // package.json
3427
- var version = "0.0.217";
3427
+ var version = "0.0.219";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
309
309
  `);
310
310
  }
311
311
  // package.json
312
- var version = "0.0.217";
312
+ var version = "0.0.219";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
57
57
  `);
58
58
  }
59
59
  // package.json
60
- var version = "0.0.217";
60
+ var version = "0.0.219";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -5709,7 +5709,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5709
5709
  }
5710
5710
 
5711
5711
  private func restoreFocus(_ focused: UIView?) {
5712
- guard let focused, focused.window != nil, !focused.isFirstResponder else { return }
5712
+ guard let focused, focused.window != nil, !focused.isFirstResponder,
5713
+ lastFocusedInput === focused, firstResponder(in: rootStack) == nil else { return }
5714
+ // A delayed acknowledgement belongs to the input that scheduled it.
5715
+ // A newer field or an explicit dismissal owns focus now.
5713
5716
  _ = focused.becomeFirstResponder()
5714
5717
  }
5715
5718
 
@@ -17,6 +17,22 @@ export interface MobileTestDevice {
17
17
  }>;
18
18
  installedBuild: () => Promise<string>;
19
19
  }
20
+ export interface IosSimulatorRecording {
21
+ path: string;
22
+ stop: () => Promise<void>;
23
+ }
24
+ /**
25
+ * Retains guest display frames, including on a headless runner. Waits for
26
+ * simctl's documented first-frame acknowledgement; stop finalizes the movie
27
+ * before a test shuts down the device or replaces its app.
28
+ */
29
+ export declare function recordIosSimulatorVideo(options: {
30
+ device: string;
31
+ path: string;
32
+ overwrite?: boolean;
33
+ xcrun?: string;
34
+ timeoutMs?: number;
35
+ }): Promise<IosSimulatorRecording>;
20
36
  /**
21
37
  * Drives an explicitly selected simulator/emulator. Restart preserves its disk;
22
38
  * update installs over the existing app, without uninstalling or clearing data.
@@ -1,12 +1,105 @@
1
1
  // @bun
2
2
  // src/mobile-testing.ts
3
3
  import { access } from "fs/promises";
4
- import { join } from "path";
4
+ import { isAbsolute, join } from "path";
5
+ function requireIosSimulator(device) {
6
+ if (!/^[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}$/i.test(device))
7
+ throw new Error("An explicit iOS simulator UUID is required");
8
+ }
9
+ async function recordIosSimulatorVideo(options) {
10
+ requireIosSimulator(options.device);
11
+ if (!isAbsolute(options.path))
12
+ throw new Error("A local absolute recording path is required");
13
+ const timeout = options.timeoutMs ?? 30000;
14
+ if (!Number.isSafeInteger(timeout) || timeout < 1)
15
+ throw new Error("Recording timeout must be a positive integer");
16
+ const child = Bun.spawn([
17
+ options.xcrun || "xcrun",
18
+ "simctl",
19
+ "io",
20
+ options.device,
21
+ "recordVideo",
22
+ "--codec=h264",
23
+ ...options.overwrite ? ["--force"] : [],
24
+ options.path
25
+ ], { stdout: "ignore", stderr: "pipe" });
26
+ let diagnostics = "";
27
+ const firstFrame = Promise.withResolvers();
28
+ const drained = (async () => {
29
+ const reader = child.stderr.getReader();
30
+ const decoder = new TextDecoder;
31
+ try {
32
+ while (true) {
33
+ const { done, value } = await reader.read();
34
+ if (done)
35
+ break;
36
+ diagnostics += decoder.decode(value, { stream: true });
37
+ if (/(?:^|\n)Recording started\r?\n/.test(diagnostics))
38
+ firstFrame.resolve();
39
+ diagnostics = diagnostics.slice(-4096);
40
+ }
41
+ } finally {
42
+ reader.releaseLock();
43
+ }
44
+ })();
45
+ drained.catch(() => {});
46
+ const startupTimeout = Promise.withResolvers();
47
+ const deadline = setTimeout(() => startupTimeout.reject(new Error("Recorder did not produce its first frame in time")), timeout);
48
+ try {
49
+ await Promise.race([
50
+ firstFrame.promise,
51
+ child.exited.then(async (code) => {
52
+ await drained;
53
+ throw new Error(`Recorder exited before its first frame (${code})`);
54
+ }),
55
+ startupTimeout.promise
56
+ ]);
57
+ } catch (cause) {
58
+ child.kill("SIGKILL");
59
+ await child.exited;
60
+ await drained.catch(() => {});
61
+ throw new Error(`${cause instanceof Error ? cause.message : cause}
62
+ ${diagnostics}`);
63
+ } finally {
64
+ clearTimeout(deadline);
65
+ }
66
+ let stopping;
67
+ return {
68
+ path: options.path,
69
+ stop: () => stopping ||= (async () => {
70
+ if (child.exitCode !== null)
71
+ throw new Error(`Recorder ended before stop
72
+ ${diagnostics}`);
73
+ child.kill("SIGINT");
74
+ const finalizationTimeout = Promise.withResolvers();
75
+ const stopDeadline = setTimeout(() => {
76
+ child.kill("SIGKILL");
77
+ finalizationTimeout.reject(new Error("Recorder did not finalize in time"));
78
+ }, timeout);
79
+ try {
80
+ const code = await Promise.race([
81
+ child.exited,
82
+ finalizationTimeout.promise
83
+ ]);
84
+ await drained;
85
+ if (code !== 0)
86
+ throw new Error(`Recorder failed (${code})
87
+ ${diagnostics}`);
88
+ const file = Bun.file(options.path);
89
+ if (!await file.exists() || file.size === 0)
90
+ throw new Error("Recorder did not write a movie");
91
+ } finally {
92
+ clearTimeout(stopDeadline);
93
+ await child.exited;
94
+ }
95
+ })()
96
+ };
97
+ }
5
98
  function createMobileTestDevice(options) {
6
99
  if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(options.appId))
7
100
  throw new Error("An explicit app identifier is required");
8
- if (options.platform === "ios" && !/^[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}$/i.test(options.device))
9
- throw new Error("An explicit iOS simulator UUID is required");
101
+ if (options.platform === "ios")
102
+ requireIosSimulator(options.device);
10
103
  if (options.platform === "android" && !/^emulator-\d+$/.test(options.device))
11
104
  throw new Error("An explicit Android emulator serial is required");
12
105
  async function run(args, timeout = 120000) {
@@ -74,5 +167,6 @@ ${errors || output}`);
74
167
  return { restart, installUpdate, installedBuild };
75
168
  }
76
169
  export {
170
+ recordIosSimulatorVideo,
77
171
  createMobileTestDevice
78
172
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.217",
3
+ "version": "0.0.219",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",