craft-native 0.0.211 → 0.0.212

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
@@ -499,6 +499,29 @@ cd packages/examples-ts
499
499
  bun run hello-world
500
500
  ```
501
501
 
502
+ ## Mobile restart and update tests
503
+
504
+ Use the Bun-only `craft-native/testing` entrypoint to interrupt an installed app
505
+ on an explicitly selected iOS Simulator or Android emulator:
506
+
507
+ ```ts
508
+ import { createMobileTestDevice } from 'craft-native/testing'
509
+
510
+ const device = createMobileTestDevice({
511
+ platform: 'android', device: 'emulator-5582', appId: 'org.example.app',
512
+ })
513
+ await device.restart()
514
+ const { previousBuild, build } = await device.installUpdate('/path/to/update.apk')
515
+ ```
516
+
517
+ For iOS use `platform: 'ios'`, the simulator UUID, and an `.app` directory.
518
+ `restart()` waits for a new boot; `installUpdate()` requires an existing app and
519
+ preserves its data. Android updates must use the original signing key. Supply a
520
+ higher build number and assert the returned installed builds, then run your
521
+ app's UI checks for persisted progress and offline content. Neither operation
522
+ uninstalls an app or clears its data. The controls deliberately refuse physical
523
+ devices and implicit device selectors.
524
+
502
525
  ## Contributing
503
526
 
504
527
  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.211";
3427
+ var version = "0.0.212";
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.211";
312
+ var version = "0.0.212";
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.211";
60
+ var version = "0.0.212";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -0,0 +1,25 @@
1
+ export type MobileTestDeviceOptions = {
2
+ platform: 'ios';
3
+ device: string;
4
+ appId: string;
5
+ xcrun?: string;
6
+ } | {
7
+ platform: 'android';
8
+ device: string;
9
+ appId: string;
10
+ adb?: string;
11
+ };
12
+ export interface MobileTestDevice {
13
+ restart: () => Promise<void>;
14
+ installUpdate: (artifact: string) => Promise<{
15
+ previousBuild: string;
16
+ build: string;
17
+ }>;
18
+ installedBuild: () => Promise<string>;
19
+ }
20
+ /**
21
+ * Drives an explicitly selected simulator/emulator. Restart preserves its disk;
22
+ * update installs over the existing app, without uninstalling or clearing data.
23
+ * Keep product assertions in the app's journey, after these OS boundaries.
24
+ */
25
+ export declare function createMobileTestDevice(options: MobileTestDeviceOptions): MobileTestDevice;
@@ -0,0 +1,78 @@
1
+ // @bun
2
+ // src/mobile-testing.ts
3
+ import { access } from "fs/promises";
4
+ import { join } from "path";
5
+ function createMobileTestDevice(options) {
6
+ if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(options.appId))
7
+ 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");
10
+ if (options.platform === "android" && !/^emulator-\d+$/.test(options.device))
11
+ throw new Error("An explicit Android emulator serial is required");
12
+ async function run(args, timeout = 120000) {
13
+ const child = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
14
+ const deadline = setTimeout(() => child.kill(), timeout);
15
+ try {
16
+ const [output, errors, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
17
+ if (code !== 0)
18
+ throw new Error(`Mobile test command failed: ${args[0]} ${args[1]} (${code})
19
+ ${errors || output}`);
20
+ return output.trim();
21
+ } finally {
22
+ clearTimeout(deadline);
23
+ }
24
+ }
25
+ const ios = (...args) => run([options.platform === "ios" ? options.xcrun || "xcrun" : "xcrun", "simctl", ...args]);
26
+ const android = (...args) => run([options.platform === "android" ? options.adb || "adb" : "adb", "-s", options.device, ...args]);
27
+ async function installedBuild() {
28
+ if (options.platform === "ios") {
29
+ const app = await ios("get_app_container", options.device, options.appId, "app");
30
+ return run(["/usr/libexec/PlistBuddy", "-c", "Print :CFBundleVersion", join(app, "Info.plist")]);
31
+ }
32
+ const result = await android("shell", "dumpsys", "package", options.appId);
33
+ const version = result.match(/\bversionCode=(\d+)/)?.[1];
34
+ if (!version)
35
+ throw new Error(`App is not installed: ${options.appId}`);
36
+ return version;
37
+ }
38
+ async function restart() {
39
+ if (options.platform === "ios") {
40
+ await ios("shutdown", options.device);
41
+ await ios("boot", options.device);
42
+ await ios("bootstatus", options.device, "-b");
43
+ return;
44
+ }
45
+ const previousBoot = await android("shell", "cat", "/proc/sys/kernel/random/boot_id");
46
+ await android("reboot");
47
+ const deadline = Date.now() + 120000;
48
+ while (Date.now() < deadline) {
49
+ try {
50
+ await android("wait-for-device");
51
+ const boot = await android("shell", "cat", "/proc/sys/kernel/random/boot_id");
52
+ if (boot !== previousBoot && await android("shell", "getprop", "sys.boot_completed") === "1") {
53
+ await android("shell", "input", "keyevent", "82");
54
+ return;
55
+ }
56
+ } catch {}
57
+ await Bun.sleep(500);
58
+ }
59
+ throw new Error("Android emulator did not complete a new boot");
60
+ }
61
+ async function installUpdate(artifact) {
62
+ const previousBuild = await installedBuild();
63
+ if (options.platform === "ios") {
64
+ const id = await run(["/usr/libexec/PlistBuddy", "-c", "Print :CFBundleIdentifier", join(artifact, "Info.plist")]);
65
+ if (id !== options.appId)
66
+ throw new Error("Update bundle identifier differs from the installed app");
67
+ await ios("install", options.device, artifact);
68
+ } else {
69
+ await access(artifact);
70
+ await android("install", "-r", artifact);
71
+ }
72
+ return { previousBuild, build: await installedBuild() };
73
+ }
74
+ return { restart, installUpdate, installedBuild };
75
+ }
76
+ export {
77
+ createMobileTestDevice
78
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.211",
3
+ "version": "0.0.212",
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>",
@@ -39,6 +39,11 @@
39
39
  "default": "./dist/index.cjs"
40
40
  }
41
41
  },
42
+ "./testing": {
43
+ "browser": null,
44
+ "types": "./dist/mobile-testing.d.ts",
45
+ "import": "./dist/mobile-testing.js"
46
+ },
42
47
  "./mobile": {
43
48
  "types": "./dist/mobile.d.ts",
44
49
  "import": "./dist/mobile.js",
@@ -67,6 +72,9 @@
67
72
  },
68
73
  "typesVersions": {
69
74
  "*": {
75
+ "testing": [
76
+ "./dist/mobile-testing.d.ts"
77
+ ],
70
78
  "mobile": [
71
79
  "./dist/mobile.d.ts"
72
80
  ],