craft-native 0.0.211 → 0.0.213
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 +23 -0
- package/dist/android/templates/CraftNative.kt.template +17 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/testing/DriverTests.swift.template +13 -6
- package/dist/macos/templates/native_layout.zig +213 -1
- package/dist/mobile-testing.d.ts +25 -0
- package/dist/mobile-testing.js +78 -0
- package/package.json +9 -1
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).
|
|
@@ -353,6 +353,12 @@ object CraftNative {
|
|
|
353
353
|
nodeData: FloatArray,
|
|
354
354
|
treeMeta: FloatArray,
|
|
355
355
|
): FloatArray?
|
|
356
|
+
private external fun nativeLayoutTreeV4(
|
|
357
|
+
width: Float,
|
|
358
|
+
height: Float,
|
|
359
|
+
nodeData: FloatArray,
|
|
360
|
+
treeMeta: FloatArray,
|
|
361
|
+
): FloatArray?
|
|
356
362
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
357
363
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
358
364
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
@@ -635,6 +641,17 @@ object CraftNative {
|
|
|
635
641
|
}
|
|
636
642
|
}
|
|
637
643
|
|
|
644
|
+
/** Resolve a reverse-aware tree with shared grid-auto-rows support. */
|
|
645
|
+
fun layoutTreeV4(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
|
|
646
|
+
if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
|
|
647
|
+
return try {
|
|
648
|
+
nativeLayoutTreeV4(width, height, nodeData, treeMeta)
|
|
649
|
+
?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
|
|
650
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
651
|
+
null
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
638
655
|
/**
|
|
639
656
|
* The clipboard's text, or null to use the Kotlin implementation.
|
|
640
657
|
*
|
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.213";
|
|
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
|
@@ -87,6 +87,13 @@ final class DriverTests: XCTestCase {
|
|
|
87
87
|
XCTAssertTrue(ok, what)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/// testIDs are exact identifiers. Resolving a unique element with the
|
|
91
|
+
/// subscript scans the whole accessibility tree for ambiguous matches;
|
|
92
|
+
/// firstMatch can stop at the identified node, including a scrolled row.
|
|
93
|
+
func identified(_ id: String) -> XCUIElement {
|
|
94
|
+
app.descendants(matching: .any).matching(NSPredicate(format: "identifier == %@", id)).firstMatch
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
func element(containing text: String, in target: XCUIApplication? = nil) -> XCUIElement? {
|
|
91
98
|
let root = target ?? app
|
|
92
99
|
let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR value CONTAINS[c] %@ OR identifier == %@", text, text, text)
|
|
@@ -197,7 +204,7 @@ final class DriverTests: XCTestCase {
|
|
|
197
204
|
switch command {
|
|
198
205
|
case "ifid":
|
|
199
206
|
let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
200
|
-
if rest.count == 2,
|
|
207
|
+
if rest.count == 2, identified(rest[0]).waitForExistence(timeout: 3) { note("ifid \(rest[0]): shown"); run(rest[1]) }
|
|
201
208
|
else { note("ifid \(rest.first ?? ""): not shown") }
|
|
202
209
|
case "launch": app.launch()
|
|
203
210
|
case "activate": app.activate()
|
|
@@ -261,7 +268,7 @@ final class DriverTests: XCTestCase {
|
|
|
261
268
|
if let target = poll(10, { exact(arg) }) { press(target); note("tapexact \(arg): ok") }
|
|
262
269
|
else { XCTFail("Element not found"); note("FAIL tapexact \(arg): NOT FOUND") }
|
|
263
270
|
case "tapid":
|
|
264
|
-
let target =
|
|
271
|
+
let target = identified(arg)
|
|
265
272
|
if target.waitForExistence(timeout: 10) { press(target); note("tapid \(arg): ok") }
|
|
266
273
|
else { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
|
|
267
274
|
case "tapxy":
|
|
@@ -281,7 +288,7 @@ final class DriverTests: XCTestCase {
|
|
|
281
288
|
case "return": app.typeText(XCUIKeyboardKey.return.rawValue); note("return")
|
|
282
289
|
case "typeid":
|
|
283
290
|
let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
284
|
-
let target =
|
|
291
|
+
let target = identified(idParts[0])
|
|
285
292
|
if target.waitForExistence(timeout: 10) {
|
|
286
293
|
target.tap()
|
|
287
294
|
Thread.sleep(forTimeInterval: 0.8)
|
|
@@ -306,15 +313,15 @@ final class DriverTests: XCTestCase {
|
|
|
306
313
|
check(poll(seconds, { exact(text) }) != nil, "expectexact \(text)")
|
|
307
314
|
case "expectid":
|
|
308
315
|
let (id, seconds) = split(arg)
|
|
309
|
-
check(
|
|
316
|
+
check(identified(id).waitForExistence(timeout: seconds), "expectid \(id)")
|
|
310
317
|
case "labelid":
|
|
311
318
|
let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
312
|
-
let target =
|
|
319
|
+
let target = identified(idParts[0])
|
|
313
320
|
let want = idParts.count > 1 ? idParts[1] : ""
|
|
314
321
|
let seen = target.waitForExistence(timeout: 5) ? target.label : "(missing)"
|
|
315
322
|
check(seen == want, "labelid \(idParts[0]) \(want) (saw \(seen))")
|
|
316
323
|
case "absentid":
|
|
317
|
-
check(!
|
|
324
|
+
check(!identified(arg).exists, "absentid \(arg)")
|
|
318
325
|
case "absent":
|
|
319
326
|
check(element(containing: arg) == nil, "absent \(arg)")
|
|
320
327
|
case "tabbar":
|
|
@@ -98,6 +98,10 @@ pub const Style = struct {
|
|
|
98
98
|
grid_columns: u32 = 1,
|
|
99
99
|
row_gap: f32 = 0,
|
|
100
100
|
column_gap: f32 = 0,
|
|
101
|
+
/// Minimum height for generated grid rows. This is optional in the tree
|
|
102
|
+
/// ABI and remains host-neutral because hosts resolve percentages before
|
|
103
|
+
/// serializing the style.
|
|
104
|
+
grid_auto_rows: ?f32 = null,
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
pub const Node = struct {
|
|
@@ -243,6 +247,53 @@ pub const LayoutNodeInputV2 = extern struct {
|
|
|
243
247
|
child_count: u32,
|
|
244
248
|
};
|
|
245
249
|
|
|
250
|
+
/// Versioned tree input that carries the resolved `grid-auto-rows` minimum.
|
|
251
|
+
/// Appending the field keeps the v1/v2/v3 layouts ABI-compatible for older
|
|
252
|
+
/// hosts while allowing a native tree to use the same auto-row rule on every
|
|
253
|
+
/// platform.
|
|
254
|
+
pub const LayoutStyleInputV4 = extern struct {
|
|
255
|
+
direction: u32,
|
|
256
|
+
width: f32,
|
|
257
|
+
height: f32,
|
|
258
|
+
intrinsic_width: f32,
|
|
259
|
+
intrinsic_height: f32,
|
|
260
|
+
baseline: f32,
|
|
261
|
+
aspect_ratio: f32,
|
|
262
|
+
flex_basis: f32,
|
|
263
|
+
flex_grow: f32,
|
|
264
|
+
flex_shrink: f32,
|
|
265
|
+
min_width: f32,
|
|
266
|
+
max_width: f32,
|
|
267
|
+
min_height: f32,
|
|
268
|
+
max_height: f32,
|
|
269
|
+
gap: f32,
|
|
270
|
+
justify_content: u32,
|
|
271
|
+
align_items: u32,
|
|
272
|
+
align_self: u32,
|
|
273
|
+
flex_wrap: u32,
|
|
274
|
+
align_content: u32,
|
|
275
|
+
margin_top: f32,
|
|
276
|
+
margin_right: f32,
|
|
277
|
+
margin_bottom: f32,
|
|
278
|
+
margin_left: f32,
|
|
279
|
+
padding_top: f32,
|
|
280
|
+
padding_right: f32,
|
|
281
|
+
padding_bottom: f32,
|
|
282
|
+
padding_left: f32,
|
|
283
|
+
grid: u32,
|
|
284
|
+
grid_columns: u32,
|
|
285
|
+
row_gap: f32,
|
|
286
|
+
column_gap: f32,
|
|
287
|
+
reverse: u32,
|
|
288
|
+
grid_auto_rows: f32,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
pub const LayoutNodeInputV4 = extern struct {
|
|
292
|
+
style: LayoutStyleInputV4,
|
|
293
|
+
first_child: u32,
|
|
294
|
+
child_count: u32,
|
|
295
|
+
};
|
|
296
|
+
|
|
246
297
|
const max_layout_nodes: usize = 65_536;
|
|
247
298
|
|
|
248
299
|
/// Resolve an entire native view tree using the same layout rules used by the
|
|
@@ -361,6 +412,43 @@ pub export fn craft_native_layout_tree_v3(
|
|
|
361
412
|
return if (result == node_count) 0 else -1;
|
|
362
413
|
}
|
|
363
414
|
|
|
415
|
+
/// v4 tree layout entry point. It preserves v3's reverse/wrap-reverse bits
|
|
416
|
+
/// and adds one resolved `grid-auto-rows` minimum per node.
|
|
417
|
+
pub export fn craft_native_layout_tree_v4(
|
|
418
|
+
viewport_width: f32,
|
|
419
|
+
viewport_height: f32,
|
|
420
|
+
nodes: [*]const LayoutNodeInputV4,
|
|
421
|
+
node_count: usize,
|
|
422
|
+
frames: [*]Frame,
|
|
423
|
+
frame_count: usize,
|
|
424
|
+
) callconv(.c) c_int {
|
|
425
|
+
if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
|
|
426
|
+
|
|
427
|
+
const input = nodes[0..node_count];
|
|
428
|
+
const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
|
|
429
|
+
defer std.heap.page_allocator.free(visited);
|
|
430
|
+
@memset(visited, false);
|
|
431
|
+
if (!validateLayoutTreeV4(input, 0, visited)) return -1;
|
|
432
|
+
|
|
433
|
+
var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
|
|
434
|
+
defer std.heap.page_allocator.free(tree);
|
|
435
|
+
for (input, 0..) |item, index| {
|
|
436
|
+
const first = @as(usize, @intCast(item.first_child));
|
|
437
|
+
const count = @as(usize, @intCast(item.child_count));
|
|
438
|
+
tree[index] = .{
|
|
439
|
+
.style = layoutStyleV4(item.style),
|
|
440
|
+
.children = tree[first .. first + count],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const result = layout(
|
|
445
|
+
tree[0],
|
|
446
|
+
.{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
|
|
447
|
+
frames[0..frame_count],
|
|
448
|
+
) catch return -1;
|
|
449
|
+
return if (result == node_count) 0 else -1;
|
|
450
|
+
}
|
|
451
|
+
|
|
364
452
|
fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []bool) bool {
|
|
365
453
|
if (index >= input.len or visited[index]) return false;
|
|
366
454
|
visited[index] = true;
|
|
@@ -387,6 +475,19 @@ fn validateLayoutTreeV2(input: []const LayoutNodeInputV2, index: usize, visited:
|
|
|
387
475
|
return true;
|
|
388
476
|
}
|
|
389
477
|
|
|
478
|
+
fn validateLayoutTreeV4(input: []const LayoutNodeInputV4, index: usize, visited: []bool) bool {
|
|
479
|
+
if (index >= input.len or visited[index]) return false;
|
|
480
|
+
visited[index] = true;
|
|
481
|
+
const item = input[index];
|
|
482
|
+
const first = @as(usize, @intCast(item.first_child));
|
|
483
|
+
const count = @as(usize, @intCast(item.child_count));
|
|
484
|
+
if (first > input.len or count > input.len - first or (count > 0 and first <= index)) return false;
|
|
485
|
+
for (first..first + count) |child| {
|
|
486
|
+
if (!validateLayoutTreeV4(input, child, visited)) return false;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
390
491
|
fn layoutStyle(input: LayoutStyleInput) Style {
|
|
391
492
|
return .{
|
|
392
493
|
.direction = if (input.direction == 1) .column else .row,
|
|
@@ -470,6 +571,46 @@ fn layoutStyleV3(input: LayoutStyleInputV2) Style {
|
|
|
470
571
|
return style;
|
|
471
572
|
}
|
|
472
573
|
|
|
574
|
+
fn layoutStyleV4(input: LayoutStyleInputV4) Style {
|
|
575
|
+
return .{
|
|
576
|
+
.direction = if (input.direction == 1) .column else .row,
|
|
577
|
+
.reverse = (input.reverse & 1) != 0,
|
|
578
|
+
.wrap_reverse = (input.reverse & 2) != 0,
|
|
579
|
+
.width = optionalFinite(input.width),
|
|
580
|
+
.height = optionalFinite(input.height),
|
|
581
|
+
.intrinsic_width = optionalFinite(input.intrinsic_width),
|
|
582
|
+
.intrinsic_height = optionalFinite(input.intrinsic_height),
|
|
583
|
+
.baseline = optionalFinite(input.baseline),
|
|
584
|
+
.aspect_ratio = optionalFinite(input.aspect_ratio),
|
|
585
|
+
.flex_basis = optionalFinite(input.flex_basis),
|
|
586
|
+
.flex_grow = finiteNonNegative(input.flex_grow),
|
|
587
|
+
.flex_shrink = finiteNonNegative(input.flex_shrink),
|
|
588
|
+
.min_width = optionalFinite(input.min_width),
|
|
589
|
+
.max_width = optionalFinite(input.max_width),
|
|
590
|
+
.min_height = optionalFinite(input.min_height),
|
|
591
|
+
.max_height = optionalFinite(input.max_height),
|
|
592
|
+
.gap = finiteNonNegative(input.gap),
|
|
593
|
+
.justify_content = @fromBackingInt(@intCast(@min(input.justify_content, 5))),
|
|
594
|
+
.align_items = @fromBackingInt(@intCast(@min(input.align_items, 4))),
|
|
595
|
+
.align_self = if (input.align_self == 0) null else @fromBackingInt(@intCast(@min(input.align_self - 1, 4))),
|
|
596
|
+
.flex_wrap = input.flex_wrap != 0,
|
|
597
|
+
.align_content = @fromBackingInt(@intCast(@min(input.align_content, 6))),
|
|
598
|
+
.margin_top = finiteNonNegative(input.margin_top),
|
|
599
|
+
.margin_right = finiteNonNegative(input.margin_right),
|
|
600
|
+
.margin_bottom = finiteNonNegative(input.margin_bottom),
|
|
601
|
+
.margin_left = finiteNonNegative(input.margin_left),
|
|
602
|
+
.padding_top = finiteNonNegative(input.padding_top),
|
|
603
|
+
.padding_right = finiteNonNegative(input.padding_right),
|
|
604
|
+
.padding_bottom = finiteNonNegative(input.padding_bottom),
|
|
605
|
+
.padding_left = finiteNonNegative(input.padding_left),
|
|
606
|
+
.grid = input.grid != 0,
|
|
607
|
+
.grid_columns = @max(@as(u32, 1), input.grid_columns),
|
|
608
|
+
.row_gap = finiteNonNegative(input.row_gap),
|
|
609
|
+
.column_gap = finiteNonNegative(input.column_gap),
|
|
610
|
+
.grid_auto_rows = optionalFinite(input.grid_auto_rows),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
473
614
|
fn optionalFinite(value: f32) ?f32 {
|
|
474
615
|
return if (std.math.isFinite(value)) value else null;
|
|
475
616
|
}
|
|
@@ -1267,7 +1408,7 @@ fn gridRowHeight(node: Node, row: usize, column_count: usize) f32 {
|
|
|
1267
1408
|
const child_height = child.style.height orelse ratio_height orelse child.style.intrinsic_height orelse child.style.flex_basis orelse 0;
|
|
1268
1409
|
height = @max(height, clamp(child_height, child.style.min_height, child.style.max_height) + verticalMargins(child));
|
|
1269
1410
|
}
|
|
1270
|
-
return height;
|
|
1411
|
+
return @max(height, finiteNonNegative(node.style.grid_auto_rows orelse 0));
|
|
1271
1412
|
}
|
|
1272
1413
|
|
|
1273
1414
|
fn childMainBasis(node: Node, parent_direction: Axis) f32 {
|
|
@@ -1524,6 +1665,46 @@ fn emptyLayoutStyleInputV2() LayoutStyleInputV2 {
|
|
|
1524
1665
|
};
|
|
1525
1666
|
}
|
|
1526
1667
|
|
|
1668
|
+
fn emptyLayoutStyleInputV4() LayoutStyleInputV4 {
|
|
1669
|
+
const unset = std.math.nan(f32);
|
|
1670
|
+
return .{
|
|
1671
|
+
.direction = 0,
|
|
1672
|
+
.width = unset,
|
|
1673
|
+
.height = unset,
|
|
1674
|
+
.intrinsic_width = unset,
|
|
1675
|
+
.intrinsic_height = unset,
|
|
1676
|
+
.baseline = unset,
|
|
1677
|
+
.aspect_ratio = unset,
|
|
1678
|
+
.flex_basis = unset,
|
|
1679
|
+
.flex_grow = 0,
|
|
1680
|
+
.flex_shrink = 0,
|
|
1681
|
+
.min_width = unset,
|
|
1682
|
+
.max_width = unset,
|
|
1683
|
+
.min_height = unset,
|
|
1684
|
+
.max_height = unset,
|
|
1685
|
+
.gap = 0,
|
|
1686
|
+
.justify_content = 0,
|
|
1687
|
+
.align_items = 3,
|
|
1688
|
+
.align_self = 0,
|
|
1689
|
+
.flex_wrap = 0,
|
|
1690
|
+
.align_content = 6,
|
|
1691
|
+
.margin_top = 0,
|
|
1692
|
+
.margin_right = 0,
|
|
1693
|
+
.margin_bottom = 0,
|
|
1694
|
+
.margin_left = 0,
|
|
1695
|
+
.padding_top = 0,
|
|
1696
|
+
.padding_right = 0,
|
|
1697
|
+
.padding_bottom = 0,
|
|
1698
|
+
.padding_left = 0,
|
|
1699
|
+
.grid = 0,
|
|
1700
|
+
.grid_columns = 1,
|
|
1701
|
+
.row_gap = 0,
|
|
1702
|
+
.column_gap = 0,
|
|
1703
|
+
.reverse = 0,
|
|
1704
|
+
.grid_auto_rows = unset,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1527
1708
|
fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
|
|
1528
1709
|
var result = finiteNonNegative(value);
|
|
1529
1710
|
if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
|
|
@@ -1566,6 +1747,37 @@ test "exports the host-neutral tree layout contract" {
|
|
|
1566
1747
|
try std.testing.expectEqual(@as(f32, 10), frames[1].height);
|
|
1567
1748
|
}
|
|
1568
1749
|
|
|
1750
|
+
test "v4 tree layout applies a resolved grid auto-row minimum" {
|
|
1751
|
+
var root_style = emptyLayoutStyleInputV4();
|
|
1752
|
+
root_style.width = 100;
|
|
1753
|
+
root_style.height = 100;
|
|
1754
|
+
root_style.grid = 1;
|
|
1755
|
+
root_style.grid_columns = 2;
|
|
1756
|
+
root_style.grid_auto_rows = 20;
|
|
1757
|
+
|
|
1758
|
+
var first_style = emptyLayoutStyleInputV4();
|
|
1759
|
+
first_style.intrinsic_width = 10;
|
|
1760
|
+
first_style.intrinsic_height = 5;
|
|
1761
|
+
var second_style = emptyLayoutStyleInputV4();
|
|
1762
|
+
second_style.intrinsic_width = 10;
|
|
1763
|
+
second_style.intrinsic_height = 7;
|
|
1764
|
+
var third_style = emptyLayoutStyleInputV4();
|
|
1765
|
+
third_style.intrinsic_width = 10;
|
|
1766
|
+
third_style.intrinsic_height = 4;
|
|
1767
|
+
|
|
1768
|
+
const nodes = [_]LayoutNodeInputV4{
|
|
1769
|
+
.{ .style = root_style, .first_child = 1, .child_count = 3 },
|
|
1770
|
+
.{ .style = first_style, .first_child = 0, .child_count = 0 },
|
|
1771
|
+
.{ .style = second_style, .first_child = 0, .child_count = 0 },
|
|
1772
|
+
.{ .style = third_style, .first_child = 0, .child_count = 0 },
|
|
1773
|
+
};
|
|
1774
|
+
var frames: [4]Frame = undefined;
|
|
1775
|
+
try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v4(100, 100, &nodes, nodes.len, &frames, frames.len));
|
|
1776
|
+
try std.testing.expectEqual(@as(f32, 0), frames[1].y);
|
|
1777
|
+
try std.testing.expectEqual(@as(f32, 0), frames[2].y);
|
|
1778
|
+
try std.testing.expectEqual(@as(f32, 20), frames[3].y);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1569
1781
|
test "lays out nested flat trees with absolute descendant frames" {
|
|
1570
1782
|
var root_style = emptyLayoutStyleInput();
|
|
1571
1783
|
root_style.width = 200;
|
|
@@ -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.
|
|
3
|
+
"version": "0.0.213",
|
|
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
|
],
|