1 import ExpoModulesTestCore 2 3 @testable import ExpoModulesCore 4 5 class FunctionSpec: ExpoSpec { 6 override func spec() { 7 let appContext = AppContext.create() 8 let functionName = "test function name" 9 10 context("native") { 11 func testFunctionReturning<T: Equatable>(value returnValue: T) { 12 waitUntil { done in 13 mockModuleHolder(appContext) { 14 AsyncFunction(functionName) { 15 return returnValue 16 } 17 } 18 .call(function: functionName, args: []) { result in 19 let value = try! result.get() 20 21 expect(value).notTo(beNil()) 22 expect(value).to(beAKindOf(T.self)) 23 expect(value as? T).to(equal(returnValue)) 24 done() 25 } 26 } 27 } 28 29 it("is called") { 30 waitUntil { done in 31 mockModuleHolder(appContext) { 32 AsyncFunction(functionName) { 33 done() 34 } 35 } 36 .call(function: functionName, args: []) 37 } 38 } 39 40 it("returns bool values") { 41 testFunctionReturning(value: true) 42 testFunctionReturning(value: false) 43 testFunctionReturning(value: [true, false]) 44 } 45 46 it("returns int values") { 47 testFunctionReturning(value: 1_234) 48 testFunctionReturning(value: [2, 1, 3, 7]) 49 } 50 51 it("returns double values") { 52 testFunctionReturning(value: 3.14) 53 testFunctionReturning(value: [0, 1.1, 2.2]) 54 } 55 56 it("returns string values") { 57 testFunctionReturning(value: "a string") 58 testFunctionReturning(value: ["expo", "modules", "core"]) 59 } 60 61 it("is called with nil value") { 62 let str: String? = nil 63 64 mockModuleHolder(appContext) { 65 AsyncFunction(functionName) { (a: String?) in 66 expect(a == nil) == true 67 } 68 } 69 .callSync(function: functionName, args: [str as Any]) 70 } 71 72 it("is called with an array of arrays") { 73 let array: [[String]] = [["expo"]] 74 75 mockModuleHolder(appContext) { 76 AsyncFunction(functionName) { (a: [[String]]) in 77 expect(a.first!.first) == array.first!.first 78 } 79 } 80 .callSync(function: functionName, args: [array]) 81 } 82 83 describe("converting dicts to records") { 84 struct TestRecord: Record { 85 @Field var property: String = "expo" 86 @Field var optionalProperty: Int? 87 @Field("propertyWithCustomKey") var customKeyProperty: String = "expo" 88 } 89 let dict = [ 90 "property": "Hello", 91 "propertyWithCustomKey": "Expo!" 92 ] 93 94 it("converts to simple record when passed as an argument") { 95 waitUntil { done in 96 mockModuleHolder(appContext) { 97 AsyncFunction(functionName) { (a: TestRecord) in 98 return a.property 99 } 100 } 101 .call(function: functionName, args: [dict]) { result in 102 let value = try! result.get() 103 104 expect(value).notTo(beNil()) 105 expect(value).to(beAKindOf(String.self)) 106 expect(value).to(be(dict["property"])) 107 done() 108 } 109 } 110 } 111 112 it("converts to record with custom key") { 113 waitUntil { done in 114 mockModuleHolder(appContext) { 115 AsyncFunction(functionName) { (a: TestRecord) in 116 return a.customKeyProperty 117 } 118 } 119 .call(function: functionName, args: [dict]) { result in 120 let value = try! result.get() 121 expect(value).notTo(beNil()) 122 expect(value).to(beAKindOf(String.self)) 123 expect(value).to(be(dict["propertyWithCustomKey"])) 124 done() 125 } 126 } 127 } 128 129 it("returns the record back") { 130 waitUntil { done in 131 mockModuleHolder(appContext) { 132 AsyncFunction(functionName) { (a: TestRecord) in 133 return a.toDictionary() 134 } 135 } 136 .call(function: functionName, args: [dict]) { result in 137 let value = try! result.get() 138 139 expect(value).notTo(beNil()) 140 expect(value).to(beAKindOf(Record.Dict.self)) 141 142 let valueAsDict = value as! Record.Dict 143 144 expect(valueAsDict["property"] as? String).to(equal(dict["property"])) 145 expect(valueAsDict["propertyWithCustomKey"] as? String).to(equal(dict["propertyWithCustomKey"])) 146 done() 147 } 148 } 149 } 150 } 151 152 it("throws when called with more arguments than expected") { 153 waitUntil { done in 154 mockModuleHolder(appContext) { 155 AsyncFunction(functionName) { (_: Int) in 156 return "something" 157 } 158 } 159 // Function expects one argument, let's give it more. 160 .call(function: functionName, args: [1, 2]) { result in 161 switch result { 162 case .failure(let error): 163 expect(error).notTo(beNil()) 164 expect(error).to(beAKindOf(InvalidArgsNumberException.self)) 165 case .success(_): 166 fail() 167 } 168 done() 169 } 170 } 171 } 172 173 it("throws when called with arguments of incompatible types") { 174 waitUntil { done in 175 mockModuleHolder(appContext) { 176 AsyncFunction(functionName) { (_: String) in 177 return "something" 178 } 179 } 180 // Function expects a string, let's give it a number. 181 .call(function: functionName, args: [1]) { result in 182 switch result { 183 case .failure(let error): 184 expect(error).notTo(beNil()) 185 expect(error).to(beAKindOf(ArgumentCastException.self)) 186 expect(error.isCausedBy(Conversions.CastingException<String>.self)) == true 187 case .success(_): 188 fail() 189 } 190 done() 191 } 192 } 193 } 194 } 195 196 context("JavaScript") { 197 let runtime = appContext.runtime 198 199 beforeSuite { 200 appContext.moduleRegistry.register(holder: mockModuleHolder(appContext) { 201 Name("TestModule") 202 203 Function("returnPi") { Double.pi } 204 205 Function("returnNull") { () -> Double? in 206 return nil 207 } 208 209 Function("isArgNull") { (arg: Double?) -> Bool in 210 return arg == nil 211 } 212 }) 213 } 214 215 it("returns values") { 216 expect(try runtime?.eval("ExpoModules.TestModule.returnPi()").asDouble()) == Double.pi 217 expect(try runtime?.eval("ExpoModules.TestModule.returnNull()").isNull()) == true 218 } 219 220 it("accepts optional arguments") { 221 expect(try runtime?.eval("ExpoModules.TestModule.isArgNull(3.14)").asBool()) == false 222 expect(try runtime?.eval("ExpoModules.TestModule.isArgNull(null)").asBool()) == true 223 } 224 } 225 } 226 } 227