// Copyright 2022-present 650 Industries. All rights reserved. import ExpoModulesTestCore @testable import ExpoModulesCore class ExpoModulesSpec: ExpoSpec { override class func spec() { let appContext = AppContext.create() let runtime = try! appContext.runtime let testModuleName = "TestModule" let testFunctionName = "testFunction" let throwingFunctionName = "throwingFunction" let exceptionToThrow = Exception(name: "Some exception", description: "Exception description") let constantsDict: [String: Any] = [ "expo": "is cool", "sdk": 45, ] beforeSuite { appContext.moduleRegistry.register(holder: mockModuleHolder(appContext) { Name(testModuleName) Constants(constantsDict) Function(testFunctionName) { Double.pi } Function(throwingFunctionName) { throw exceptionToThrow } }) } describe("host object") { it("is defined") { expect(try! runtime.eval("'expo' in this").asBool()).to(beTrue()) expect(try! runtime.eval("'modules' in expo").asBool()).to(beTrue()) } it("has native module defined") { expect(try! runtime.eval("'\(testModuleName)' in expo.modules").asBool()).to(beTrue()) } it("can access native module") { let nativeModule = try runtime.eval("expo.modules.\(testModuleName)") expect(nativeModule.isUndefined()) == false expect(nativeModule.isObject()) == true expect(nativeModule.getRaw()).notTo(beNil()) } it("has keys for registered modules") { let registeredModuleNames = appContext.moduleRegistry.getModuleNames() let keys = try runtime.eval("Object.keys(expo.modules)").asArray().compactMap { return try! $0?.asString() } expect(keys).to(contain(registeredModuleNames)) } } describe("module") { it("exposes constants") { let dict = try runtime.eval("expo.modules.TestModule").asDict() dict.forEach { (key: String, value: Any) in expect(value as! NSObject) === dict[key] as! NSObject } } it("has function") { expect(try runtime.eval("typeof expo.modules.TestModule.\(testFunctionName)").asString()) == "function" expect(try runtime.eval("expo.modules.TestModule.\(testFunctionName)").isFunction()) == true } it("calls function") { expect(try runtime.eval("expo.modules.TestModule.\(testFunctionName)()").asDouble()) == Double.pi } it("throws from sync function") { // Invoke the throwing function and return the error (eval shouldn't rethrow here) let error = try runtime.eval("try { expo.modules.TestModule.\(throwingFunctionName)() } catch (error) { error }").asObject() // We just check if it contains the description — they won't be equal for the following reasons: // - the `exceptionToThrow` is just the root cause, in fact it returns `FunctionCallException` // - the debug description contains the file and line number, so it's hard to mock the `FunctionCallException` // Ideally if we have a better way (error codes/names) to identify them w/o relying on the description that may change over time. expect(error.getProperty("message").getString()).to(contain(exceptionToThrow.debugDescription)) } } } }