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
10 changes: 10 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let package = Package(
products: [
.library(name: "SkipFuseUI", type: .dynamic, targets: ["SkipFuseUI"] + (android ? ["SwiftUI"] : [])),
.library(name: "SkipSwiftUI", type: .dynamic, targets: ["SkipSwiftUI"]),
.library(name: "SkipSwiftUITestSupport", type: .dynamic, targets: ["SkipSwiftUITestSupport"]),
],
dependencies: [
.package(url: "https://source.skip.tools/skip.git", from: "1.7.4"),
Expand All @@ -27,8 +28,17 @@ let package = Package(
.product(name: "SwiftJNI", package: "swift-jni"),
.product(name: "SkipUI", package: "skip-ui")
], plugins: [.plugin(name: "skipstone", package: "skip")]),
.target(
name: "SkipSwiftUITestSupport",
dependencies: [
.product(name: "SkipBridge", package: "skip-bridge"),
],
path: "Tests/SkipSwiftUITestSupport",
plugins: [.plugin(name: "skipstone", package: "skip")]
),
.testTarget(name: "SkipSwiftUITests", dependencies: [
"SkipSwiftUI",
"SkipSwiftUITestSupport",
.product(name: "SkipTest", package: "skip")
], plugins: [.plugin(name: "skipstone", package: "skip")]),
]
Expand Down
78 changes: 78 additions & 0 deletions Tests/SkipSwiftUITestSupport/ObservationProbeMount.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2025–2026 Skip
// SPDX-License-Identifier: MPL-2.0
import Observation
import SkipBridge

public final class ObservationProbeMount {
private let box: any ObservationProbeMountBox

public init() {
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
self.box = ObservableObservationProbeMountBox()
} else {
self.box = UnsupportedObservationProbeMountBox()
}
}

public func setCount(_ count: Int) {
self.box.setCount(count)
}

public func renderedText() -> String {
"count: \(self.box.count)"
}

public func renderedText(onChange: @escaping () -> Void) -> String {
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
let handler = ObservationChangeHandler(onChange)
return withObservationTracking {
self.renderedText()
} onChange: {
handler()
}
} else {
return self.renderedText()
}
}
}

private struct ObservationChangeHandler: @unchecked Sendable {
private let handler: () -> Void

init(_ handler: @escaping () -> Void) {
self.handler = handler
}

func callAsFunction() {
self.handler()
}
}

private protocol ObservationProbeMountBox: AnyObject {
var count: Int { get }
func setCount(_ count: Int)
}

private final class UnsupportedObservationProbeMountBox: ObservationProbeMountBox {
var count: Int { 0 }
func setCount(_ count: Int) {}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
private final class ObservableObservationProbeMountBox: ObservationProbeMountBox {
private let model = ObservationProbeModel()

var count: Int {
self.model.count
}

func setCount(_ count: Int) {
self.model.count = count
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Observable
private final class ObservationProbeModel {
var count = 0
}
6 changes: 6 additions & 0 deletions Tests/SkipSwiftUITestSupport/Skip/skip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Configuration file for Skip (https://skip.dev) project
skip:
mode: 'native'
bridging:
auto: true
options: 'kotlincompat'
17 changes: 17 additions & 0 deletions Tests/SkipSwiftUITests/Skip/skip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
build:
contents:
- block: 'dependencies'
contents:
- 'testImplementation(libs.androidx.compose.ui.test)'
- 'testImplementation(libs.androidx.compose.ui.test.junit4)'
- 'testImplementation(libs.androidx.compose.ui.test.manifest)'
- block: 'tasks.named("buildLocalSwiftPackage")'
contents:
- block: 'doLast'
contents:
- block: 'project.objects.newInstance<SkipBridgeExecOps>().execOps.exec'
contents:
- 'workingDir(layout.projectDirectory)'
- 'commandLine("sh", "-cx", "swift build --package-path \"${swiftSourceFolder()}\" --configuration debug --product SkipSwiftUITestSupport --scratch-path \"${layout.projectDirectory}/.build/SkipSwiftUITestSupport/swift\" -Xcc -fPIC -Xswiftc -DSKIP_BRIDGE -Xswiftc -DROBOLECTRIC")'
- 'environment("SKIP_BRIDGE", "1")'
- 'if (file(skipCommand).exists()) { environment("SKIP_COMMAND_OVERRIDE", skipCommand) }'
67 changes: 65 additions & 2 deletions Tests/SkipSwiftUITests/SkipSwiftUITests.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,75 @@
// Copyright 2025–2026 Skip
// SPDX-License-Identifier: MPL-2.0
import XCTest
#if !SKIP
@testable import SkipSwiftUI

#if SKIP
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import org.junit.Rule
import SkipBridge
#endif

@testable import SkipSwiftUI
import SkipSwiftUITestSupport

final class SkipSwiftUITests: XCTestCase {
func testSkipUI() throws {
XCTAssertEqual(3, 1 + 2)
}
}

final class ObservationMountRuntimeTests: SkipUITestCase {
// SKIP INSERT: @get:Rule val composeRule = createComposeRule()

override func setUp() {
super.setUp()
#if SKIP
loadPeerLibrary(packageName: "skip-fuse-ui", moduleName: "SkipSwiftUITestSupport")
#endif
}

func testMountedViewInvalidatesWhenObservablePropertyChanges() throws {
#if SKIP
let probe = ObservationProbeMount()

composeRule.setContent {
Text(probe.renderedText()).Compose()
}
composeRule.waitForIdle()
composeRule.onNodeWithText("count: 0").assertIsDisplayed()

probe.setCount(1)
composeRule.waitForIdle()

composeRule.onNodeWithText("count: 1").assertIsDisplayed()
#else
throw XCTSkip("Runs only in the transpiled Skip Compose test runtime.")
#endif
}

func testMountedViewInvalidatesWithObservationTracking() throws {
#if SKIP
let probe = ObservationProbeMount()

composeRule.setContent {
let invalidation = remember { mutableStateOf(0) }
_ = invalidation.value
Text(probe.renderedText {
invalidation.value += 1
}).Compose()
}
composeRule.waitForIdle()
composeRule.onNodeWithText("count: 0").assertIsDisplayed()

probe.setCount(1)
composeRule.waitForIdle()

composeRule.onNodeWithText("count: 1").assertIsDisplayed()
#else
throw XCTSkip("Runs only in the transpiled Skip Compose test runtime.")
#endif
}
}
7 changes: 5 additions & 2 deletions Tests/SkipSwiftUITests/XCSkipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ open class SkipUITestCase : XCTestCase {
public var testName: String? {
#if SKIP
let tname = _testName.methodName // "testLocalizedText$SkipUI_debugUnitTest"
return tname.split(separator: "$").first
return tname.split(separator: Character("$")).first
#else
let tname = testRun?.test.name // "-[SkipUITests testLocalizedText]"
return tname?
Expand Down Expand Up @@ -66,7 +66,10 @@ open class SkipUITestCase : XCTestCase {

public class TestIntrospectionTests : SkipUITestCase {
func testTestIntrospection() {
XCTAssertEqual("testTestIntrospection", self.testName)
XCTAssertTrue(
["testTestIntrospection", "TestIntrospectionTests.testTestIntrospection"]
.contains(self.testName)
)
}
}

Expand Down
Loading