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 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 (sync)") { 130 let result = try Function(functionName) { (record: TestRecord) in record } 131 .call(by: nil, withArguments: [dict], appContext: appContext) as? TestRecord 132 133 guard let result = Conversions.convertFunctionResult(result, appContext: appContext) as? TestRecord.Dict else { 134 return fail() 135 } 136 137 expect(result).notTo(beNil()) 138 expect(result["property"] as? String).to(equal(dict["property"])) 139 expect(result["propertyWithCustomKey"] as? String).to(equal(dict["propertyWithCustomKey"])) 140 } 141 142 it("returns the record back (async)") { 143 waitUntil { done in 144 mockModuleHolder(appContext) { 145 AsyncFunction(functionName) { (a: TestRecord) in 146 return a 147 } 148 } 149 .call(function: functionName, args: [dict]) { result in 150 let value = try! result.get() 151 152 expect(value).notTo(beNil()) 153 expect(value).to(beAKindOf(Record.Dict.self)) 154 155 let valueAsDict = value as! Record.Dict 156 157 expect(valueAsDict["property"] as? String).to(equal(dict["property"])) 158 expect(valueAsDict["propertyWithCustomKey"] as? String).to(equal(dict["propertyWithCustomKey"])) 159 done() 160 } 161 } 162 } 163 } 164 165 it("throws when called with more arguments than expected") { 166 waitUntil { done in 167 mockModuleHolder(appContext) { 168 AsyncFunction(functionName) { (_: Int) in 169 return "something" 170 } 171 } 172 // Function expects one argument, let's give it more. 173 .call(function: functionName, args: [1, 2]) { result in 174 switch result { 175 case .failure(let error): 176 expect(error).notTo(beNil()) 177 expect(error).to(beAKindOf(InvalidArgsNumberException.self)) 178 case .success(_): 179 fail() 180 } 181 done() 182 } 183 } 184 } 185 186 it("allows to skip trailing optional arguments") { 187 let returnedValue = "something" 188 let fn = Function(functionName) { (a: String, b: Int?, c: Bool?) in 189 expect(c).to(beNil()) 190 return returnedValue 191 } 192 193 expect({ try fn.call(by: nil, withArguments: ["test"], appContext: appContext) }) 194 .notTo(throwError()) 195 .to(be(returnedValue)) 196 197 expect({ try fn.call(by: nil, withArguments: ["test", 3], appContext: appContext) }) 198 .notTo(throwError()) 199 .to(be(returnedValue)) 200 } 201 202 it("throws when called without required arguments") { 203 let fn = Function(functionName) { (requiredArgument: String, optionalArgument: Int?) in 204 return "something" 205 } 206 207 expect({ try fn.call(by: nil, withArguments: [], appContext: appContext) }) 208 .to(throwError(errorType: FunctionCallException.self) { error in 209 expect(error.rootCause).to(beAKindOf(InvalidArgsNumberException.self)) 210 let exception = error.rootCause as! InvalidArgsNumberException 211 expect(exception.param.received) == 0 212 expect(exception.param.required) == 1 213 expect(exception.param.expected) == 2 214 }) 215 } 216 217 it("throws when called with arguments of incompatible types") { 218 waitUntil { done in 219 mockModuleHolder(appContext) { 220 AsyncFunction(functionName) { (_: String) in 221 return "something" 222 } 223 } 224 // Function expects a string, let's give it a number. 225 .call(function: functionName, args: [1]) { result in 226 switch result { 227 case .failure(let error): 228 expect(error).notTo(beNil()) 229 expect(error).to(beAKindOf(FunctionCallException.self)) 230 expect(error.isCausedBy(ArgumentCastException.self)) == true 231 expect(error.isCausedBy(Conversions.CastingException<String>.self)) == true 232 case .success(_): 233 fail() 234 } 235 done() 236 } 237 } 238 } 239 } 240 241 context("JavaScript") { 242 let runtime = try! appContext.runtime 243 244 beforeSuite { 245 appContext.moduleRegistry.register(holder: mockModuleHolder(appContext) { 246 Name("TestModule") 247 248 Function("returnPi") { Double.pi } 249 250 Function("returnNull") { () -> Double? in 251 return nil 252 } 253 254 Function("isArgNull") { (arg: Double?) -> Bool in 255 return arg == nil 256 } 257 258 Function("returnObjectDefinition") { (initial: Int) -> ObjectDefinition in 259 var foo = initial 260 261 return Object { 262 Function("increment") { () -> Int in 263 foo += 1 264 return foo 265 } 266 } 267 } 268 269 Function("withFunction") { (fn: JavaScriptFunction<String>) -> String in 270 return try fn.call("foo", "bar") 271 } 272 }) 273 } 274 275 it("returns values") { 276 expect(try runtime.eval("expo.modules.TestModule.returnPi()").asDouble()) == Double.pi 277 expect(try runtime.eval("expo.modules.TestModule.returnNull()").isNull()) == true 278 } 279 280 it("accepts optional arguments") { 281 expect(try runtime.eval("expo.modules.TestModule.isArgNull(3.14)").asBool()) == false 282 expect(try runtime.eval("expo.modules.TestModule.isArgNull(null)").asBool()) == true 283 } 284 285 it("returns object made from definition") { 286 let initialValue = Int.random(in: 1..<100) 287 let object = try runtime.eval("object = expo.modules.TestModule.returnObjectDefinition(\(initialValue))") 288 289 expect(object.kind) == .object 290 expect(object.getObject().hasProperty("increment")) == true 291 292 let result = try runtime.eval("object.increment()") 293 294 expect(result.kind) == .number 295 expect(result.getInt()) == initialValue + 1 296 } 297 298 it("takes JavaScriptFunction argument") { 299 let value = try runtime.eval("expo.modules.TestModule.withFunction((a, b) => a + b)") 300 301 expect(value.kind) == .string 302 expect(value.getString()) == "foobar" 303 } 304 } 305 } 306 } 307