Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/native-ui-engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ jobs:
POPOVER_CLOSE=NativeUIPluginTests/NativeUISnapshotTests/testNativePopoverClosureNotifiesWebBeforeReopening
POPOVER_REPLACE=NativeUIPluginTests/NativeUISnapshotTests/testPopoverReopensAfterPresentationIdentityKeepsReplacedSource
COVERED_ORIENTATION=NativeUIPluginTests/NativeUIPluginLifecycleTests/testOrientationPreservesCoveredControlsUntilRestore
SOURCE_ORDER=NativeUIPluginTests/NativeUISnapshotTests/testViewCapturePreservesNativeSourceOrder
xcodebuild -scheme RdlaboCapacitorNativeUi-Package -destination "platform=iOS Simulator,id=$DEVICE" CODE_SIGNING_ALLOWED=NO -clonedSourcePackagesDirPath "$RUNNER_TEMP/native-ui-spm" -skip-testing:"$POPOVER_CLOSE" -skip-testing:"$POPOVER_REPLACE" -skip-testing:"$COVERED_ORIENTATION" test
cd ios/Tests
xcodebuild -project NativeUIHostedTests.xcodeproj -scheme NativeUIHostedTests -destination "platform=iOS Simulator,id=$DEVICE" CODE_SIGNING_ALLOWED=NO -clonedSourcePackagesDirPath "$RUNNER_TEMP/native-ui-spm" -only-testing:"$POPOVER_CLOSE" -only-testing:"$POPOVER_REPLACE" -only-testing:"$COVERED_ORIENTATION" test
xcodebuild -project NativeUIHostedTests.xcodeproj -scheme NativeUIHostedTests -destination "platform=iOS Simulator,id=$DEVICE" CODE_SIGNING_ALLOWED=NO -clonedSourcePackagesDirPath "$RUNNER_TEMP/native-ui-spm" -only-testing:"$POPOVER_CLOSE" -only-testing:"$POPOVER_REPLACE" -only-testing:"$COVERED_ORIENTATION" -only-testing:"$SOURCE_ORDER" test
android:
runs-on: ubuntu-latest
steps:
Expand Down
46 changes: 46 additions & 0 deletions ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@ import Capacitor
@testable import NativeUICore

final class NativeUISnapshotTests: XCTestCase {
@MainActor func testViewCapturePreservesNativeSourceOrder() async throws {
guard UIApplication.shared.delegate != nil else { throw XCTSkip("Requires hosted UIApplication") }
guard #available(iOS 26.0, *) else { throw XCTSkip("Requires native controls") }
let window = UIWindow(frame: UIScreen.main.bounds)
let owner = UIViewController(); window.rootViewController = owner; window.makeKeyAndVisible()
let web = WKWebView(frame: window.bounds); owner.view.addSubview(web)
let controller = NativeUIController(environment: NativeUIEnvironment(webView: { web }, viewController: { owner }, emit: { _, _ in }, hasListeners: { _ in false }, trigger: { _, _ in }))
defer { window.isHidden = true }
let nodes = [control(["id": "a", "items": [item(["id": "a-action", "label": "Shared", "buttonFill": "clear", "color": "rgb(255, 0, 0)"])]]),
control(["id": "b", "items": [item(["id": "b-action", "label": "Shared", "buttonFill": "clear", "color": "rgb(0, 0, 255)"])]]),
control(["id": "c", "x": 160.0])]
let applied: [String: Any] = await withCheckedContinuation { continuation in
controller.update(NativeUICall(options: ["revision": 1, "viewportWidth": Double(web.bounds.width), "controls": nodes],
resolve: { continuation.resume(returning: $0) }, reject: { continuation.resume(returning: ["error": $0]) }))
}
XCTAssertNil(applied["error"])
window.layoutIfNeeded()
func buttons(_ view: UIView) -> [UIButton] { (view as? UIButton).map { [$0] } ?? view.subviews.flatMap(buttons) }
let button = try XCTUnwrap(buttons(owner.view).first { $0.accessibilityIdentifier == "a-action" })
func capture() async -> [String: Any] {
await withCheckedContinuation { continuation in
controller.coveredSnapshot(NativeUICall(options: ["action": "prepare", "ids": ["a", "b"], "revision": 1, "captureMode": "view", "viewportWidth": Double(web.bounds.width)],
resolve: { continuation.resume(returning: $0) }, reject: { continuation.resume(returning: ["error": $0]) }))
}
}
func pixels(_ result: [String: Any]) throws -> Data {
let parts = try XCTUnwrap(result["parts"] as? [[String: Any]])
let path = try XCTUnwrap(parts.first?["url"] as? String)
let image = try XCTUnwrap(UIImage(data: Data(contentsOf: XCTUnwrap(URL(string: path))))?.cgImage)
return try XCTUnwrap(image.dataProvider?.data) as Data
}
func stablePixels() async throws -> Data {
let first = await capture()
let expected = try pixels(first)
for _ in 0..<4 {
let repeated = await capture()
XCTAssertEqual(try pixels(repeated), expected, "Repeated handoffs must keep the native drawing order")
}
return expected
}
let before = try await stablePixels()
button.superview?.bringSubviewToFront(button)
let after = try await stablePixels()
XCTAssertNotEqual(after, before, "Overlapping controls must use their current native drawing order")
}

@MainActor func testProminentButtonStyleDecodesAndResetsTintWhenReused() throws {
guard #available(iOS 26.0, *) else { throw XCTSkip("Requires glass buttons") }
let rendering = NativeUIControlRendering()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,9 @@ public final class NativeUIController: NSObject, UITabBarDelegate {
ids.isDisjoint(with: railIDs) || ids.isSuperset(of: railIDs) else {
return nil
}
var views = Array(self.controls.filter { ids.contains($0.key) }.values)
let selected = Array(self.controls.filter { ids.contains($0.key) }.values)
var views = selected
if let ordered = self.host?.subviews.filter({ selected.contains($0) }), ordered.count == selected.count { views = ordered }
if !railIDs.isEmpty, ids.isSuperset(of: railIDs), let rail = self.verticalBars { views.append(rail.view) }
return views
}
Expand Down
Loading