craft-native 0.0.217 → 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 +10 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/mobile-testing.d.ts +16 -0
- package/dist/mobile-testing.js +97 -3
- package/package.json +1 -1
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.
|
|
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
package/dist/index.js
CHANGED
package/dist/mobile-testing.d.ts
CHANGED
|
@@ -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.
|
package/dist/mobile-testing.js
CHANGED
|
@@ -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"
|
|
9
|
-
|
|
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
|
};
|