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