From ca80dfbfa3f67f8ea5527d676e4bd1e13b1eac5f Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Tue, 7 Mar 2023 16:34:14 -0600 Subject: [PATCH] implement and test final proposal --- packages/core/src/pattern-config.mjs | 120 +++-- packages/core/tests/pattern-init.test.mjs | 185 -------- .../core/tests/pattern-part-hiding.test.mjs | 415 ++++++++++++++++++ 3 files changed, 474 insertions(+), 246 deletions(-) create mode 100644 packages/core/tests/pattern-part-hiding.test.mjs diff --git a/packages/core/src/pattern-config.mjs b/packages/core/src/pattern-config.mjs index 020e698dce8..2af181ce3aa 100644 --- a/packages/core/src/pattern-config.mjs +++ b/packages/core/src/pattern-config.mjs @@ -1,4 +1,4 @@ -import { __addNonEnumProp } from './utils.mjs' +import { __addNonEnumProp, capitalize } from './utils.mjs' /** * Get the name of the given plugin config @@ -46,12 +46,16 @@ export function PatternConfig(pattern) { __addNonEnumProp(this, '__mutated', { optionDistance: {}, partDistance: {}, + hideDistance: {}, }) /** @type {Object} tracking for dependency hiding */ __addNonEnumProp(this, '__hiding', { - all: {}, - deps: {}, + from: {}, + after: {}, + inherited: {}, + always: {}, + never: {}, }) } @@ -133,14 +137,15 @@ PatternConfig.prototype.asConfig = function () { PatternConfig.prototype.__addPart = function (depChain) { // the current part is the head of the chain const part = depChain[0] - // the longer the chain, the deeper the part is down it - const distance = depChain.length + + // only process a part that hasn't already been processed if (!this.parts[part.name]) this.parts[part.name] = Object.freeze(part) else return // if it hasn't been registered with a distance, do that now if (typeof this.__mutated.partDistance[part.name] === 'undefined') { - this.__mutated.partDistance[part.name] = distance + // the longer the chain, the deeper the part is down it + this.__mutated.partDistance[part.name] = depChain.length if (DISTANCE_DEBUG) this.store.log.debug( @@ -149,11 +154,7 @@ PatternConfig.prototype.__addPart = function (depChain) { } // Handle various hiding possibilities - if (part.hide || part.hideAll) this.partHide[part.name] = true - if (part.hideDependencies) this.__hiding.deps[part.name] = true - if (part.hideAll) { - this.__hiding.all[part.name] = true - } + this.__resolvePartHiding(part) // resolve its dependencies this.__resolvePartDependencies(depChain) @@ -188,7 +189,7 @@ PatternConfig.prototype.__addPartOptions = function (part) { if (!part.options) return this // get the part's option priority - const partDistance = this.__mutated.partDistance?.[part.name] || 0 + const partDistance = this.__mutated.partDistance?.[part.name] // loop through options for (const optionName in part.options) { @@ -325,7 +326,37 @@ PatternConfig.prototype.__addPartPlugins = function (part) { // the two types of dependencies const depTypes = ['from', 'after'] +const exceptionTypes = ['never', 'always'] +PatternConfig.prototype.__resolvePartHiding = function (part) { + if (part.hide) { + // get the part's option priority + const partDistance = this.__mutated.partDistance?.[part.name] + const neverDistance = this.__hiding.never[part.name] || Infinity + const alwaysDistance = this.__hiding.always[part.name] || Infinity + if (part.hide.self && (neverDistance > partDistance || alwaysDistance <= neverDistance)) + this.partHide[part.name] = true + + exceptionTypes.forEach((e, i) => { + if (part.hide[e]) { + part.hide[e].forEach((p) => { + const otherDistance = this.__hiding[exceptionTypes[Math.abs(i - 1)]][p] || Infinity + + if (otherDistance > partDistance) { + const thisDistance = this.__hiding[e][p] || Infinity + this.__hiding[e][p] = Math.min(thisDistance, partDistance) + this.partHide[p] = i == 1 + } + }) + } + }) + + Object.keys(this.__hiding).forEach((k) => { + if (!exceptionTypes.includes(k) && this.__hiding[k][part.name] === undefined) + this.__hiding[k][part.name] = part.hide[k] + }) + } +} /** * Recursively register part dependencies * triggers {@link __addPart} on new parts found during resolution @@ -364,9 +395,11 @@ PatternConfig.prototype.__resolvePartDependencies = function (depChain) { this.__addPart([dot, ...depChain]) } else { // if it's already registered, recursion won't happen, but we still need to add its resolved dependencies to all parts in the chain - this.resolvedDependencies[dot.name].forEach((r) => { - depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, r)) - }) + // this.resolvedDependencies[dot.name].forEach((r) => { + // depChain.forEach((c) => this.__resolvePartDependencies('resolvedDependencies', c.name, r)) + // }) + this.__resolvePartDependencies([dot, ...depChain]) + // and check for stricter hiding policies } }) } @@ -399,14 +432,18 @@ PatternConfig.prototype.__addDependency = function (dependencyList, partName, de * @private */ PatternConfig.prototype.__handlePartDependencyOfType = function (part, depName, depType) { - switch (depType) { - case 'from': - this.__setFromHide(part, depName) - this.inject[part.name] = depName - break - case 'after': - this.__setAfterHide(part, depName) + if (this.__hiding[depType][part.name] === true && this.partHide[depName] === undefined) { + this.partHide[depName] = true } + + const hideInherited = this.__hiding.inherited[part.name] + if (depType === 'from') { + this.inject[part.name] = depName + this.__hiding.after[depName] = hideInherited + } + + this.__hiding.from[depName] = hideInherited + this.__hiding.inherited[depName] = hideInherited } /** @@ -455,42 +492,3 @@ PatternConfig.prototype.__resolveDraftOrder = function () { return this.__draftOrder } - -/** - * Sets visibility of a 'from' dependency based on its config - * - * @private - * @param {Part} part - The part of which this is a dependency - * @param {string} depName - The name of the dependency - * @return {Pattern} this - The Pattern instance - */ -PatternConfig.prototype.__setFromHide = function (part, depName) { - if (this.__hiding.deps[part.name]) { - this.partHide[depName] = true - this.__hiding.deps[depName] = true - } - if (this.__hiding.all[part.name]) { - this.partHide[depName] = true - this.__hiding.all[depName] = true - } - - return this -} - -/** - * Sets visibility of an 'after' dependency based on its config - * - * @private - * @param {Part} part - The part of which this is a dependency - * @param {string} depName - The name of the dependency - * @param {int} set - The index of the set in the list of settings - * @return {Pattern} this - The Pattern instance - */ -PatternConfig.prototype.__setAfterHide = function (part, depName) { - if (this.__hiding.all[part.name]) { - this.partHide[depName] = true - this.__hiding.all[depName] = true - } - - return this -} diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 8a3e4d21f77..743d8bc5ab7 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -814,191 +814,6 @@ describe('Pattern', () => { expect(count).to.equal(2) }) - describe('Hiding parts', () => { - const blankDraft = ({ part }) => part - const afterPart = { - name: 'afterPart', - draft: blankDraft, - } - const fromPart = { - name: 'fromPart', - draft: blankDraft, - } - describe('{hide: true}', () => { - const mainPart = { - name: 'mainPart', - after: afterPart, - from: fromPart, - hide: true, - draft: blankDraft, - } - - const Test = new Design({ - name: 'test', - parts: [mainPart], - }) - - const pattern = new Test() - pattern.__init() - - it('Should hide the part', () => { - expect(pattern.__isPartHidden('mainPart')).to.be.true - }) - - it("Should not hide the part's dependencies", () => { - expect(pattern.__isPartHidden('fromPart')).to.be.false - expect(pattern.__isPartHidden('afterPart')).to.be.false - }) - - describe('Nested Parts', () => { - const mainPart = { - name: 'mainPart', - after: afterPart, - from: fromPart, - draft: blankDraft, - } - const grandChild = { - name: 'grandChild', - from: mainPart, - hide: true, - draft: blankDraft, - } - const Test = new Design({ - name: 'test', - parts: [grandChild], - }) - - const pattern = new Test() - pattern.__init() - - it('should not hide nested `from` dependencies', () => { - expect(pattern.__isPartHidden('fromPart')).to.be.false - expect(pattern.__isPartHidden('mainPart')).to.be.false - }) - - it('should not hide nested `after` dependencies', () => { - expect(pattern.__isPartHidden('afterPart')).to.be.false - }) - }) - }) - - describe('{hideDependencies: true}', () => { - const mainPart = { - name: 'mainPart', - hideDependencies: true, - after: afterPart, - from: fromPart, - draft: blankDraft, - } - const Test = new Design({ - name: 'test', - parts: [mainPart], - }) - - const pattern = new Test() - pattern.__init() - - it('Should not hide the part', () => { - expect(pattern.__isPartHidden('mainPart')).to.be.false - }) - it("Should hide the part's `from` dependencies", () => { - expect(pattern.__isPartHidden('fromPart')).to.be.true - }) - it("Should not hide the part's `after` dependencies", () => { - expect(pattern.__isPartHidden('afterPart')).to.be.false - }) - - describe('Nested Parts', () => { - const mainPart = { - name: 'mainPart', - after: afterPart, - from: fromPart, - draft: blankDraft, - } - const grandChild = { - name: 'grandChild', - from: mainPart, - hideDependencies: true, - draft: blankDraft, - } - const Test = new Design({ - name: 'test', - parts: [grandChild], - }) - - const pattern = new Test() - pattern.__init() - - it('should hide nested `from` dependencies', () => { - expect(pattern.__isPartHidden('fromPart')).to.be.true - expect(pattern.__isPartHidden('mainPart')).to.be.true - }) - - it('should not hide nested `after` dependencies', () => { - expect(pattern.__isPartHidden('afterPart')).to.be.false - }) - }) - }) - - describe('{hideAll: true}', () => { - const mainPart = { - name: 'mainPart', - hideAll: true, - after: afterPart, - from: fromPart, - draft: blankDraft, - } - const Test = new Design({ - name: 'test', - parts: [mainPart], - }) - - const pattern = new Test() - pattern.__init() - - it('Should hide the part', () => { - expect(pattern.__isPartHidden('mainPart')).to.be.true - }) - it("Should hide the part's `from` dependencies", () => { - expect(pattern.__isPartHidden('fromPart')).to.be.true - }) - it("Should hide the part's `after` dependencies", () => { - expect(pattern.__isPartHidden('afterPart')).to.be.true - }) - - describe('Nested Parts', () => { - const mainPart = { - name: 'mainPart', - after: afterPart, - from: fromPart, - draft: blankDraft, - } - const grandChild = { - name: 'grandChild', - from: mainPart, - hideAll: true, - draft: blankDraft, - } - const Test = new Design({ - name: 'test', - parts: [grandChild], - }) - - const pattern = new Test() - pattern.__init() - - it('should hide nested `from` dependencies', () => { - expect(pattern.__isPartHidden('fromPart')).to.be.true - expect(pattern.__isPartHidden('mainPart')).to.be.true - }) - - it('should hide nested `after` dependencies', () => { - expect(pattern.__isPartHidden('afterPart')).to.be.true - }) - }) - }) - }) - it('Should check whether created parts get the pattern context', () => { let partContext const plugin = { diff --git a/packages/core/tests/pattern-part-hiding.test.mjs b/packages/core/tests/pattern-part-hiding.test.mjs new file mode 100644 index 00000000000..ce5c5e9d277 --- /dev/null +++ b/packages/core/tests/pattern-part-hiding.test.mjs @@ -0,0 +1,415 @@ +import chai from 'chai' +import { Design } from '../src/index.mjs' + +const expect = chai.expect +function hidePartMatcher(partName) { + const isHidden = this._obj.__isPartHidden(partName) + if (!this._obj.config.parts[partName]) { + throw new chai.AssertionError(`expected part \`${partName}\` to exist in pattern`) + this.fail() + } + this.assert( + isHidden, + `expected part ${partName} to be hidden, but it is shown`, + `expected part ${partName} to NOT be hidden, but it is hidden` + ) +} +chai.Assertion.addMethod('hidePart', hidePartMatcher) + +const blankDraft = ({ part }) => part +const blankPart = (name, config = {}) => ({ + name, + draft: blankDraft, + ...config, +}) + +describe('Hiding parts', () => { + const afterPart = blankPart('afterPart') + const fromPart = blankPart('fromPart') + + describe('With {hide: {self: true}}', () => { + const mainPart = { + name: 'mainPart', + after: afterPart, + from: fromPart, + hide: { self: true }, + draft: blankDraft, + } + + const Test = new Design({ + name: 'test', + parts: [mainPart], + }) + + const pattern = new Test() + pattern.__init() + + it('Should hide the part', () => { + expect(pattern).to.hidePart('mainPart') + }) + + it("Should NOT hide the part's dependencies", () => { + expect(pattern).not.to.hidePart('fromPart') + expect(pattern).not.to.hidePart('afterPart') + }) + + describe('Inherited Parts', () => { + const mainPart = { + name: 'mainPart', + after: afterPart, + from: fromPart, + draft: blankDraft, + } + const grandChild = { + name: 'grandChild', + from: mainPart, + hide: { self: true }, + draft: blankDraft, + } + const Test = new Design({ + name: 'test', + parts: [grandChild], + }) + + const pattern = new Test() + pattern.__init() + + it('Should NOT hide inherited `from` dependencies', () => { + expect(pattern).not.to.hidePart('fromPart') + expect(pattern).not.to.hidePart('mainPart') + }) + + it('Should NOT hide inherited `after` dependencies', () => { + expect(pattern).not.to.hidePart('afterPart') + }) + }) + }) + + describe('With {hide: {from: true}}', () => { + const mainPart = { + name: 'mainPart', + hide: { from: true }, + after: afterPart, + from: fromPart, + draft: blankDraft, + } + + const Test = new Design({ + name: 'test', + parts: [mainPart], + }) + + const pattern = new Test() + pattern.__init() + + it('Should NOT hide the part', () => { + expect(pattern).not.to.hidePart('mainPart') + }) + it("Should hide the part's `from` dependencies", () => { + expect(pattern).to.hidePart('fromPart') + }) + it("Should NOT hide the part's `after` dependencies", () => { + expect(pattern).not.to.hidePart('afterPart') + }) + + describe('Inherited Parts', () => { + const mainPart = { + name: 'mainPart', + after: afterPart, + from: fromPart, + draft: blankDraft, + } + const grandChild = { + name: 'grandChild', + from: mainPart, + hide: { from: true }, + draft: blankDraft, + } + const Test = new Design({ + name: 'test', + parts: [grandChild], + }) + + const pattern = new Test() + pattern.__init() + + it("Should hide the part's `from` dependencies", () => { + expect(pattern).to.hidePart('mainPart') + }) + + it('Should NOT hide inherited `from` dependencies', () => { + expect(pattern).not.to.hidePart('fromPart') + }) + + it('Should NOT hide inherited `after` dependencies', () => { + expect(pattern).not.to.hidePart('afterPart') + }) + }) + }) + + describe('With {hide: {after: true}}', () => { + const mainPart = { + name: 'mainPart', + hide: { after: true }, + after: afterPart, + from: fromPart, + draft: blankDraft, + } + const Test = new Design({ + name: 'test', + parts: [mainPart], + }) + + const pattern = new Test() + pattern.__init() + + it('Should NOT hide the part', () => { + expect(pattern).not.to.hidePart('mainPart') + }) + it("Should NOT hide the part's `from` dependencies", () => { + expect(pattern).not.to.hidePart('fromPart') + }) + it("Should hide the part's `after` dependencies", () => { + expect(pattern).to.hidePart('afterPart') + }) + + describe('Inherited Parts', () => { + const mainPart = { + name: 'mainPart', + after: afterPart, + from: fromPart, + draft: blankDraft, + } + const grandChild = { + name: 'grandChild', + from: mainPart, + hide: { after: true }, + draft: blankDraft, + } + const Test = new Design({ + name: 'test', + parts: [grandChild], + }) + + const pattern = new Test() + pattern.__init() + + it('Should NOT hide inherited `from` dependencies', () => { + expect(pattern).not.to.hidePart('fromPart') + }) + + it('Should NOT hide inherited `after` dependencies', () => { + expect(pattern).not.to.hidePart('afterPart') + }) + }) + }) + + describe('With {hide: {inherited: true}}', () => { + const grandParent = blankPart('grandParent', { from: fromPart, after: afterPart }) + const parentAfter = blankPart('parentAfter') + const parent = blankPart('parent', { from: grandParent, after: parentAfter }) + const mainAfterFrom = blankPart('mainAfterFrom') + const mainAfterAfter = blankPart('mainAfterAfter') + const mainAfter = blankPart('mainAfter', { after: mainAfterAfter, from: mainAfterFrom }) + const mainPart = { + name: 'mainPart', + from: parent, + after: mainAfter, + hide: { inherited: true }, + draft: blankDraft, + } + const Test = new Design({ + name: 'test', + parts: [mainPart], + }) + + const pattern = new Test() + pattern.__init() + + it('Should NOT hide the part', () => { + expect(pattern).not.to.hidePart('mainPart') + }) + + it('Should NOT hide the `from` dependency', () => { + expect(pattern).not.to.hidePart('parent') + }) + + it('Should NOT hide the `after` dependency', () => { + expect(pattern).not.to.hidePart('mainAfter') + }) + + it('Should NOT hide the `after` dependencies of `after` dependencies', () => { + expect(pattern).not.to.hidePart('mainAfterAfter') + }) + + it('Should hide the `from` dependencies of `after` dependencies', () => { + expect(pattern).to.hidePart('mainAfterFrom') + }) + + it('Should hide the `after` dependencies of `from` dependencies', () => { + expect(pattern).to.hidePart('afterPart') + expect(pattern).to.hidePart('parentAfter') + }) + + it('Should hide the `from` dependencies of `from` dependencies', () => { + expect(pattern).to.hidePart('fromPart') + expect(pattern).to.hidePart('grandParent') + }) + }) + + describe("With {hide: {always: ['partname']} }", () => { + it('Should hide the given part', () => { + const mainPart = blankPart('mainPart', { + after: afterPart, + hide: { + always: ['afterPart'], + }, + }) + + const Test = new Design({ + name: 'test', + parts: [mainPart], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).to.hidePart('afterPart') + }) + + it("Should NOT hide the given part if a higher-level part includes it in {hide: {never: ['partName']} }", () => { + const grandParent = blankPart('grandParent') + const parent = blankPart('parent', { + from: grandParent, + hide: { always: ['grandParent'] }, + }) + const main1 = blankPart('main1', { + from: parent, + hide: { from: true, never: ['grandParent'] }, + }) + + const Test = new Design({ + name: 'test', + parts: [main1], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).not.to.hidePart('grandParent') + }) + }) + + describe("With {hide: {never: ['partName']} }", () => { + it('Should NOT hide the given part even if another setting would hide it', () => { + const parent = blankPart('parent') + const main1 = blankPart('main1', { + from: parent, + hide: { from: true, never: ['parent'] }, + }) + + const Test = new Design({ + name: 'test', + parts: [main1], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).not.to.hidePart('parent') + }) + + it("Should hide the given part if a higher-level part includes it in {hide: {always: ['partName']} }", () => { + const grandParent = blankPart('grandParent') + const parent = blankPart('parent', { + from: grandParent, + hide: { never: ['grandParent'] }, + }) + const main1 = blankPart('main1', { + from: parent, + hide: { from: true, always: ['grandParent'] }, + }) + + const Test = new Design({ + name: 'test', + parts: [main1], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).to.hidePart('grandParent') + }) + }) + + describe('With complex inheritance', () => { + it('Should use the strictest hiding configuration given by toplevel parts', () => { + const greatGrandParent = blankPart('greatGrandParent') + const grandParent = blankPart('grandParent', { from: greatGrandParent }) + const parent = blankPart('parent', { from: grandParent }) + const main1 = blankPart('main1', { + from: parent, + }) + const main2 = blankPart('main2', { + from: parent, + hide: { from: true, inherited: true }, + }) + + const Test = new Design({ + name: 'test', + parts: [main1, main2], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).to.hidePart('parent') + expect(pattern).to.hidePart('grandParent') + expect(pattern).to.hidePart('greatGrandParent') + }) + it('Should use inherited configurations that are not overridden', () => { + const greatGrandParent = blankPart('greatGrandParent') + const grandParent = blankPart('grandParent', { from: greatGrandParent }) + const parent = blankPart('parent', { + from: grandParent, + hide: { inherited: true }, + }) + const main1 = blankPart('main1', { + from: parent, + hide: { from: true }, + }) + + const Test = new Design({ + name: 'test', + parts: [main1], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).to.hidePart('greatGrandParent') + }) + it('Should override inherited hiding configurations', () => { + const greatGrandParent = blankPart('greatGrandParent') + const grandParent = blankPart('grandParent', { from: greatGrandParent }) + const parent = blankPart('parent', { + from: grandParent, + hide: { inherited: true }, + }) + const main1 = blankPart('main1', { + from: parent, + hide: { from: true, inherited: false }, + }) + + const Test = new Design({ + name: 'test', + parts: [main1], + }) + + const pattern = new Test() + pattern.__init() + + expect(pattern).not.to.hidePart('greatGrandParent') + }) + }) +})