craft-native 0.0.216 → 0.0.218

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.216";
3427
+ var version = "0.0.218";
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.216";
312
+ var version = "0.0.218";
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.216";
60
+ var version = "0.0.218";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -797,9 +797,11 @@ final class CraftChrome: ObservableObject {
797
797
 
798
798
  /// Move the tab indicator with an in-flight native-stack swipe. The
799
799
  /// controller commits or restores this preview when the finger lifts.
800
- func beginTabSwipe(to id: String) {
801
- guard id != selected, tabs.contains(where: { $0.id == id }) else { return }
802
- tabSwipePrevious = selected
800
+ func beginTabSwipe(to id: String, from previous: String) {
801
+ guard id != previous, tabs.contains(where: { $0.id == id }) else { return }
802
+ // A page's pending selection report may disagree with the native
803
+ // stack. Cancellation belongs to the stack the finger left.
804
+ tabSwipePrevious = previous
803
805
  withAnimation(nil) { selected = id }
804
806
  }
805
807
 
@@ -1203,7 +1203,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1203
1203
  tabSwipeFrom = -direction * width
1204
1204
  target.view.frame = view.bounds.offsetBy(dx: tabSwipeFrom, dy: 0)
1205
1205
  view.bringSubviewToFront(target.view)
1206
- CraftChrome.shared.beginTabSwipe(to: id)
1206
+ CraftChrome.shared.beginTabSwipe(to: id, from: currentTab)
1207
1207
  return true
1208
1208
  }
1209
1209
 
@@ -1668,12 +1668,16 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1668
1668
  /// tab's root: over a native screen, or on an entry the shell pushed
1669
1669
  /// there, the page cannot know which tab it is in (a workout page
1670
1670
  /// loaded on its own lights Calendar, though Today opened it). The bar
1671
- /// then keeps the tab that is on screen.
1671
+ /// then keeps the tab that is on screen, or the finger's active preview.
1672
1672
  func pageSelectedTab(_ id: String) {
1673
- guard id != currentTab, pendingTab?.id != id else { return }
1674
- guard let top = visibleStack?.topViewController as? CraftHybridWebSlot, top.isTabRoot else {
1675
- let shown = currentTab
1676
- DispatchQueue.main.async {
1673
+ guard id != (activeTabSwipeTarget ?? currentTab), pendingTab?.id != id else { return }
1674
+ guard activeTabSwipeTarget == nil,
1675
+ let top = visibleStack?.topViewController as? CraftHybridWebSlot, top.isTabRoot else {
1676
+ DispatchQueue.main.async { [weak self] in
1677
+ guard let self, self.hybrid.controller === self else { return }
1678
+ // Resolve ownership when the correction runs: a newer tap
1679
+ // or swipe may have arrived while a native root was mounting.
1680
+ let shown = self.activeTabSwipeTarget ?? self.currentTab
1677
1681
  guard CraftChrome.shared.selected != shown else { return }
1678
1682
  CraftChrome.shared.selected = shown
1679
1683
  }
@@ -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.216",
3
+ "version": "0.0.218",
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>",