import ExpoModulesTestCore @testable import ExpoModulesCore class ClassDefinitionSpec: ExpoSpec { override class func spec() { describe("basic") { it("factory returns a definition") { let klass = Class("") {} expect(klass).to(beAnInstanceOf(ClassDefinition.self)) } it("has a name") { let expoClassName = "ExpoClass" let klass = Class(expoClassName) {} expect(klass.name) == expoClassName } it("is without native constructor") { let klass = Class("") {} expect(klass.constructor).to(beNil()) } it("has native constructor") { let klass = Class("") { Constructor {} } expect(klass.constructor).notTo(beNil()) } it("ignores constructor as function") { let klass = Class("") { Constructor {} } expect(klass.functions["constructor"]).to(beNil()) } it("builds a class") { let appContext = AppContext.create() let klass = Class("") {} let object = try klass.build(appContext: appContext) expect(object.hasProperty("prototype")) == true expect(object.getProperty("prototype").kind) == .object } } describe("module") { let appContext = AppContext.create() let runtime = try! appContext.runtime beforeSuite { class ClassTestModule: Module { func definition() -> ModuleDefinition { Name("ClassTest") Class("MyClass") { Constructor {} Function("myFunction") { return "foobar" } Property("foo") { return "bar" } } } } appContext.moduleRegistry.register(moduleType: ClassTestModule.self) } it("is a function") { let klass = try runtime.eval("expo.modules.ClassTest.MyClass") expect(klass.isFunction()) == true } it("has a name") { let klass = try runtime.eval("expo.modules.ClassTest.MyClass.name") expect(klass.getString()) == "MyClass" } it("has a prototype") { let prototype = try runtime.eval("expo.modules.ClassTest.MyClass.prototype") expect(prototype.isObject()) == true } it("has keys in prototype") { let prototypeKeys = try runtime.eval("Object.keys(expo.modules.ClassTest.MyClass.prototype)") .getArray() .map { $0.getString() } expect(prototypeKeys).to(contain("myFunction")) expect(prototypeKeys).notTo(contain("__native_constructor__")) } it("is an instance of") { let isInstanceOf = try runtime.eval([ "myObject = new expo.modules.ClassTest.MyClass()", "myObject instanceof expo.modules.ClassTest.MyClass", ]) expect(isInstanceOf.getBool()) == true } it("defines properties on initialization") { // The properties are not specified in the prototype, but defined during initialization. let object = try runtime.eval("new expo.modules.ClassTest.MyClass()").asObject() expect(object.getPropertyNames()).to(contain("foo")) expect(object.getProperty("foo").getString()) == "bar" } } describe("class with associated type") { let appContext = AppContext.create() let runtime = try! appContext.runtime beforeSuite { appContext.moduleRegistry.register(moduleType: ModuleWithCounterClass.self) } it("is defined") { let isDefined = try runtime.eval("'Counter' in expo.modules.TestModule") expect(isDefined.getBool()) == true } it("creates shared object") { let jsObject = try runtime.eval("new expo.modules.TestModule.Counter(0)").getObject() let nativeObject = appContext.sharedObjectRegistry.toNativeObject(jsObject) expect(nativeObject).notTo(beNil()) } it("registers shared object") { let oldSize = appContext.sharedObjectRegistry.size try runtime.eval("object = new expo.modules.TestModule.Counter(0)") expect(appContext.sharedObjectRegistry.size) == oldSize + 1 } it("calls function with owner") { try runtime.eval([ "object = new expo.modules.TestModule.Counter(0)", "object.increment(1)", ]) // no expectations, just checking if it doesn't fail } it("creates with initial value") { let initialValue = Int.random(in: 1..<100) let value = try runtime.eval([ "object = new expo.modules.TestModule.Counter(\(initialValue))", "object.getValue()", ]) expect(value.kind) == .number expect(value.getInt()) == initialValue } it("gets shared object value") { let value = try runtime.eval([ "object = new expo.modules.TestModule.Counter(0)", "object.getValue()", ]) expect(value.kind) == .number expect(value.isNumber()) == true } it("changes shared object") { try runtime.eval("object = new expo.modules.TestModule.Counter(0)") let incrementBy = Int.random(in: 1..<100) let value = try runtime.eval("object.getValue()").asInt() let newValue = try runtime.eval([ "object.increment(\(incrementBy))", "object.getValue()", ]) expect(newValue.kind) == .number expect(newValue.getInt()) == value + incrementBy } it("gets value from the dynamic property") { let initialValue = Int.random(in: 1..<100) let value = try runtime.eval([ "object = new expo.modules.TestModule.Counter(\(initialValue))", "object.currentValue" ]) expect(value.kind) == .number expect(value.getInt()) == initialValue } it("initializes the shared object from native") { let initialValue = Int.random(in: 1..<100) let value = try runtime.eval("expo.modules.TestModule.newCounter(\(initialValue))") expect(value.kind) == .object expect(value.getObject().getProperty("currentValue").getInt()) == initialValue } } } } /** A module that exposes a Counter class with an associated shared object class. */ fileprivate final class ModuleWithCounterClass: Module { func definition() -> ModuleDefinition { Name("TestModule") Function("newCounter") { (initialValue: Int) in return Counter(initialValue: initialValue) } Class(Counter.self) { Constructor { (initialValue: Int) in return Counter(initialValue: initialValue) } Function("increment") { (counter, value: Int) in counter.increment(by: value) } Function("getValue") { counter in return counter.currentValue } Property("currentValue") { counter in return counter.currentValue } } } } /** A shared object class that stores some native value and can be used as an associated type of the JS class. */ fileprivate final class Counter: SharedObject { var currentValue = 0 init(initialValue: Int = 0) { self.currentValue = initialValue } func increment(by value: Int = 1) { currentValue += value } }