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]) as? TestRecord.Dict 132 133 expect(result).notTo(beNil()) 134 expect(result?["property"] as? String).to(equal(dict["property"])) 135 expect(result?["propertyWithCustomKey"] as? String).to(equal(dict["propertyWithCustomKey"])) 136 } 137 138 it("returns the record back (async)") { 139 waitUntil { done in 140 mockModuleHolder(appContext) { 141 AsyncFunction(functionName) { (a: TestRecord) in 142 return a 143 } 144 } 145 .call(function: functionName, args: [dict]) { result in 146 let value = try! result.get() 147 148 expect(value).notTo(beNil()) 149 expect(value).to(beAKindOf(Record.Dict.self)) 150 151 let valueAsDict = value as! Record.Dict 152 153 expect(valueAsDict["property"] as? String).to(equal(dict["property"])) 154 expect(valueAsDict["propertyWithCustomKey"] as? String).to(equal(dict["propertyWithCustomKey"])) 155 done() 156 } 157 } 158 } 159 } 160 161 it("throws when called with more arguments than expected") { 162 waitUntil { done in 163 mockModuleHolder(appContext) { 164 AsyncFunction(functionName) { (_: Int) in 165 return "something" 166 } 167 } 168 // Function expects one argument, let's give it more. 169 .call(function: functionName, args: [1, 2]) { result in 170 switch result { 171 case .failure(let error): 172 expect(error).notTo(beNil()) 173 expect(error).to(beAKindOf(InvalidArgsNumberException.self)) 174 case .success(_): 175 fail() 176 } 177 done() 178 } 179 } 180 } 181 182 it("allows to skip trailing optional arguments") { 183 let returnedValue = "something" 184 let fn = Function(functionName) { (a: String, b: Int?, c: Bool?) in 185 expect(c).to(beNil()) 186 return returnedValue 187 } 188 189 expect({ try fn.call(by: nil, withArguments: ["test"]) }) 190 .notTo(throwError()) 191 .to(be(returnedValue)) 192 193 expect({ try fn.call(by: nil, withArguments: ["test", 3]) }) 194 .notTo(throwError()) 195 .to(be(returnedValue)) 196 } 197 198 it("throws when called without required arguments") { 199 let fn = Function(functionName) { (requiredArgument: String, optionalArgument: Int?) in 200 return "something" 201 } 202 203 expect({ try fn.call(by: nil, withArguments: []) }) 204 .to(throwError(errorType: FunctionCallException.self) { error in 205 expect(error.rootCause).to(beAKindOf(InvalidArgsNumberException.self)) 206 let exception = error.rootCause as! InvalidArgsNumberException 207 expect(exception.param.received) == 0 208 expect(exception.param.required) == 1 209 expect(exception.param.expected) == 2 210 }) 211 } 212 213 it("throws when called with arguments of incompatible types") { 214 waitUntil { done in 215 mockModuleHolder(appContext) { 216 AsyncFunction(functionName) { (_: String) in 217 return "something" 218 } 219 } 220 // Function expects a string, let's give it a number. 221 .call(function: functionName, args: [1]) { result in 222 switch result { 223 case .failure(let error): 224 expect(error).notTo(beNil()) 225 expect(error).to(beAKindOf(ArgumentCastException.self)) 226 expect(error.isCausedBy(Conversions.CastingException<String>.self)) == true 227 case .success(_): 228 fail() 229 } 230 done() 231 } 232 } 233 } 234 } 235 236 context("JavaScript") { 237 let runtime = appContext.runtime 238 239 beforeSuite { 240 appContext.moduleRegistry.register(holder: mockModuleHolder(appContext) { 241 Name("TestModule") 242 243 Function("returnPi") { Double.pi } 244 245 Function("returnNull") { () -> Double? in 246 return nil 247 } 248 249 Function("isArgNull") { (arg: Double?) -> Bool in 250 return arg == nil 251 } 252 }) 253 } 254 255 it("returns values") { 256 expect(try runtime?.eval("expo.modules.TestModule.returnPi()").asDouble()) == Double.pi 257 expect(try runtime?.eval("expo.modules.TestModule.returnNull()").isNull()) == true 258 } 259 260 it("accepts optional arguments") { 261 expect(try runtime?.eval("expo.modules.TestModule.isArgNull(3.14)").asBool()) == false 262 expect(try runtime?.eval("expo.modules.TestModule.isArgNull(null)").asBool()) == true 263 } 264 } 265 } 266 } 267