Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

split out test helper functionality so XCTest isn't a hard requirement #70

Merged
merged 1 commit into from
Aug 25, 2022
Merged
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
14 changes: 12 additions & 2 deletions PlayerUI.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,26 @@ and display it as a SwiftUI view comprised of registered assets.
}
end

s.subspec 'TestUtilities' do |utils|
s.subspec 'TestUtilitiesCore' do |utils|
utils.dependency 'PlayerUI/Core'
utils.dependency 'PlayerUI/SwiftUI'

utils.ios.deployment_target = '13.0'

utils.source_files = 'ios/packages/test-utils/Sources/**/*'
utils.source_files = 'ios/packages/test-utils-core/Sources/**/*'
utils.resource_bundles = {
'TestUtilities' => ['ios/packages/test-utils/Resources/**/*.js']
}
end

s.subspec 'TestUtilities' do |utils|
utils.dependency 'PlayerUI/Core'
utils.dependency 'PlayerUI/SwiftUI'
utils.dependency 'PlayerUI/TestUtilitiesCore'

utils.ios.deployment_target = '13.0'

utils.source_files = 'ios/packages/test-utils/Sources/**/*'

utils.weak_framework = 'XCTest'
utils.pod_target_xcconfig = {
Expand Down
8 changes: 8 additions & 0 deletions generated.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ def PlayerUI(
"ios/packages/test-utils/Sources/**/*.c",
"ios/packages/test-utils/Sources/**/*.cc",
"ios/packages/test-utils/Sources/**/*.cpp",
"ios/packages/test-utils-core/Sources/**/*.h",
"ios/packages/test-utils-core/Sources/**/*.hh",
"ios/packages/test-utils-core/Sources/**/*.m",
"ios/packages/test-utils-core/Sources/**/*.mm",
"ios/packages/test-utils-core/Sources/**/*.swift",
"ios/packages/test-utils-core/Sources/**/*.c",
"ios/packages/test-utils-core/Sources/**/*.cc",
"ios/packages/test-utils-core/Sources/**/*.cpp",
"ios/plugins/TransitionPlugin/Sources/**/*.h",
"ios/plugins/TransitionPlugin/Sources/**/*.hh",
"ios/plugins/TransitionPlugin/Sources/**/*.m",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ class TestAssetType: PlayerAsset, Decodable {
var rawValue: JSValue?
var id: String
var type: String
var value: String?
struct Data: Decodable {
var id: String
var type: String
var value: String?
}
required init(from decoder: Decoder) throws {
let data = try decoder.singleValueContainer().decode(Data.self)
id = data.id
type = data.type
value = data.value
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ActionAssetTests: SwiftUIAssetUnitTestCase {
func setup() {
XCUIApplication().terminate()
}
func testAssetDecoding() throws {
func testAssetDecoding() async throws {
let json = """
{
"id": "action",
Expand All @@ -40,7 +40,7 @@ class ActionAssetTests: SwiftUIAssetUnitTestCase {
}
"""

guard let action: ActionAsset = getAsset(json) else {
guard let action: ActionAsset = await getAsset(json) else {
return XCTFail("unable to get asset")
}

Expand All @@ -57,16 +57,16 @@ class ActionAssetTests: SwiftUIAssetUnitTestCase {
_ = try view.inspect().button()
}

func testViewWithLabel() throws {
guard let text: TextAsset = getAsset("""
func testViewWithLabel() async throws {
guard let text: TextAsset = await getAsset("""
{"id": "text", "type": "text", "value":"hello world"}
""")
else { return XCTFail("unable to get asset") }
let data = ActionData(id: "id", type: "action", label: WrappedAsset(forAsset: text), run: nil)

let model = AssetViewModel<ActionData>(data)

let view = ActionAssetView(model: model)
let view = await ActionAssetView(model: model)

_ = try view.inspect().button()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class CollectionAssetTests: SwiftUIAssetUnitTestCase {
registry.register("text", asset: TextAsset.self)
}

func testDecoding() throws {
func testDecoding() async throws {
let json = """
{
"id": "collection",
Expand All @@ -45,15 +45,15 @@ class CollectionAssetTests: SwiftUIAssetUnitTestCase {
}
"""

guard let collection: CollectionAsset = getAsset(json) else { return XCTFail("could not get asset") }
guard let collection: CollectionAsset = await getAsset(json) else { return XCTFail("could not get asset") }

_ = try collection.view.inspect().find(CollectionAssetView.self).vStack()
}

func testView() throws {
func testView() async throws {
guard
let text1: TextAsset = getAsset("{\"id\": \"text\", \"type\": \"text\", \"value\":\"hello world\"}"),
let text2: TextAsset = getAsset("{\"id\": \"text2\", \"type\": \"text\", \"value\":\"goodbye world\"}")
let text1: TextAsset = await getAsset("{\"id\": \"text\", \"type\": \"text\", \"value\":\"hello world\"}"),
let text2: TextAsset = await getAsset("{\"id\": \"text2\", \"type\": \"text\", \"value\":\"goodbye world\"}")
else { return XCTFail("could not get assets") }
let model = AssetViewModel<CollectionData>(
CollectionData(
Expand All @@ -66,7 +66,7 @@ class CollectionAssetTests: SwiftUIAssetUnitTestCase {
)
)

let view = CollectionAssetView(model: model)
let view = await CollectionAssetView(model: model)

let stack = try view.inspect().vStack()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class InfoAssetTests: SwiftUIAssetUnitTestCase {
registry.register("action", asset: ActionAsset.self)
}

func testDecoding() throws {
func testDecoding() async throws {
let json = """
{
"id": "view-1",
Expand Down Expand Up @@ -53,16 +53,16 @@ class InfoAssetTests: SwiftUIAssetUnitTestCase {
}
"""

guard let info: InfoAsset = getAsset(json) else { return XCTFail("could not get asset") }
guard let info: InfoAsset = await getAsset(json) else { return XCTFail("could not get asset") }

_ = try info.view.inspect().find(InfoAssetView.self)
}

func testView() throws {
func testView() async throws {
guard
let title: TextAsset = getAsset("{\"id\": \"text\", \"type\": \"text\", \"value\":\"hello world\"}"),
let action1: ActionAsset = getAsset("{\"id\": \"action1\", \"type\": \"action\", \"value\":\"next\"}"),
let action2: ActionAsset = getAsset("{\"id\": \"action2\", \"type\": \"action\", \"value\":\"prev\"}")
let title: TextAsset = await getAsset("{\"id\": \"text\", \"type\": \"text\", \"value\":\"hello world\"}"),
let action1: ActionAsset = await getAsset("{\"id\": \"action1\", \"type\": \"action\", \"value\":\"next\"}"),
let action2: ActionAsset = await getAsset("{\"id\": \"action2\", \"type\": \"action\", \"value\":\"prev\"}")
else { return XCTFail("could not get assets") }

let data = InfoData(
Expand All @@ -75,7 +75,7 @@ class InfoAssetTests: SwiftUIAssetUnitTestCase {
]
)
let model = AssetViewModel<InfoData>(data)
let view = InfoAssetView(model: model)
let view = await InfoAssetView(model: model)

let stack = try view.inspect().vStack()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class InputAssetTests: SwiftUIAssetUnitTestCase {
registry.register("text", asset: TextAsset.self)
}

func testDecoding() throws {
func testDecoding() async throws {
let json = """
{
"id": "input",
Expand All @@ -41,7 +41,7 @@ class InputAssetTests: SwiftUIAssetUnitTestCase {
}
"""

guard let input: InputAsset = getAsset(json) else { return XCTFail("could not get asset") }
guard let input: InputAsset = await getAsset(json) else { return XCTFail("could not get asset") }

_ = try input.view.inspect().find(InputAssetView.self).vStack().textField(1)
}
Expand Down Expand Up @@ -123,9 +123,9 @@ class InputAssetTests: SwiftUIAssetUnitTestCase {
XCTAssertEqual(Color(red: 0.729, green: 0.745, blue: 0.773), try background.foregroundColor())
}

func testViewWithLabel() throws {
func testViewWithLabel() async throws {
guard
let label: TextAsset = getAsset("{\"id\": \"text\", \"type\": \"text\", \"value\":\"hello world\"}")
let label: TextAsset = await getAsset("{\"id\": \"text\", \"type\": \"text\", \"value\":\"hello world\"}")
else { return XCTFail("could not get asset") }
let val = context.evaluateScript("('a')")
let modelRef = ModelReference(rawValue: val)
Expand All @@ -142,7 +142,7 @@ class InputAssetTests: SwiftUIAssetUnitTestCase {

let model = InputAssetViewModel(data)

let view = InputAssetView(model: model)
let view = await InputAssetView(model: model)

let stack = try view.inspect().vStack()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class TextAssetTests: SwiftUIAssetUnitTestCase {
override func register(registry: SwiftUIRegistry) {
registry.register("text", asset: TextAsset.self)
}
func testAssetDecoding() throws {
func testAssetDecoding() async throws {
let json = """
{
"id": "text",
Expand All @@ -28,7 +28,7 @@ class TextAssetTests: SwiftUIAssetUnitTestCase {
}
"""

guard let text: TextAsset = getAsset(json) else { return XCTFail("could not get asset") }
guard let text: TextAsset = await getAsset(json) else { return XCTFail("could not get asset") }

_ = try text.view.inspect().find(TextAssetView.self).text()

Expand Down
122 changes: 122 additions & 0 deletions ios/packages/test-utils-core/Sources/AssetTestHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Foundation
import XCTest
import JavaScriptCore

extension JSContext {
func createAssetJsValue(string: String) -> JSValue {
guard let container = self.evaluateScript("(\(string))") else { fatalError("JSON was malformed") }
return container
}

func loadMakeFlow() {
guard objectForKeyedSubscript("MakeFlow").isUndefined else { return }
guard
let url = ResourceUtilities.urlForFile(
name: "make-flow.prod",
ext: "js",
bundle: Bundle(for: MakeFlowResourceShim.self), pathComponent: "TestUtilities.bundle"),
let contents = try? String(contentsOf: url)
else { return }
evaluateScript(contents)
}
}

class MakeFlowResourceShim {}

open class AssetTestHelper<WrapperType: AssetContainer & Decodable, Registry> where Registry: BaseAssetRegistry<WrapperType> {

/// The JSContext where utilities are loaded
/// and asset resolution is performed
public var context: JSContext = JSContext()

/// A closure to create the registry for this instance of the AssetTestHelper
public var makeRegistry: () -> Registry

/// Create an AssetTestHelper
/// - Parameter makeRegistry: A closure to create the registry
public init(makeRegistry: @escaping () -> Registry) {
self.makeRegistry = makeRegistry
context.loadMakeFlow()
}

/// Retrieves an asset from the provided JSON string definition
/// This utilizes @player-ui/make-flow to turn a single asset into a flow
/// and then runs that flow in a headless player to allow the registry to resolve asset IDs
/// - Parameters:
/// - json: The JSON Asset definition to decode
/// - plugins: Plugins to include for the headless player that resolves the assets
/// - Returns: The decoded asset if it was decodable
public func getAsset<Asset>(_ json: String, plugins: [NativePlugin] = []) async -> Asset? {
await Task { @MainActor () -> Asset? in
guard let flow = makeFlow(json) else { return nil }
let player = TestPlayer<WrapperType, Registry>(
plugins: plugins,
registry: makeRegistry()
)

let root: JSValue? = try? await withCheckedThrowingContinuation { result in
player.hooks?.viewController.tap({ (viewController) in
viewController.hooks.view.tap { (view) in
view.hooks.onUpdate.tap { val in
result.resume(returning: val)
}
}
})
player.start(flow: flow) { res in
guard case .failure(let error) = res else { return }
result.resume(throwing: error)
}
}

let jsValue = context.createAssetJsValue(string: json)
do {
return try player.assetRegistry.decode(jsValue) as? Asset
} catch {
// If the user passed in an entire flow, decode the asset that was the root of
// the flow
guard let root = root else { return nil }
return try? player.assetRegistry.decode(root) as? Asset
}
}.value
}

/**
Turns a single Asset JSON definition into a full flow
- parameters:
- json: The JSON definition of a single asset
- returns: A string that is a full JSON flow containing the single asset
*/
public func makeFlow(_ json: String) -> String? {
return context.evaluateScript("JSON.stringify(MakeFlow.makeFlow(\(json)))")?.toString()
}
}

public typealias SwiftUIAssetTestHelper = AssetTestHelper<WrappedAsset, SwiftUIRegistry>

public extension AssetTestHelper where WrapperType == WrappedAsset, Registry == SwiftUIRegistry {
/// An AssetTestHelper for `SwiftUIAsset`
static var swiftui: SwiftUIAssetTestHelper {
AssetTestHelper<WrappedAsset, SwiftUIRegistry> { SwiftUIRegistry(logger: TapableLogger()) }
}
}

public extension AssetTestHelper where WrapperType == WrappedAsset {
/**
Wraps a completion handler into a WrappedFunction for using XCTestExpectations to test functions
that are added to assets via a JS transform
- parameters:
- completion: A completion handler to run when the function is invoked
- returns: A WrappedFunction that will call your completion handler
*/
func getWrappedFunction<T>(completion: @escaping () -> Void) -> WrappedFunction<T>? {
let callback: @convention(block) (JSValue) -> JSValue = { value in
completion()
return value
}

guard
let function = JSValue(object: callback, in: context)
else { return nil }
return WrappedFunction(rawValue: function)
}
}
Loading