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