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 +10 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +5 -3
- package/dist/ios/templates/CraftHybrid.swift +10 -6
- 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
|
@@ -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 !=
|
|
802
|
-
|
|
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
|
|
1675
|
-
|
|
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
|
}
|
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
|
};
|