« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/cacheable-object.js274
-rw-r--r--test/data-validators.js277
2 files changed, 551 insertions, 0 deletions
diff --git a/test/cacheable-object.js b/test/cacheable-object.js
new file mode 100644
index 00000000..203d2af0
--- /dev/null
+++ b/test/cacheable-object.js
@@ -0,0 +1,274 @@
+import test from 'tape';
+
+import CacheableObject from '../src/thing/cacheable-object.js';
+
+// Utility
+
+function newCacheableObject(PD) {
+    return new (class extends CacheableObject {
+        static propertyDescriptors = PD;
+    });
+}
+
+// Tests
+
+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);
+});
+
+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.is(computeCount, 0);
+
+    obj.string = 'hello world';
+    t.is(computeCount, 0);
+
+    obj.karkat;
+    t.is(computeCount, 1);
+
+    obj.karkat;
+    t.is(computeCount, 1);
+
+    obj.string = 'testing once again';
+    t.is(computeCount, 1);
+
+    obj.karkat;
+    t.is(computeCount, 2);
+
+    obj.string = 'testing once again';
+    t.is(computeCount, 2);
+
+    obj.karkat;
+    t.is(computeCount, 2);
+});
+
+test(`CacheableObject combined update & expose (no transform)`, t => {
+    const obj = newCacheableObject({
+        directory: {
+            flags: {
+                update: true,
+                expose: true
+            }
+        }
+    });
+
+    t.plan(2);
+
+    t.directory = 'the-world-revolving';
+    t.is(t.directory, 'the-world-revolving');
+
+    t.directory = 'chaos-king';
+    t.is(t.directory, 'chaos-king');
+});
+
+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.is(obj.getsRepeated, 'dogdog');
+});
+
+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.is(obj.customRepeat, 'dog');
+
+    obj.times = 5;
+    t.is(obj.customRepeat, 'dogdogdogdogdog');
+
+    obj.customRepeat = 'cat';
+    t.is(obj.customRepeat, 'catcatcatcatcat');
+});
+
+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.is(obj.directory, 'megalovania');
+
+    try {
+        obj.directory = 25;
+    } catch (err) {
+        thrownError = err;
+    }
+
+    t.is(thrownError, mockError);
+    t.is(obj.directory, 'megalovania');
+
+    const date = new Date(`25 December 2009`);
+
+    obj.date = date;
+    t.is(obj.date, date);
+
+    try {
+        obj.date = `TWELFTH PERIGEE'S EVE`;
+    } catch (err) {
+        thrownError = err;
+    }
+
+    t.is(thrownError?.constructor, TypeError);
+    t.is(obj.date, date);
+});
+
+test(`CacheableObject default update property value`, t => {
+    const obj = newCacheableObject({
+        fruit: {
+            flags: {
+                update: true,
+                expose: true
+            },
+
+            update: {
+                default: 'potassium'
+            }
+        }
+    });
+
+    t.plan(1);
+    t.is(obj.fruit, 'potassium');
+});
+
+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.is(thrownError, mockError);
+});
diff --git a/test/data-validators.js b/test/data-validators.js
new file mode 100644
index 00000000..739333a3
--- /dev/null
+++ b/test/data-validators.js
@@ -0,0 +1,277 @@
+import _test from 'tape';
+import { showAggregate } from '../src/util/sugar.js';
+
+import {
+    // Basic types
+    isBoolean,
+    isCountingNumber,
+    isNumber,
+    isString,
+    isStringNonEmpty,
+
+    // Complex types
+    isArray,
+    isObject,
+    validateArrayItems,
+
+    // Wiki data
+    isDimensions,
+    isDirectory,
+    isDuration,
+    isFileExtension,
+    validateReference,
+    validateReferenceList,
+
+    // Compositional utilities
+    oneOf,
+} from '../src/thing/validators.js';
+
+function test(msg, fn) {
+    _test(msg, t => {
+        try {
+            fn(t);
+        } catch (error) {
+            if (error instanceof AggregateError) {
+                showAggregate(error);
+            }
+            throw error;
+        }
+    });
+}
+
+test.skip = _test.skip;
+
+// Basic types
+
+test('isBoolean', t => {
+    t.plan(4);
+    t.ok(isBoolean(true));
+    t.ok(isBoolean(false));
+    t.throws(() => isBoolean(1), TypeError);
+    t.throws(() => isBoolean('yes'), TypeError);
+});
+
+test('isNumber', t => {
+    t.plan(6);
+    t.ok(isNumber(123));
+    t.ok(isNumber(0.05));
+    t.ok(isNumber(0));
+    t.ok(isNumber(-10));
+    t.throws(() => isNumber('413'), TypeError);
+    t.throws(() => isNumber(true), TypeError);
+});
+
+test('isCountingNumber', t => {
+    t.plan(6);
+    t.ok(isCountingNumber(3));
+    t.ok(isCountingNumber(1));
+    t.throws(() => isCountingNumber(1.75), TypeError);
+    t.throws(() => isCountingNumber(0), TypeError);
+    t.throws(() => isCountingNumber(-1), TypeError);
+    t.throws(() => isCountingNumber('612'), TypeError);
+});
+
+test('isString', t => {
+    t.plan(3);
+    t.ok(isString('hello!'));
+    t.ok(isString(''));
+    t.throws(() => isString(100), TypeError);
+});
+
+test('isStringNonEmpty', t => {
+    t.plan(4);
+    t.ok(isStringNonEmpty('hello!'));
+    t.throws(() => isStringNonEmpty(''), TypeError);
+    t.throws(() => isStringNonEmpty('     '), TypeError);
+    t.throws(() => isStringNonEmpty(100), TypeError);
+});
+
+// Complex types
+
+test('isArray', t => {
+    t.plan(3);
+    t.ok(isArray([]));
+    t.throws(() => isArray({}), TypeError);
+    t.throws(() => isArray('1, 2, 3'), TypeError);
+});
+
+test.skip('isDate', t => {
+    // TODO
+});
+
+test('isObject', t => {
+    t.plan(3);
+    t.ok(isObject({}));
+    t.ok(isObject([]));
+    t.throws(() => isObject(null), TypeError);
+});
+
+test('validateArrayItems', t => {
+    t.plan(6);
+
+    t.ok(validateArrayItems(isNumber)([3, 4, 5]));
+    t.ok(validateArrayItems(validateArrayItems(isNumber))([[3, 4], [4, 5], [6, 7]]));
+
+    let caughtError = null;
+    try {
+        validateArrayItems(isNumber)([10, 20, 'one hundred million consorts', 30]);
+    } catch (err) {
+        caughtError = err;
+    }
+
+    t.isNot(caughtError, null);
+    t.true(caughtError instanceof AggregateError);
+    t.is(caughtError.errors.length, 1);
+    t.true(caughtError.errors[0] instanceof TypeError);
+});
+
+// Wiki data
+
+test.skip('isColor', t => {
+    // TODO
+});
+
+test.skip('isCommentary', t => {
+    // TODO
+});
+
+test.skip('isContribution', t => {
+    // TODO
+});
+
+test.skip('isContributionList', t => {
+    // TODO
+});
+
+test('isDimensions', t => {
+    t.plan(6);
+    t.ok(isDimensions([1, 1]));
+    t.ok(isDimensions([50, 50]));
+    t.ok(isDimensions([5000, 1]));
+    t.throws(() => isDimensions([1]), TypeError);
+    t.throws(() => isDimensions([413, 612, 1025]), TypeError);
+    t.throws(() => isDimensions('800x200'), TypeError);
+});
+
+test('isDirectory', t => {
+    t.plan(6);
+    t.ok(isDirectory('savior-of-the-waking-world'));
+    t.ok(isDirectory('MeGaLoVania'));
+    t.ok(isDirectory('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'));
+    t.throws(() => isDirectory(123), TypeError);
+    t.throws(() => isDirectory(''), TypeError);
+    t.throws(() => isDirectory('troll saint nicholas and the quest for the holy pail'), TypeError);
+});
+
+test('isDuration', t => {
+    t.plan(5);
+    t.ok(isDuration(60));
+    t.ok(isDuration(0.02));
+    t.ok(isDuration(0));
+    t.throws(() => isDuration(-1), TypeError);
+    t.throws(() => isDuration('10:25'), TypeError);
+});
+
+test('isFileExtension', t => {
+    t.plan(6);
+    t.ok(isFileExtension('png'));
+    t.ok(isFileExtension('jpg'));
+    t.ok(isFileExtension('sub_loc'));
+    t.throws(() => isFileExtension(''), TypeError);
+    t.throws(() => isFileExtension('.jpg'), TypeError);
+    t.throws(() => isFileExtension('just an image bro!!!!'), TypeError);
+});
+
+test.skip('isName', t => {
+    // TODO
+});
+
+test.skip('isURL', t => {
+    // TODO
+});
+
+test('validateReference', t => {
+    t.plan(16);
+
+    const typeless = validateReference();
+    const track = validateReference('track');
+    const album = validateReference('album');
+
+    t.ok(track('track:doctor'));
+    t.ok(track('track:MeGaLoVania'));
+    t.ok(track('Showtime (Imp Strife Mix)'));
+    t.throws(() => track('track:troll saint nic'), TypeError);
+    t.throws(() => track('track:'), TypeError);
+    t.throws(() => track('album:homestuck-vol-1'), TypeError);
+
+    t.ok(album('album:sburb'));
+    t.ok(album('album:the-wanderers'));
+    t.ok(album('Homestuck Vol. 8'));
+    t.throws(() => album('album:Hiveswap Friendsim'), TypeError);
+    t.throws(() => album('album:'), TypeError);
+    t.throws(() => album('track:showtime-piano-refrain'), TypeError);
+
+    t.ok(typeless('Hopes and Dreams'));
+    t.ok(typeless('track:snowdin-town'));
+    t.throws(() => typeless(''), TypeError);
+    t.throws(() => typeless('album:undertale-soundtrack'));
+});
+
+test('validateReferenceList', t => {
+    const track = validateReferenceList('track');
+    const artist = validateReferenceList('artist');
+
+    t.plan(9);
+
+    t.ok(track(['track:fallen-down', 'Once Upon a Time']));
+    t.ok(artist(['artist:toby-fox', 'Mark Hadley']));
+    t.ok(track(['track:amalgam']));
+    t.ok(track([]));
+
+    let caughtError = null;
+    try {
+        track(['Dog', 'album:vaporwave-2016', 'Cat', 'artist:john-madden']);
+    } catch (err) {
+        caughtError = err;
+    }
+
+    t.isNot(caughtError, null);
+    t.true(caughtError instanceof AggregateError);
+    t.is(caughtError.errors.length, 2);
+    t.true(caughtError.errors[0] instanceof TypeError);
+    t.true(caughtError.errors[1] instanceof TypeError);
+});
+
+test('oneOf', t => {
+    t.plan(11);
+
+    const isStringOrNumber = oneOf(isString, isNumber);
+
+    t.ok(isStringOrNumber('hello world'));
+    t.ok(isStringOrNumber(42));
+    t.throws(() => isStringOrNumber(false));
+
+    const mockError = new Error();
+    const neverSucceeds = () => {
+        throw mockError;
+    };
+
+    const isStringOrGetRekt = oneOf(isString, neverSucceeds);
+
+    t.ok(isStringOrGetRekt('phew!'));
+
+    let caughtError = null;
+    try {
+        isStringOrGetRekt(0xdeadbeef);
+    } catch (err) {
+        caughtError = err;
+    }
+
+    t.isNot(caughtError, null);
+    t.true(caughtError instanceof AggregateError);
+    t.is(caughtError.errors.length, 2);
+    t.true(caughtError.errors[0] instanceof TypeError);
+    t.is(caughtError.errors[0].check, isString);
+    t.is(caughtError.errors[1], mockError);
+    t.is(caughtError.errors[1].check, neverSucceeds);
+});