diff --git a/src/utils/class.js b/src/utils/class.js index 6b062d90..ca1072d9 100644 --- a/src/utils/class.js +++ b/src/utils/class.js @@ -37,28 +37,28 @@ export function extendClassStaticProps(childClass, parentClass, excludedProps = } /** - * Extends an object with a parent class namespace. + * Slightly different implementation of extendClass assuming excludedProps + * is contained within the child-most class definition and assigning only + * the most recent props rather than the most distant props. * See extendClass. - * TODO : match call signature with extendClass and - * make use of getPrototypeOf rather than __proto__ - * but verify correctness in additional use cases first */ -export function extendThis(child, childClass, parentClass, excludedProps = [], ...args) { - let props; - let obj = new parentClass.prototype.constructor(...args); +export function extendThis(childClass, parentClass, config) { + let props, protos; + let obj = new parentClass.prototype.constructor(config); const exclude = ["constructor", ...Object.getOwnPropertyNames(childClass.prototype)]; const seen = []; // remember most recent occurrence of prop name (like inheritance) - while (obj.__proto__) { - props = Object.getOwnPropertyNames(obj.__proto__); + while (Object.getPrototypeOf(obj)) { + protos = Object.getPrototypeOf(obj); + props = Object.getOwnPropertyNames(protos); props.filter(p => !exclude.includes(p)).map((prop) => { if (seen.includes(prop)) return; - const getter = obj.__lookupGetter__(prop); - const setter = obj.__lookupSetter__(prop); - if (getter) child.__defineGetter__(prop, getter); - if (setter) child.__defineSetter__(prop, setter); - if (!(getter || setter)) child[prop] = obj[prop]; + const getter = protos.__lookupGetter__(prop); + const setter = protos.__lookupSetter__(prop); + if (getter) childClass.prototype.__defineGetter__(prop, getter); + if (setter) childClass.prototype.__defineSetter__(prop, setter); + if (!(getter || setter)) childClass.prototype[prop] = protos[prop]; seen.push(prop); }) - obj = obj.__proto__; + obj = protos; } } diff --git a/tests/utils.class.js b/tests/utils.class.js new file mode 100644 index 00000000..0616d282 --- /dev/null +++ b/tests/utils.class.js @@ -0,0 +1,133 @@ +import { mix } from "mixwith"; +import { expect } from "chai"; +import { InMemoryEntity, NamedInMemoryEntity, DefaultableMixin, RuntimeItemsMixin } from "../src/entity"; +import { deepClone } from "../src/utils/clone"; +import { extendClass, extendThis } from "../src/utils/class"; + + +class BaseEntity extends mix(InMemoryEntity).with(RuntimeItemsMixin) { + + constructor(config) { + super(config); + } + + baseMethod() { + return "base"; + } + +} + + +class ExtendClassEntity extends mix(NamedInMemoryEntity).with(DefaultableMixin) { + + constructor(config, excluded = []) { + super(config); + extendClass(ExtendClassEntity, BaseEntity, excluded, [config]); + + } + + baseMethod() { + return "derived"; + } + +} + + +class BaseBetweenEntity extends NamedInMemoryEntity { + + static staticAttr = "base"; + + constructor(config) { + super(config); + this.instanceAttr = "base"; + } + + betweenMethod() { + return "base"; + } + +} + + +class BetweenEntity extends BaseBetweenEntity { + + static staticAttr = "between"; + + constructor(config) { + super(config); + this.instanceAttr = "between"; + } + + betweenMethod() { + return "between"; + } +} + + +class ExtendThisEntity extends mix(BetweenEntity).with(DefaultableMixin) { + + constructor(config, excluded = []) { + super(config); + extendThis(ExtendThisEntity, BaseEntity, config); + + } + + baseMethod() { + return "derived"; + } + +} + + +describe("extendClass", () => { + + it("extends classes no excluded props", () => { + const obj = new ExtendClassEntity({}); + expect(obj.baseMethod()).to.be.equal("base"); + }); + + it("should support excluded props but doesnt", () => { + const obj = new ExtendClassEntity({}); + expect(obj.baseMethod()).not.to.be.equal("derived"); + }); + + it("should have results but doesnt", () => { + const obj = new ExtendClassEntity({"results": ["test"]}); + expect(JSON.stringify(obj.results)).not.to.be.equal(JSON.stringify([{"name": "test"}])); + }); + +}); + + +describe("extendThis", () => { + + it("extends this prefer child method", () => { + const obj = new ExtendThisEntity({}); + expect(obj.baseMethod()).to.be.equal("derived"); + }); + + it("extends this support base mixins", () => { + const obj = new ExtendThisEntity({"results": ["test"]}); + expect(JSON.stringify(obj.results)).to.be.equal(JSON.stringify([{"name": "test"}])); + }); + + it("remembers intermediate methods", () => { + const base = new BaseBetweenEntity(); + expect(base.betweenMethod()).to.be.equal("base"); + const obj = new ExtendThisEntity({}); + expect(obj.betweenMethod()).to.be.equal("between"); + }); + + it("propagates instance attributes", () => { + const base = new BaseBetweenEntity({}); + expect(base.instanceAttr).to.be.equal("base"); + const obj = new ExtendThisEntity({}); + expect(obj.instanceAttr).to.be.equal("between"); + }); + + it("propagates static attributes", () => { + expect(BaseBetweenEntity.staticAttr).to.be.equal("base"); + expect(ExtendThisEntity.staticAttr).to.be.equal("between"); + }); + +});