From ac37f9db30d997d64de069a7b3b53c3474bdc413 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 28 Sep 2023 18:29:33 -0300 Subject: test: reorganize data tests a lil --- test/unit/data/cacheable-object.js | 270 ++++++++++++++++ .../composite/common-utilities/exposeConstant.js | 52 ++++ .../composite/common-utilities/exposeDependency.js | 74 +++++ .../withResultOfAvailabilityCheck.js | 226 ++++++++++++++ test/unit/data/composite/compositeFrom.js | 345 --------------------- test/unit/data/composite/exposeConstant.js | 52 ---- test/unit/data/composite/exposeDependency.js | 74 ----- test/unit/data/composite/templateCompositeFrom.js | 218 ------------- .../composite/withResultOfAvailabilityCheck.js | 226 -------------- test/unit/data/compositeFrom.js | 345 +++++++++++++++++++++ test/unit/data/templateCompositeFrom.js | 218 +++++++++++++ test/unit/data/things/cacheable-object.js | 270 ---------------- 12 files changed, 1185 insertions(+), 1185 deletions(-) create mode 100644 test/unit/data/cacheable-object.js create mode 100644 test/unit/data/composite/common-utilities/exposeConstant.js create mode 100644 test/unit/data/composite/common-utilities/exposeDependency.js create mode 100644 test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js delete mode 100644 test/unit/data/composite/compositeFrom.js delete mode 100644 test/unit/data/composite/exposeConstant.js delete mode 100644 test/unit/data/composite/exposeDependency.js delete mode 100644 test/unit/data/composite/templateCompositeFrom.js delete mode 100644 test/unit/data/composite/withResultOfAvailabilityCheck.js create mode 100644 test/unit/data/compositeFrom.js create mode 100644 test/unit/data/templateCompositeFrom.js delete mode 100644 test/unit/data/things/cacheable-object.js diff --git a/test/unit/data/cacheable-object.js b/test/unit/data/cacheable-object.js new file mode 100644 index 0000000..2e82af0 --- /dev/null +++ b/test/unit/data/cacheable-object.js @@ -0,0 +1,270 @@ +import t from 'tap'; + +import {CacheableObject} from '#things'; + +function newCacheableObject(PD) { + return new (class extends CacheableObject { + static propertyDescriptors = PD; + }); +} + +t.test(`CacheableObject simple separate update & expose`, t => { + const obj = newCacheableObject({ + number: { + flags: { + update: true + } + }, + + timesTwo: { + flags: { + expose: true + }, + + expose: { + dependencies: ['number'], + compute: ({ number }) => number * 2 + } + } + }); + + t.plan(1); + obj.number = 5; + t.equal(obj.timesTwo, 10); +}); + +t.test(`CacheableObject basic cache behavior`, t => { + let computeCount = 0; + + const obj = newCacheableObject({ + string: { + flags: { + update: true + } + }, + + karkat: { + flags: { + expose: true + }, + + expose: { + dependencies: ['string'], + compute: ({ string }) => { + computeCount++; + return string.toUpperCase(); + } + } + } + }); + + t.plan(8); + + t.equal(computeCount, 0); + + obj.string = 'hello world'; + t.equal(computeCount, 0); + + obj.karkat; + t.equal(computeCount, 1); + + obj.karkat; + t.equal(computeCount, 1); + + obj.string = 'testing once again'; + t.equal(computeCount, 1); + + obj.karkat; + t.equal(computeCount, 2); + + obj.string = 'testing once again'; + t.equal(computeCount, 2); + + obj.karkat; + t.equal(computeCount, 2); +}); + +t.test(`CacheableObject combined update & expose (no transform)`, t => { + const obj = newCacheableObject({ + directory: { + flags: { + update: true, + expose: true + } + } + }); + + t.plan(2); + + obj.directory = 'the-world-revolving'; + t.equal(obj.directory, 'the-world-revolving'); + + obj.directory = 'chaos-king'; + t.equal(obj.directory, 'chaos-king'); +}); + +t.test(`CacheableObject combined update & expose (basic transform)`, t => { + const obj = newCacheableObject({ + getsRepeated: { + flags: { + update: true, + expose: true + }, + + expose: { + transform: value => value.repeat(2) + } + } + }); + + t.plan(1); + + obj.getsRepeated = 'dog'; + t.equal(obj.getsRepeated, 'dogdog'); +}); + +t.test(`CacheableObject combined update & expose (transform with dependency)`, t => { + const obj = newCacheableObject({ + customRepeat: { + flags: { + update: true, + expose: true + }, + + expose: { + dependencies: ['times'], + transform: (value, { times }) => value.repeat(times) + } + }, + + times: { + flags: { + update: true + } + } + }); + + t.plan(3); + + obj.customRepeat = 'dog'; + obj.times = 1; + t.equal(obj.customRepeat, 'dog'); + + obj.times = 5; + t.equal(obj.customRepeat, 'dogdogdogdogdog'); + + obj.customRepeat = 'cat'; + t.equal(obj.customRepeat, 'catcatcatcatcat'); +}); + +t.test(`CacheableObject validate on update`, t => { + const mockError = new TypeError(`Expected a string, not ${typeof value}`); + + const obj = newCacheableObject({ + directory: { + flags: { + update: true, + expose: true + }, + + update: { + validate: value => { + if (typeof value !== 'string') { + throw mockError; + } + return true; + } + } + }, + + date: { + flags: { + update: true, + expose: true + }, + + update: { + validate: value => (value instanceof Date) + } + } + }); + + let thrownError; + t.plan(6); + + obj.directory = 'megalovania'; + t.equal(obj.directory, 'megalovania'); + + try { + obj.directory = 25; + } catch (err) { + thrownError = err; + } + + t.equal(thrownError, mockError); + t.equal(obj.directory, 'megalovania'); + + const date = new Date(`25 December 2009`); + + obj.date = date; + t.equal(obj.date, date); + + try { + obj.date = `TWELFTH PERIGEE'S EVE`; + } catch (err) { + thrownError = err; + } + + t.equal(thrownError?.constructor, TypeError); + t.equal(obj.date, date); +}); + +t.test(`CacheableObject default update property value`, t => { + const obj = newCacheableObject({ + fruit: { + flags: { + update: true, + expose: true + }, + + update: { + default: 'potassium' + } + } + }); + + t.plan(1); + t.equal(obj.fruit, 'potassium'); +}); + +t.test(`CacheableObject default property throws if invalid`, t => { + const mockError = new TypeError(`Expected a string, not ${typeof value}`); + + t.plan(1); + + let thrownError; + + try { + newCacheableObject({ + string: { + flags: { + update: true + }, + + update: { + default: 123, + validate: value => { + if (typeof value !== 'string') { + throw mockError; + } + return true; + } + } + } + }); + } catch (err) { + thrownError = err; + } + + t.equal(thrownError, mockError); +}); diff --git a/test/unit/data/composite/common-utilities/exposeConstant.js b/test/unit/data/composite/common-utilities/exposeConstant.js new file mode 100644 index 0000000..829dc70 --- /dev/null +++ b/test/unit/data/composite/common-utilities/exposeConstant.js @@ -0,0 +1,52 @@ +import t from 'tap'; + +import { + compositeFrom, + continuationSymbol, + exposeConstant, + input, +} from '#composite'; + +t.test(`exposeConstant: basic behavior`, t => { + t.plan(2); + + const composite1 = compositeFrom({ + compose: false, + + steps: [ + exposeConstant({ + value: input.value('foo'), + }), + ], + }); + + t.match(composite1, { + expose: { + dependencies: [], + }, + }); + + t.equal(composite1.expose.compute(), 'foo'); +}); + +t.test(`exposeConstant: validate inputs`, t => { + t.plan(2); + + t.throws( + () => exposeConstant({}), + { + message: `Errors in input options passed to exposeConstant`, + errors: [ + {message: `Required these inputs: value`}, + ], + }); + + t.throws( + () => exposeConstant({value: 'some dependency'}), + { + message: `Errors in input options passed to exposeConstant`, + errors: [ + {message: `value: Expected input.value() call, got dependency name`}, + ], + }); +}); diff --git a/test/unit/data/composite/common-utilities/exposeDependency.js b/test/unit/data/composite/common-utilities/exposeDependency.js new file mode 100644 index 0000000..7880134 --- /dev/null +++ b/test/unit/data/composite/common-utilities/exposeDependency.js @@ -0,0 +1,74 @@ +import t from 'tap'; + +import { + compositeFrom, + continuationSymbol, + exposeDependency, + input, +} from '#composite'; + +t.test(`exposeDependency: basic behavior`, t => { + t.plan(4); + + const composite1 = compositeFrom({ + compose: false, + + steps: [ + exposeDependency({dependency: 'foo'}), + ], + }); + + t.match(composite1, { + expose: { + dependencies: ['foo'], + }, + }); + + t.equal(composite1.expose.compute({foo: 'bar'}), 'bar'); + + const composite2 = compositeFrom({ + compose: false, + + steps: [ + { + dependencies: ['foo'], + compute: (continuation, {foo}) => + continuation({'#bar': foo.toUpperCase()}), + }, + + exposeDependency({dependency: '#bar'}), + ], + }); + + t.match(composite2, { + expose: { + dependencies: ['foo'], + }, + }); + + t.equal(composite2.expose.compute({foo: 'bar'}), 'BAR'); +}); + +t.test(`exposeDependency: validate inputs`, t => { + t.plan(2); + + t.throws( + () => exposeDependency({}), + { + message: `Errors in input options passed to exposeDependency`, + errors: [ + {message: `Required these inputs: dependency`}, + ], + }); + + t.throws( + () => exposeDependency({ + dependency: input.value('some static value'), + }), + { + message: `Errors in input options passed to exposeDependency`, + errors: [ + {message: `dependency: Expected dependency name, got input.value() call`}, + ], + }); +}); diff --git a/test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js b/test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js new file mode 100644 index 0000000..01220a3 --- /dev/null +++ b/test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js @@ -0,0 +1,226 @@ +import t from 'tap'; + +import { + compositeFrom, + continuationSymbol, + withResultOfAvailabilityCheck, + input, +} from '#composite'; + +const composite = compositeFrom({ + compose: false, + + steps: [ + withResultOfAvailabilityCheck({ + from: 'from', + mode: 'mode', + }).outputs({ + ['#availability']: '#result', + }), + + { + dependencies: ['#result'], + compute: ({'#result': result}) => result, + }, + ], +}); + +t.test(`withResultOfAvailabilityCheck: basic behavior`, t => { + t.plan(1); + + t.match(composite, { + expose: { + dependencies: ['from', 'mode'], + }, + }); +}); + +const quickCompare = (t, expect, {from, mode}) => + t.equal(composite.expose.compute({from, mode}), expect); + +const quickThrows = (t, {from, mode}) => + t.throws(() => composite.expose.compute({from, mode})); + +t.test(`withResultOfAvailabilityCheck: mode = null`, t => { + t.plan(10); + + quickCompare(t, true, {mode: 'null', from: 'truthy string'}); + quickCompare(t, true, {mode: 'null', from: 123}); + quickCompare(t, true, {mode: 'null', from: true}); + + quickCompare(t, true, {mode: 'null', from: ''}); + quickCompare(t, true, {mode: 'null', from: 0}); + quickCompare(t, true, {mode: 'null', from: false}); + + quickCompare(t, true, {mode: 'null', from: [1, 2, 3]}); + quickCompare(t, true, {mode: 'null', from: []}); + + quickCompare(t, false, {mode: 'null', from: null}); + quickCompare(t, false, {mode: 'null', from: undefined}); +}); + +t.test(`withResultOfAvailabilityCheck: mode = empty`, t => { + t.plan(10); + + quickThrows(t, {mode: 'empty', from: 'truthy string'}); + quickThrows(t, {mode: 'empty', from: 123}); + quickThrows(t, {mode: 'empty', from: true}); + + quickThrows(t, {mode: 'empty', from: ''}); + quickThrows(t, {mode: 'empty', from: 0}); + quickThrows(t, {mode: 'empty', from: false}); + + quickCompare(t, true, {mode: 'empty', from: [1, 2, 3]}); + quickCompare(t, false, {mode: 'empty', from: []}); + + quickCompare(t, false, {mode: 'empty', from: null}); + quickCompare(t, false, {mode: 'empty', from: undefined}); +}); + +t.test(`withResultOfAvailabilityCheck: mode = falsy`, t => { + t.plan(10); + + quickCompare(t, true, {mode: 'falsy', from: 'truthy string'}); + quickCompare(t, true, {mode: 'falsy', from: 123}); + quickCompare(t, true, {mode: 'falsy', from: true}); + + quickCompare(t, false, {mode: 'falsy', from: ''}); + quickCompare(t, false, {mode: 'falsy', from: 0}); + quickCompare(t, false, {mode: 'falsy', from: false}); + + quickCompare(t, true, {mode: 'falsy', from: [1, 2, 3]}); + quickCompare(t, false, {mode: 'falsy', from: []}); + + quickCompare(t, false, {mode: 'falsy', from: null}); + quickCompare(t, false, {mode: 'falsy', from: undefined}); +}); + +t.test(`withResultOfAvailabilityCheck: default mode`, t => { + t.plan(1); + + const template = withResultOfAvailabilityCheck({ + from: 'foo', + }); + + t.match(template.toDescription(), { + inputMapping: { + from: input.dependency('foo'), + mode: input.value('null'), + }, + }); +}); + +t.test(`withResultOfAvailabilityCheck: validate static inputs`, t => { + t.plan(5); + + let caughtError; + + try { + caughtError = null; + withResultOfAvailabilityCheck({}); + } catch (error) { + caughtError = error; + } + + t.match(caughtError, { + errors: [/Required these inputs: from/], + }); + + t.doesNotThrow(() => + withResultOfAvailabilityCheck({ + from: 'dependency1', + mode: 'dependency2', + })); + + t.doesNotThrow(() => + withResultOfAvailabilityCheck({ + from: input.value('some static value'), + mode: input.value('null'), + })); + + try { + caughtError = null; + withResultOfAvailabilityCheck({ + from: 'foo', + mode: input.value('invalid'), + }); + } catch (error) { + caughtError = error; + } + + t.match(caughtError, { + message: /Errors in input options passed to withResultOfAvailabilityCheck/, + errors: [ + /mode: Expected one of null empty falsy, got invalid/, + ], + }); + + try { + caughtError = null; + withResultOfAvailabilityCheck({ + from: input.value(null), + mode: input.value(null), + }); + } catch (error) { + caughtError = error; + } + + t.match(caughtError, { + message: /Errors in input options passed to withResultOfAvailabilityCheck/, + errors: [ + /mode: Expected value, got null/, + ], + }); +}); + +t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => { + t.plan(2); + + let caughtError; + + try { + caughtError = null; + composite.expose.compute({ + from: 'apple', + mode: 'banana', + }); + } catch (error) { + caughtError = error; + } + + t.match(caughtError, { + message: /Error computing composition/, + cause: { + message: /Error computing composition withResultOfAvailabilityCheck/, + cause: { + message: /Errors in input values provided to withResultOfAvailabilityCheck/, + errors: [ + /mode: Expected one of null empty falsy, got banana/, + ], + }, + }, + }); + + try { + caughtError = null; + composite.expose.compute({ + from: null, + mode: null, + }); + } catch (error) { + caughtError = error; + } + + t.match(caughtError, { + message: /Error computing composition/, + cause: { + message: /Error computing composition withResultOfAvailabilityCheck/, + cause: { + message: /Errors in input values provided to withResultOfAvailabilityCheck/, + errors: [ + /mode: Expected value, got null/, + ], + }, + }, + }); +}); diff --git a/test/unit/data/composite/compositeFrom.js b/test/unit/data/composite/compositeFrom.js deleted file mode 100644 index 0029667..0000000 --- a/test/unit/data/composite/compositeFrom.js +++ /dev/null @@ -1,345 +0,0 @@ -import t from 'tap'; - -import {compositeFrom, continuationSymbol, input} from '#composite'; -import {isString} from '#validators'; - -t.test(`compositeFrom: basic behavior`, t => { - t.plan(2); - - const composite = compositeFrom({ - annotation: `myComposite`, - compose: false, - - steps: [ - { - dependencies: ['foo'], - compute: (continuation, {foo}) => - continuation({'#bar': foo * 2}), - }, - - { - dependencies: ['#bar', 'baz', 'suffix'], - compute: ({'#bar': bar, baz, suffix}) => - baz.repeat(bar) + suffix, - }, - ], - }); - - t.match(composite, { - annotation: `myComposite`, - - flags: {expose: true, compose: false, update: false}, - - expose: { - dependencies: ['foo', 'baz', 'suffix'], - compute: Function, - transform: null, - }, - - update: null, - }); - - t.equal( - composite.expose.compute({ - foo: 3, - baz: 'ba', - suffix: 'BOOM', - }), - 'babababababaBOOM'); -}); - -t.test(`compositeFrom: input-shaped step dependencies`, t => { - t.plan(2); - - const composite = compositeFrom({ - compose: false, - steps: [ - { - dependencies: [ - input.myself(), - input.updateValue(), - ], - - transform: (updateValue1, { - [input.myself()]: me, - [input.updateValue()]: updateValue2, - }) => ({me, updateValue1, updateValue2}), - }, - ], - }); - - t.match(composite, { - expose: { - dependencies: ['this'], - transform: Function, - compute: null, - }, - }); - - const myself = {foo: 'bar'}; - - t.same( - composite.expose.transform('banana', { - this: myself, - pomelo: 'delicious', - }), - { - me: myself, - updateValue1: 'banana', - updateValue2: 'banana', - }); -}); - -t.test(`compositeFrom: dependencies from inputs`, t => { - t.plan(3); - - const composite = compositeFrom({ - annotation: `myComposite`, - - compose: true, - - inputMapping: { - foo: input('bar'), - pomelo: input.value('delicious'), - humorous: input.dependency('#mammal'), - data: input.dependency('albumData'), - ref: input.updateValue(), - }, - - inputDescriptions: { - foo: input(), - pomelo: input(), - humorous: input(), - data: input(), - ref: input(), - }, - - steps: [ - { - dependencies: [ - input('foo'), - input('pomelo'), - input('humorous'), - input('data'), - input('ref'), - ], - - compute: (continuation, { - [input('foo')]: foo, - [input('pomelo')]: pomelo, - [input('humorous')]: humorous, - [input('data')]: data, - [input('ref')]: ref, - }) => continuation.exit({foo, pomelo, humorous, data, ref}), - }, - ], - }); - - t.match(composite, { - expose: { - dependencies: [ - input('bar'), - '#mammal', - 'albumData', - ], - - transform: Function, - compute: null, - }, - }); - - const exitData = {}; - const continuation = { - exit(value) { - Object.assign(exitData, value); - return continuationSymbol; - }, - }; - - t.equal( - composite.expose.transform('album:bepis', continuation, { - [input('bar')]: 'squid time', - '#mammal': 'fox', - 'albumData': ['album1', 'album2'], - }), - continuationSymbol); - - t.same(exitData, { - foo: 'squid time', - pomelo: 'delicious', - humorous: 'fox', - data: ['album1', 'album2'], - ref: 'album:bepis', - }); -}); - -t.test(`compositeFrom: update from various sources`, t => { - t.plan(3); - - const match = { - flags: {update: true, expose: true, compose: false}, - - update: { - validate: isString, - default: 'foo', - }, - - expose: { - transform: Function, - compute: null, - }, - }; - - t.test(`compositeFrom: update from composition description`, t => { - t.plan(2); - - const composite = compositeFrom({ - compose: false, - - update: { - validate: isString, - default: 'foo', - }, - - steps: [ - {transform: (value, continuation) => continuation(value.repeat(2))}, - {transform: (value) => `Xx_${value}_xX`}, - ], - }); - - t.match(composite, match); - t.equal(composite.expose.transform('foo'), `Xx_foofoo_xX`); - }); - - t.test(`compositeFrom: update from step dependencies`, t => { - t.plan(2); - - const composite = compositeFrom({ - compose: false, - - steps: [ - { - dependencies: [ - input.updateValue({ - validate: isString, - default: 'foo', - }), - ], - - compute: ({ - [input.updateValue()]: value, - }) => `Xx_${value.repeat(2)}_xX`, - }, - ], - }); - - t.match(composite, match); - t.equal(composite.expose.transform('foo'), 'Xx_foofoo_xX'); - }); - - t.test(`compositeFrom: update from inputs`, t => { - t.plan(3); - - const composite = compositeFrom({ - inputMapping: { - myInput: input.updateValue({ - validate: isString, - default: 'foo', - }), - }, - - inputDescriptions: { - myInput: input(), - }, - - steps: [ - { - dependencies: [input('myInput')], - compute: (continuation, { - [input('myInput')]: value, - }) => continuation({ - '#value': `Xx_${value.repeat(2)}_xX`, - }), - }, - - { - dependencies: ['#value'], - transform: (_value, continuation, {'#value': value}) => - continuation(value), - }, - ], - }); - - let continuationValue = null; - const continuation = value => { - continuationValue = value; - return continuationSymbol; - }; - - t.match(composite, { - ...match, - - flags: {update: true, expose: true, compose: true}, - }); - - t.equal( - composite.expose.transform('foo', continuation), - continuationSymbol); - - t.equal(continuationValue, 'Xx_foofoo_xX'); - }); -}); - -t.test(`compositeFrom: dynamic input validation from type`, t => { - t.plan(2); - - const composite = compositeFrom({ - inputMapping: { - string: input('string'), - number: input('number'), - boolean: input('boolean'), - function: input('function'), - object: input('object'), - array: input('array'), - }, - - inputDescriptions: { - string: input({null: true, type: 'string'}), - number: input({null: true, type: 'number'}), - boolean: input({null: true, type: 'boolean'}), - function: input({null: true, type: 'function'}), - object: input({null: true, type: 'object'}), - array: input({null: true, type: 'array'}), - }, - - outputs: {'#result': '#result'}, - - steps: [ - {compute: continuation => continuation({'#result': 'OK'})}, - ], - }); - - const notCalledSymbol = Symbol('continuation not called'); - - let continuationValue; - const continuation = value => { - continuationValue = value; - return continuationSymbol; - }; - - let thrownError; - - try { - continuationValue = notCalledSymbol; - thrownError = null; - composite.expose.compute(continuation, { - [input('string')]: 123, - }); - } catch (error) { - thrownError = error; - } - - t.equal(continuationValue, notCalledSymbol); - t.match(thrownError, { - }); -}); diff --git a/test/unit/data/composite/exposeConstant.js b/test/unit/data/composite/exposeConstant.js deleted file mode 100644 index 829dc70..0000000 --- a/test/unit/data/composite/exposeConstant.js +++ /dev/null @@ -1,52 +0,0 @@ -import t from 'tap'; - -import { - compositeFrom, - continuationSymbol, - exposeConstant, - input, -} from '#composite'; - -t.test(`exposeConstant: basic behavior`, t => { - t.plan(2); - - const composite1 = compositeFrom({ - compose: false, - - steps: [ - exposeConstant({ - value: input.value('foo'), - }), - ], - }); - - t.match(composite1, { - expose: { - dependencies: [], - }, - }); - - t.equal(composite1.expose.compute(), 'foo'); -}); - -t.test(`exposeConstant: validate inputs`, t => { - t.plan(2); - - t.throws( - () => exposeConstant({}), - { - message: `Errors in input options passed to exposeConstant`, - errors: [ - {message: `Required these inputs: value`}, - ], - }); - - t.throws( - () => exposeConstant({value: 'some dependency'}), - { - message: `Errors in input options passed to exposeConstant`, - errors: [ - {message: `value: Expected input.value() call, got dependency name`}, - ], - }); -}); diff --git a/test/unit/data/composite/exposeDependency.js b/test/unit/data/composite/exposeDependency.js deleted file mode 100644 index 7880134..0000000 --- a/test/unit/data/composite/exposeDependency.js +++ /dev/null @@ -1,74 +0,0 @@ -import t from 'tap'; - -import { - compositeFrom, - continuationSymbol, - exposeDependency, - input, -} from '#composite'; - -t.test(`exposeDependency: basic behavior`, t => { - t.plan(4); - - const composite1 = compositeFrom({ - compose: false, - - steps: [ - exposeDependency({dependency: 'foo'}), - ], - }); - - t.match(composite1, { - expose: { - dependencies: ['foo'], - }, - }); - - t.equal(composite1.expose.compute({foo: 'bar'}), 'bar'); - - const composite2 = compositeFrom({ - compose: false, - - steps: [ - { - dependencies: ['foo'], - compute: (continuation, {foo}) => - continuation({'#bar': foo.toUpperCase()}), - }, - - exposeDependency({dependency: '#bar'}), - ], - }); - - t.match(composite2, { - expose: { - dependencies: ['foo'], - }, - }); - - t.equal(composite2.expose.compute({foo: 'bar'}), 'BAR'); -}); - -t.test(`exposeDependency: validate inputs`, t => { - t.plan(2); - - t.throws( - () => exposeDependency({}), - { - message: `Errors in input options passed to exposeDependency`, - errors: [ - {message: `Required these inputs: dependency`}, - ], - }); - - t.throws( - () => exposeDependency({ - dependency: input.value('some static value'), - }), - { - message: `Errors in input options passed to exposeDependency`, - errors: [ - {message: `dependency: Expected dependency name, got input.value() call`}, - ], - }); -}); diff --git a/test/unit/data/composite/templateCompositeFrom.js b/test/unit/data/composite/templateCompositeFrom.js deleted file mode 100644 index e96b782..0000000 --- a/test/unit/data/composite/templateCompositeFrom.js +++ /dev/null @@ -1,218 +0,0 @@ -import t from 'tap'; - -import {isString} from '#validators'; - -import { - compositeFrom, - continuationSymbol, - input, - templateCompositeFrom, -} from '#composite'; - -t.test(`templateCompositeFrom: basic behavior`, t => { - t.plan(1); - - const myCoolUtility = templateCompositeFrom({ - annotation: `myCoolUtility`, - - inputs: { - foo: input(), - }, - - outputs: ['#bar'], - - steps: () => [ - { - dependencies: [input('foo')], - compute: (continuation, { - [input('foo')]: foo, - }) => continuation({ - ['#bar']: (typeof foo).toUpperCase() - }), - }, - ], - }); - - const instantiatedTemplate = myCoolUtility({ - foo: 'color', - }); - - t.match(instantiatedTemplate.toDescription(), { - annotation: `myCoolUtility`, - - inputMapping: { - foo: input.dependency('color'), - }, - - inputDescriptions: { - foo: input(), - }, - - outputs: { - '#bar': '#bar', - }, - - steps: Function, - }); -}); - -t.test(`templateCompositeFrom: validate static input values`, t => { - t.plan(3); - - const stub = { - annotation: 'stubComposite', - outputs: ['#result'], - steps: () => [{compute: continuation => continuation({'#result': 'OK'})}], - }; - - const quickThrows = (t, composite, inputOptions, ...errorMessages) => - t.throws( - () => composite(inputOptions), - { - message: `Errors in input options passed to stubComposite`, - errors: errorMessages.map(message => ({message})), - }); - - t.test(`templateCompositeFrom: validate input token shapes`, t => { - t.plan(15); - - const template1 = templateCompositeFrom({ - ...stub, inputs: { - foo: input(), - }, - }); - - t.doesNotThrow( - () => template1({foo: 'dependency'})); - - t.doesNotThrow( - () => template1({foo: input.dependency('dependency')})); - - t.doesNotThrow( - () => template1({foo: input.value('static value')})); - - t.doesNotThrow( - () => template1({foo: input('outerInput')})); - - t.doesNotThrow( - () => template1({foo: input.updateValue()})); - - t.doesNotThrow( - () => template1({foo: input.myself()})); - - quickThrows(t, template1, - {foo: input.staticValue()}, - `foo: Expected dependency name or value-providing input() call, got input.staticValue`); - - quickThrows(t, template1, - {foo: input.staticDependency()}, - `foo: Expected dependency name or value-providing input() call, got input.staticDependency`); - - const template2 = templateCompositeFrom({ - ...stub, inputs: { - bar: input.staticDependency(), - }, - }); - - t.doesNotThrow( - () => template2({bar: 'dependency'})); - - t.doesNotThrow( - () => template2({bar: input.dependency('dependency')})); - - quickThrows(t, template2, - {bar: input.value(123)}, - `bar: Expected dependency name, got input.value`); - - quickThrows(t, template2, - {bar: input('outOfPlace')}, - `bar: Expected dependency name, got input`); - - const template3 = templateCompositeFrom({ - ...stub, inputs: { - baz: input.staticValue(), - }, - }); - - t.doesNotThrow( - () => template3({baz: input.value(1025)})); - - quickThrows(t, template3, - {baz: 'dependency'}, - `baz: Expected input.value() call, got dependency name`); - - quickThrows(t, template3, - {baz: input('outOfPlace')}, - `baz: Expected input.value() call, got input() call`); - }); - - t.test(`templateCompositeFrom: validate missing / misplaced inputs`, t => { - t.plan(1); - - const template = templateCompositeFrom({ - ...stub, inputs: { - foo: input(), - bar: input(), - }, - }); - - t.throws( - () => template({ - baz: 'aeiou', - raz: input.value(123), - }), - { - message: `Errors in input options passed to stubComposite`, - errors: [ - {message: `Unexpected input names: baz, raz`}, - {message: `Required these inputs: foo, bar`}, - ], - }); - }); - - t.test(`templateCompositeFrom: validate acceptsNull / defaultValue: null`, t => { - t.plan(3); - - const template1 = templateCompositeFrom({ - ...stub, inputs: { - foo: input(), - }, - }); - - t.throws( - () => template1({}), - { - message: `Errors in input options passed to stubComposite`, - errors: [ - {message: `Required these inputs: foo`}, - ], - }, - `throws if input missing and not marked specially`); - - const template2 = templateCompositeFrom({ - ...stub, inputs: { - bar: input({acceptsNull: true}), - }, - }); - - t.throws( - () => template2({}), - { - message: `Errors in input options passed to stubComposite`, - errors: [ - {message: `Required these inputs: bar`}, - ], - }, - `throws if input missing even if marked {acceptsNull}`); - - const template3 = templateCompositeFrom({ - ...stub, inputs: { - baz: input({defaultValue: null}), - }, - }); - - t.doesNotThrow( - () => template3({}), - `does not throw if input missing if marked {defaultValue: null}`); - }); -}); diff --git a/test/unit/data/composite/withResultOfAvailabilityCheck.js b/test/unit/data/composite/withResultOfAvailabilityCheck.js deleted file mode 100644 index 01220a3..0000000 --- a/test/unit/data/composite/withResultOfAvailabilityCheck.js +++ /dev/null @@ -1,226 +0,0 @@ -import t from 'tap'; - -import { - compositeFrom, - continuationSymbol, - withResultOfAvailabilityCheck, - input, -} from '#composite'; - -const composite = compositeFrom({ - compose: false, - - steps: [ - withResultOfAvailabilityCheck({ - from: 'from', - mode: 'mode', - }).outputs({ - ['#availability']: '#result', - }), - - { - dependencies: ['#result'], - compute: ({'#result': result}) => result, - }, - ], -}); - -t.test(`withResultOfAvailabilityCheck: basic behavior`, t => { - t.plan(1); - - t.match(composite, { - expose: { - dependencies: ['from', 'mode'], - }, - }); -}); - -const quickCompare = (t, expect, {from, mode}) => - t.equal(composite.expose.compute({from, mode}), expect); - -const quickThrows = (t, {from, mode}) => - t.throws(() => composite.expose.compute({from, mode})); - -t.test(`withResultOfAvailabilityCheck: mode = null`, t => { - t.plan(10); - - quickCompare(t, true, {mode: 'null', from: 'truthy string'}); - quickCompare(t, true, {mode: 'null', from: 123}); - quickCompare(t, true, {mode: 'null', from: true}); - - quickCompare(t, true, {mode: 'null', from: ''}); - quickCompare(t, true, {mode: 'null', from: 0}); - quickCompare(t, true, {mode: 'null', from: false}); - - quickCompare(t, true, {mode: 'null', from: [1, 2, 3]}); - quickCompare(t, true, {mode: 'null', from: []}); - - quickCompare(t, false, {mode: 'null', from: null}); - quickCompare(t, false, {mode: 'null', from: undefined}); -}); - -t.test(`withResultOfAvailabilityCheck: mode = empty`, t => { - t.plan(10); - - quickThrows(t, {mode: 'empty', from: 'truthy string'}); - quickThrows(t, {mode: 'empty', from: 123}); - quickThrows(t, {mode: 'empty', from: true}); - - quickThrows(t, {mode: 'empty', from: ''}); - quickThrows(t, {mode: 'empty', from: 0}); - quickThrows(t, {mode: 'empty', from: false}); - - quickCompare(t, true, {mode: 'empty', from: [1, 2, 3]}); - quickCompare(t, false, {mode: 'empty', from: []}); - - quickCompare(t, false, {mode: 'empty', from: null}); - quickCompare(t, false, {mode: 'empty', from: undefined}); -}); - -t.test(`withResultOfAvailabilityCheck: mode = falsy`, t => { - t.plan(10); - - quickCompare(t, true, {mode: 'falsy', from: 'truthy string'}); - quickCompare(t, true, {mode: 'falsy', from: 123}); - quickCompare(t, true, {mode: 'falsy', from: true}); - - quickCompare(t, false, {mode: 'falsy', from: ''}); - quickCompare(t, false, {mode: 'falsy', from: 0}); - quickCompare(t, false, {mode: 'falsy', from: false}); - - quickCompare(t, true, {mode: 'falsy', from: [1, 2, 3]}); - quickCompare(t, false, {mode: 'falsy', from: []}); - - quickCompare(t, false, {mode: 'falsy', from: null}); - quickCompare(t, false, {mode: 'falsy', from: undefined}); -}); - -t.test(`withResultOfAvailabilityCheck: default mode`, t => { - t.plan(1); - - const template = withResultOfAvailabilityCheck({ - from: 'foo', - }); - - t.match(template.toDescription(), { - inputMapping: { - from: input.dependency('foo'), - mode: input.value('null'), - }, - }); -}); - -t.test(`withResultOfAvailabilityCheck: validate static inputs`, t => { - t.plan(5); - - let caughtError; - - try { - caughtError = null; - withResultOfAvailabilityCheck({}); - } catch (error) { - caughtError = error; - } - - t.match(caughtError, { - errors: [/Required these inputs: from/], - }); - - t.doesNotThrow(() => - withResultOfAvailabilityCheck({ - from: 'dependency1', - mode: 'dependency2', - })); - - t.doesNotThrow(() => - withResultOfAvailabilityCheck({ - from: input.value('some static value'), - mode: input.value('null'), - })); - - try { - caughtError = null; - withResultOfAvailabilityCheck({ - from: 'foo', - mode: input.value('invalid'), - }); - } catch (error) { - caughtError = error; - } - - t.match(caughtError, { - message: /Errors in input options passed to withResultOfAvailabilityCheck/, - errors: [ - /mode: Expected one of null empty falsy, got invalid/, - ], - }); - - try { - caughtError = null; - withResultOfAvailabilityCheck({ - from: input.value(null), - mode: input.value(null), - }); - } catch (error) { - caughtError = error; - } - - t.match(caughtError, { - message: /Errors in input options passed to withResultOfAvailabilityCheck/, - errors: [ - /mode: Expected value, got null/, - ], - }); -}); - -t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => { - t.plan(2); - - let caughtError; - - try { - caughtError = null; - composite.expose.compute({ - from: 'apple', - mode: 'banana', - }); - } catch (error) { - caughtError = error; - } - - t.match(caughtError, { - message: /Error computing composition/, - cause: { - message: /Error computing composition withResultOfAvailabilityCheck/, - cause: { - message: /Errors in input values provided to withResultOfAvailabilityCheck/, - errors: [ - /mode: Expected one of null empty falsy, got banana/, - ], - }, - }, - }); - - try { - caughtError = null; - composite.expose.compute({ - from: null, - mode: null, - }); - } catch (error) { - caughtError = error; - } - - t.match(caughtError, { - message: /Error computing composition/, - cause: { - message: /Error computing composition withResultOfAvailabilityCheck/, - cause: { - message: /Errors in input values provided to withResultOfAvailabilityCheck/, - errors: [ - /mode: Expected value, got null/, - ], - }, - }, - }); -}); diff --git a/test/unit/data/compositeFrom.js b/test/unit/data/compositeFrom.js new file mode 100644 index 0000000..0029667 --- /dev/null +++ b/test/unit/data/compositeFrom.js @@ -0,0 +1,345 @@ +import t from 'tap'; + +import {compositeFrom, continuationSymbol, input} from '#composite'; +import {isString} from '#validators'; + +t.test(`compositeFrom: basic behavior`, t => { + t.plan(2); + + const composite = compositeFrom({ + annotation: `myComposite`, + compose: false, + + steps: [ + { + dependencies: ['foo'], + compute: (continuation, {foo}) => + continuation({'#bar': foo * 2}), + }, + + { + dependencies: ['#bar', 'baz', 'suffix'], + compute: ({'#bar': bar, baz, suffix}) => + baz.repeat(bar) + suffix, + }, + ], + }); + + t.match(composite, { + annotation: `myComposite`, + + flags: {expose: true, compose: false, update: false}, + + expose: { + dependencies: ['foo', 'baz', 'suffix'], + compute: Function, + transform: null, + }, + + update: null, + }); + + t.equal( + composite.expose.compute({ + foo: 3, + baz: 'ba', + suffix: 'BOOM', + }), + 'babababababaBOOM'); +}); + +t.test(`compositeFrom: input-shaped step dependencies`, t => { + t.plan(2); + + const composite = compositeFrom({ + compose: false, + steps: [ + { + dependencies: [ + input.myself(), + input.updateValue(), + ], + + transform: (updateValue1, { + [input.myself()]: me, + [input.updateValue()]: updateValue2, + }) => ({me, updateValue1, updateValue2}), + }, + ], + }); + + t.match(composite, { + expose: { + dependencies: ['this'], + transform: Function, + compute: null, + }, + }); + + const myself = {foo: 'bar'}; + + t.same( + composite.expose.transform('banana', { + this: myself, + pomelo: 'delicious', + }), + { + me: myself, + updateValue1: 'banana', + updateValue2: 'banana', + }); +}); + +t.test(`compositeFrom: dependencies from inputs`, t => { + t.plan(3); + + const composite = compositeFrom({ + annotation: `myComposite`, + + compose: true, + + inputMapping: { + foo: input('bar'), + pomelo: input.value('delicious'), + humorous: input.dependency('#mammal'), + data: input.dependency('albumData'), + ref: input.updateValue(), + }, + + inputDescriptions: { + foo: input(), + pomelo: input(), + humorous: input(), + data: input(), + ref: input(), + }, + + steps: [ + { + dependencies: [ + input('foo'), + input('pomelo'), + input('humorous'), + input('data'), + input('ref'), + ], + + compute: (continuation, { + [input('foo')]: foo, + [input('pomelo')]: pomelo, + [input('humorous')]: humorous, + [input('data')]: data, + [input('ref')]: ref, + }) => continuation.exit({foo, pomelo, humorous, data, ref}), + }, + ], + }); + + t.match(composite, { + expose: { + dependencies: [ + input('bar'), + '#mammal', + 'albumData', + ], + + transform: Function, + compute: null, + }, + }); + + const exitData = {}; + const continuation = { + exit(value) { + Object.assign(exitData, value); + return continuationSymbol; + }, + }; + + t.equal( + composite.expose.transform('album:bepis', continuation, { + [input('bar')]: 'squid time', + '#mammal': 'fox', + 'albumData': ['album1', 'album2'], + }), + continuationSymbol); + + t.same(exitData, { + foo: 'squid time', + pomelo: 'delicious', + humorous: 'fox', + data: ['album1', 'album2'], + ref: 'album:bepis', + }); +}); + +t.test(`compositeFrom: update from various sources`, t => { + t.plan(3); + + const match = { + flags: {update: true, expose: true, compose: false}, + + update: { + validate: isString, + default: 'foo', + }, + + expose: { + transform: Function, + compute: null, + }, + }; + + t.test(`compositeFrom: update from composition description`, t => { + t.plan(2); + + const composite = compositeFrom({ + compose: false, + + update: { + validate: isString, + default: 'foo', + }, + + steps: [ + {transform: (value, continuation) => continuation(value.repeat(2))}, + {transform: (value) => `Xx_${value}_xX`}, + ], + }); + + t.match(composite, match); + t.equal(composite.expose.transform('foo'), `Xx_foofoo_xX`); + }); + + t.test(`compositeFrom: update from step dependencies`, t => { + t.plan(2); + + const composite = compositeFrom({ + compose: false, + + steps: [ + { + dependencies: [ + input.updateValue({ + validate: isString, + default: 'foo', + }), + ], + + compute: ({ + [input.updateValue()]: value, + }) => `Xx_${value.repeat(2)}_xX`, + }, + ], + }); + + t.match(composite, match); + t.equal(composite.expose.transform('foo'), 'Xx_foofoo_xX'); + }); + + t.test(`compositeFrom: update from inputs`, t => { + t.plan(3); + + const composite = compositeFrom({ + inputMapping: { + myInput: input.updateValue({ + validate: isString, + default: 'foo', + }), + }, + + inputDescriptions: { + myInput: input(), + }, + + steps: [ + { + dependencies: [input('myInput')], + compute: (continuation, { + [input('myInput')]: value, + }) => continuation({ + '#value': `Xx_${value.repeat(2)}_xX`, + }), + }, + + { + dependencies: ['#value'], + transform: (_value, continuation, {'#value': value}) => + continuation(value), + }, + ], + }); + + let continuationValue = null; + const continuation = value => { + continuationValue = value; + return continuationSymbol; + }; + + t.match(composite, { + ...match, + + flags: {update: true, expose: true, compose: true}, + }); + + t.equal( + composite.expose.transform('foo', continuation), + continuationSymbol); + + t.equal(continuationValue, 'Xx_foofoo_xX'); + }); +}); + +t.test(`compositeFrom: dynamic input validation from type`, t => { + t.plan(2); + + const composite = compositeFrom({ + inputMapping: { + string: input('string'), + number: input('number'), + boolean: input('boolean'), + function: input('function'), + object: input('object'), + array: input('array'), + }, + + inputDescriptions: { + string: input({null: true, type: 'string'}), + number: input({null: true, type: 'number'}), + boolean: input({null: true, type: 'boolean'}), + function: input({null: true, type: 'function'}), + object: input({null: true, type: 'object'}), + array: input({null: true, type: 'array'}), + }, + + outputs: {'#result': '#result'}, + + steps: [ + {compute: continuation => continuation({'#result': 'OK'})}, + ], + }); + + const notCalledSymbol = Symbol('continuation not called'); + + let continuationValue; + const continuation = value => { + continuationValue = value; + return continuationSymbol; + }; + + let thrownError; + + try { + continuationValue = notCalledSymbol; + thrownError = null; + composite.expose.compute(continuation, { + [input('string')]: 123, + }); + } catch (error) { + thrownError = error; + } + + t.equal(continuationValue, notCalledSymbol); + t.match(thrownError, { + }); +}); diff --git a/test/unit/data/templateCompositeFrom.js b/test/unit/data/templateCompositeFrom.js new file mode 100644 index 0000000..e96b782 --- /dev/null +++ b/test/unit/data/templateCompositeFrom.js @@ -0,0 +1,218 @@ +import t from 'tap'; + +import {isString} from '#validators'; + +import { + compositeFrom, + continuationSymbol, + input, + templateCompositeFrom, +} from '#composite'; + +t.test(`templateCompositeFrom: basic behavior`, t => { + t.plan(1); + + const myCoolUtility = templateCompositeFrom({ + annotation: `myCoolUtility`, + + inputs: { + foo: input(), + }, + + outputs: ['#bar'], + + steps: () => [ + { + dependencies: [input('foo')], + compute: (continuation, { + [input('foo')]: foo, + }) => continuation({ + ['#bar']: (typeof foo).toUpperCase() + }), + }, + ], + }); + + const instantiatedTemplate = myCoolUtility({ + foo: 'color', + }); + + t.match(instantiatedTemplate.toDescription(), { + annotation: `myCoolUtility`, + + inputMapping: { + foo: input.dependency('color'), + }, + + inputDescriptions: { + foo: input(), + }, + + outputs: { + '#bar': '#bar', + }, + + steps: Function, + }); +}); + +t.test(`templateCompositeFrom: validate static input values`, t => { + t.plan(3); + + const stub = { + annotation: 'stubComposite', + outputs: ['#result'], + steps: () => [{compute: continuation => continuation({'#result': 'OK'})}], + }; + + const quickThrows = (t, composite, inputOptions, ...errorMessages) => + t.throws( + () => composite(inputOptions), + { + message: `Errors in input options passed to stubComposite`, + errors: errorMessages.map(message => ({message})), + }); + + t.test(`templateCompositeFrom: validate input token shapes`, t => { + t.plan(15); + + const template1 = templateCompositeFrom({ + ...stub, inputs: { + foo: input(), + }, + }); + + t.doesNotThrow( + () => template1({foo: 'dependency'})); + + t.doesNotThrow( + () => template1({foo: input.dependency('dependency')})); + + t.doesNotThrow( + () => template1({foo: input.value('static value')})); + + t.doesNotThrow( + () => template1({foo: input('outerInput')})); + + t.doesNotThrow( + () => template1({foo: input.updateValue()})); + + t.doesNotThrow( + () => template1({foo: input.myself()})); + + quickThrows(t, template1, + {foo: input.staticValue()}, + `foo: Expected dependency name or value-providing input() call, got input.staticValue`); + + quickThrows(t, template1, + {foo: input.staticDependency()}, + `foo: Expected dependency name or value-providing input() call, got input.staticDependency`); + + const template2 = templateCompositeFrom({ + ...stub, inputs: { + bar: input.staticDependency(), + }, + }); + + t.doesNotThrow( + () => template2({bar: 'dependency'})); + + t.doesNotThrow( + () => template2({bar: input.dependency('dependency')})); + + quickThrows(t, template2, + {bar: input.value(123)}, + `bar: Expected dependency name, got input.value`); + + quickThrows(t, template2, + {bar: input('outOfPlace')}, + `bar: Expected dependency name, got input`); + + const template3 = templateCompositeFrom({ + ...stub, inputs: { + baz: input.staticValue(), + }, + }); + + t.doesNotThrow( + () => template3({baz: input.value(1025)})); + + quickThrows(t, template3, + {baz: 'dependency'}, + `baz: Expected input.value() call, got dependency name`); + + quickThrows(t, template3, + {baz: input('outOfPlace')}, + `baz: Expected input.value() call, got input() call`); + }); + + t.test(`templateCompositeFrom: validate missing / misplaced inputs`, t => { + t.plan(1); + + const template = templateCompositeFrom({ + ...stub, inputs: { + foo: input(), + bar: input(), + }, + }); + + t.throws( + () => template({ + baz: 'aeiou', + raz: input.value(123), + }), + { + message: `Errors in input options passed to stubComposite`, + errors: [ + {message: `Unexpected input names: baz, raz`}, + {message: `Required these inputs: foo, bar`}, + ], + }); + }); + + t.test(`templateCompositeFrom: validate acceptsNull / defaultValue: null`, t => { + t.plan(3); + + const template1 = templateCompositeFrom({ + ...stub, inputs: { + foo: input(), + }, + }); + + t.throws( + () => template1({}), + { + message: `Errors in input options passed to stubComposite`, + errors: [ + {message: `Required these inputs: foo`}, + ], + }, + `throws if input missing and not marked specially`); + + const template2 = templateCompositeFrom({ + ...stub, inputs: { + bar: input({acceptsNull: true}), + }, + }); + + t.throws( + () => template2({}), + { + message: `Errors in input options passed to stubComposite`, + errors: [ + {message: `Required these inputs: bar`}, + ], + }, + `throws if input missing even if marked {acceptsNull}`); + + const template3 = templateCompositeFrom({ + ...stub, inputs: { + baz: input({defaultValue: null}), + }, + }); + + t.doesNotThrow( + () => template3({}), + `does not throw if input missing if marked {defaultValue: null}`); + }); +}); diff --git a/test/unit/data/things/cacheable-object.js b/test/unit/data/things/cacheable-object.js deleted file mode 100644 index 2e82af0..0000000 --- a/test/unit/data/things/cacheable-object.js +++ /dev/null @@ -1,270 +0,0 @@ -import t from 'tap'; - -import {CacheableObject} from '#things'; - -function newCacheableObject(PD) { - return new (class extends CacheableObject { - static propertyDescriptors = PD; - }); -} - -t.test(`CacheableObject simple separate update & expose`, t => { - const obj = newCacheableObject({ - number: { - flags: { - update: true - } - }, - - timesTwo: { - flags: { - expose: true - }, - - expose: { - dependencies: ['number'], - compute: ({ number }) => number * 2 - } - } - }); - - t.plan(1); - obj.number = 5; - t.equal(obj.timesTwo, 10); -}); - -t.test(`CacheableObject basic cache behavior`, t => { - let computeCount = 0; - - const obj = newCacheableObject({ - string: { - flags: { - update: true - } - }, - - karkat: { - flags: { - expose: true - }, - - expose: { - dependencies: ['string'], - compute: ({ string }) => { - computeCount++; - return string.toUpperCase(); - } - } - } - }); - - t.plan(8); - - t.equal(computeCount, 0); - - obj.string = 'hello world'; - t.equal(computeCount, 0); - - obj.karkat; - t.equal(computeCount, 1); - - obj.karkat; - t.equal(computeCount, 1); - - obj.string = 'testing once again'; - t.equal(computeCount, 1); - - obj.karkat; - t.equal(computeCount, 2); - - obj.string = 'testing once again'; - t.equal(computeCount, 2); - - obj.karkat; - t.equal(computeCount, 2); -}); - -t.test(`CacheableObject combined update & expose (no transform)`, t => { - const obj = newCacheableObject({ - directory: { - flags: { - update: true, - expose: true - } - } - }); - - t.plan(2); - - obj.directory = 'the-world-revolving'; - t.equal(obj.directory, 'the-world-revolving'); - - obj.directory = 'chaos-king'; - t.equal(obj.directory, 'chaos-king'); -}); - -t.test(`CacheableObject combined update & expose (basic transform)`, t => { - const obj = newCacheableObject({ - getsRepeated: { - flags: { - update: true, - expose: true - }, - - expose: { - transform: value => value.repeat(2) - } - } - }); - - t.plan(1); - - obj.getsRepeated = 'dog'; - t.equal(obj.getsRepeated, 'dogdog'); -}); - -t.test(`CacheableObject combined update & expose (transform with dependency)`, t => { - const obj = newCacheableObject({ - customRepeat: { - flags: { - update: true, - expose: true - }, - - expose: { - dependencies: ['times'], - transform: (value, { times }) => value.repeat(times) - } - }, - - times: { - flags: { - update: true - } - } - }); - - t.plan(3); - - obj.customRepeat = 'dog'; - obj.times = 1; - t.equal(obj.customRepeat, 'dog'); - - obj.times = 5; - t.equal(obj.customRepeat, 'dogdogdogdogdog'); - - obj.customRepeat = 'cat'; - t.equal(obj.customRepeat, 'catcatcatcatcat'); -}); - -t.test(`CacheableObject validate on update`, t => { - const mockError = new TypeError(`Expected a string, not ${typeof value}`); - - const obj = newCacheableObject({ - directory: { - flags: { - update: true, - expose: true - }, - - update: { - validate: value => { - if (typeof value !== 'string') { - throw mockError; - } - return true; - } - } - }, - - date: { - flags: { - update: true, - expose: true - }, - - update: { - validate: value => (value instanceof Date) - } - } - }); - - let thrownError; - t.plan(6); - - obj.directory = 'megalovania'; - t.equal(obj.directory, 'megalovania'); - - try { - obj.directory = 25; - } catch (err) { - thrownError = err; - } - - t.equal(thrownError, mockError); - t.equal(obj.directory, 'megalovania'); - - const date = new Date(`25 December 2009`); - - obj.date = date; - t.equal(obj.date, date); - - try { - obj.date = `TWELFTH PERIGEE'S EVE`; - } catch (err) { - thrownError = err; - } - - t.equal(thrownError?.constructor, TypeError); - t.equal(obj.date, date); -}); - -t.test(`CacheableObject default update property value`, t => { - const obj = newCacheableObject({ - fruit: { - flags: { - update: true, - expose: true - }, - - update: { - default: 'potassium' - } - } - }); - - t.plan(1); - t.equal(obj.fruit, 'potassium'); -}); - -t.test(`CacheableObject default property throws if invalid`, t => { - const mockError = new TypeError(`Expected a string, not ${typeof value}`); - - t.plan(1); - - let thrownError; - - try { - newCacheableObject({ - string: { - flags: { - update: true - }, - - update: { - default: 123, - validate: value => { - if (typeof value !== 'string') { - throw mockError; - } - return true; - } - } - } - }); - } catch (err) { - thrownError = err; - } - - t.equal(thrownError, mockError); -}); -- cgit 1.3.0-6-gf8a5