@lumjs/tests 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.6.0] - 2022-09-12
10
+ #### The *sub-classes* update
11
+ ### Added
12
+ - `Test.new()` works like the `@lumjs/tests.new()`, but uses `this` so it's sub-classable.
13
+ - `Test.static()`, calls `@lumjs/tests.functional()`, and passes `this` as the `testClass`.
14
+ - `Test#TAP`, read-only accessor alias to `Test#tap()`.
15
+ - `Test.$METHODS.$meta`, a list of properties to skip in `$METHODS.all` output.
16
+ - `Test.$METHODS.extend()`, allow `Test` *sub-classes* to build upon the `$METHODS`
17
+ without changing the list in the original `Test` class.
18
+ ### Changed
19
+ - Reworked a bunch of the DocBlocks to make the docs better.
20
+ - Modified the `functional()` method to accept a `testClass` parameter.
21
+ This allows *sub-classes* to make their own functional APIs.
22
+ - Changed `Log.tap()` to make the `details` structure more flexible.
23
+ The `wanted` property is now optional.
24
+ - The `Test` class saves the test method list for `call()` into `this.$testMethods`
25
+ instead of using the `.$METHODS.test` directly.
26
+ ### Fixed
27
+ - Added some missing functions to the registered list of test methods.
28
+
29
+ ## [1.5.0] - 2022-08-30
30
+ ### Added
31
+ - Sample `data` used in some of the old tests.
32
+ ### Fixed
33
+ - Mistakes in the changelog.
34
+
35
+ ## [1.4.0] - 2022-07-29
36
+ ### Added
37
+ - Configuration for JSDoc.
38
+ - A few module-level *docblocks*.
39
+ - Explicit `exports` section in `package.json` file.
40
+ - Added `test.done()` method to be used instead of `test.output()`.
41
+ - Added ability to configure the stringify depth.
42
+ - Added ability to detect if the script was ran directly.
43
+ - Added ability to check for a top-level `Harness` instance.
44
+ - Added four new *binary flag* comparitor tests to `cmp()` method.
45
+ - Added `not` alias for `!==` comparitor.
46
+ - Added `matches` method for using a regular expression to match a string.
47
+ - Added `callIs()` method that is like `call()` but takes a desired value and passes the function return value to `cmp()`, `isa()`, or other test methods.
48
+ - A new `test.ran` computed property.
49
+
50
+ ### Changed
51
+ - Updated `@lumjs/core` dependency to `^1.0.0` (no more *beta* tags!)
52
+ - Updated various *docblocks* for documentation.
53
+ - Enhanced a lot of docblocks.
54
+ - Updated anything using `types.stringify()` to support the depth setting.
55
+ - Updated `run()` so it can use either `call()` or `callIs()` as the underlying test method when using a custom `function` test.
56
+
9
57
  ## [1.3.0] - 2022-07-27
10
58
  ### Added
11
59
  - `$call()` function; powers `call()`, `lives()`, `dies()`, and `diesWith()`.
@@ -57,7 +105,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
57
105
  - Ported from Lum.js v4 library set.
58
106
  - Added a few more features from the PHP version.
59
107
 
60
- [Unreleased]: https://github.com/supernovus/lum.tests.js/compare/v1.3.0...HEAD
108
+ [Unreleased]: https://github.com/supernovus/lum.tests.js/compare/v1.6.0...HEAD
109
+ [1.6.0]: https://github.com/supernovus/lum.tests.js/compare/v1.5.0...v1.6.0
110
+ [1.5.0]: https://github.com/supernovus/lum.tests.js/compare/v1.4.0...v1.5.0
111
+ [1.4.0]: https://github.com/supernovus/lum.tests.js/compare/v1.3.0...v1.4.0
61
112
  [1.3.0]: https://github.com/supernovus/lum.tests.js/compare/v1.2.0...v1.3.0
62
113
  [1.2.0]: https://github.com/supernovus/lum.tests.js/compare/v1.1.1...v1.2.0
63
114
  [1.1.1]: https://github.com/supernovus/lum.tests.js/compare/v1.1.0...v1.1.1
package/TODO.md CHANGED
@@ -3,6 +3,8 @@
3
3
  - Write tests for:
4
4
  - `call()`
5
5
  - `diesWith()`
6
+ - `callIs()`
7
+ - `matches()`
6
8
  - `run()`
7
9
  - Write the `Harness` class, based of the Lum.php version.
8
10
  - Add tests for `Harness`.
package/jsdoc.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "tags":
3
+ {
4
+ "allowUnknownTags": true
5
+ },
6
+ "source":
7
+ {
8
+ "include": ["./lib"]
9
+ },
10
+ "opts":
11
+ {
12
+ "destination": "./docs/api",
13
+ "recurse": true
14
+ },
15
+ "plugins":
16
+ [
17
+ "plugins/markdown"
18
+ ],
19
+ "templates":
20
+ {
21
+ "cleverLinks": false,
22
+ "monospaceLinks": false,
23
+ "default":
24
+ {
25
+ "outputSourceFiles": true
26
+ },
27
+ "path": "ink-docstrap",
28
+ "theme": "cerulean",
29
+ "navType": "vertical",
30
+ "linenums": true,
31
+ "dateFormat": "YYYY-MM-DD, hh:mm:ss"
32
+ }
33
+ }
package/lib/functional.js CHANGED
@@ -1,17 +1,18 @@
1
1
  // A functional interface to the test library.
2
2
  const Test = require('./test');
3
3
 
4
- // A list of methods we can proxy directly.
5
- const PROXY_METHODS = Test.$METHODS.all;
6
-
7
4
  /**
8
5
  * A new test instance and a set of functions wrapping it.
9
- * @typedef {object} Functional
6
+ *
7
+ * @typedef {object} module:@lumjs/tests/functional.Functional
10
8
  * @property {Test} test - The new test instance.
11
9
  *
12
10
  * All of the rest of the properties are functions that
13
11
  * can be imported into a JS scope using destructuring, and
14
12
  * which wrap the corresponding test instance method.
13
+ *
14
+ * Basically anything that is a method of the `Test` instance
15
+ * will become a wrapped `function` available in this structure.
15
16
  */
16
17
 
17
18
  /**
@@ -21,11 +22,12 @@ const PROXY_METHODS = Test.$METHODS.all;
21
22
  * Usage is like:
22
23
  *
23
24
  * ```js
24
- * const {plan,ok,isa} = require('@lumjs/tests').functional({module});
25
+ * const {plan,ok,isa,done} = require('@lumjs/tests').functional({module});
25
26
  *
26
27
  * plan(2);
27
28
  * ok(true, 'ok() works');
28
29
  * isa(isa, 'function', 'isa is a function');
30
+ * done();
29
31
  *
30
32
  * ```
31
33
  *
@@ -36,14 +38,25 @@ const PROXY_METHODS = Test.$METHODS.all;
36
38
  *
37
39
  * @param {object} [opts] Options to pass to the `Test` constructor.
38
40
  *
39
- * @returns {Functional}
41
+ * @param {function} [testClass=Test] The class we're creating an instance of.
42
+ *
43
+ * This defaults to `Test`, but can be changed to be a sub-class such as
44
+ * `DOMTest` (defined in the `@lumjs/tests-dom` library.) This feature is
45
+ * likely not useful for most end users, as knowledge of the internals is
46
+ * required to make the functional API work.
47
+ *
48
+ * @returns {module:@lumjs/tests/functional.Functional}
40
49
  *
50
+ * @exports module:@lumjs/tests/functional
41
51
  */
42
- function functional(opts={})
52
+ function functional(opts={}, testClass=Test)
43
53
  {
44
- const test = new Test(opts);
54
+ //console.debug("functional()", testClass, testClass.$METHODS);
55
+ // A list of methods we can proxy directly.
56
+ const proxyMethods = testClass.$METHODS.all;
57
+ const test = new testClass(opts);
45
58
  const functions = { test };
46
- for (const meth of PROXY_METHODS)
59
+ for (const meth of proxyMethods)
47
60
  {
48
61
  functions[meth] = function ()
49
62
  {
package/lib/harness.js CHANGED
@@ -2,6 +2,11 @@
2
2
  const Test = require('./test');
3
3
  const Log = require('./log');
4
4
 
5
+ /**
6
+ * Module defining the Harness class.
7
+ * @module @lumjs/tests/harness
8
+ */
9
+
5
10
  /**
6
11
  * A class that acts as a test harness for running other tests.
7
12
  *
@@ -14,3 +19,7 @@ class Harness
14
19
  throw new Error("Not yet implemented");
15
20
  }
16
21
  }
22
+
23
+ // Export it.
24
+ module.exports = Harness;
25
+
package/lib/index.js CHANGED
@@ -1,13 +1,28 @@
1
1
  /**
2
2
  * Several test related classes.
3
+ * @module @lumjs/tests
3
4
  */
4
5
 
6
+ /**
7
+ * Test class.
8
+ * @alias module:@lumjs/tests.Test
9
+ * @see module:@lumjs/tests/test
10
+ */
5
11
  const Test = require('./test');
6
-
7
12
  module.exports.Test = Test;
13
+
14
+ /**
15
+ * Test Harness class.
16
+ * @alias module:@lumjs/tests.Harness
17
+ * @see module:@lumjs/tests/harness
18
+ */
8
19
  module.exports.Harness = require('./harness');
9
20
 
10
- // This one is not a class, but a special function.
21
+ /**
22
+ * Functional API registration.
23
+ * @alias module:@lumjs/tests.functional
24
+ * @see module:@lumjs/tests/functional
25
+ */
11
26
  module.exports.functional = require('./functional');
12
27
 
13
28
  /**
package/lib/log.js CHANGED
@@ -1,10 +1,12 @@
1
1
 
2
2
  const types = require('@lumjs/core').types;
3
- const {F,S,O,isArray,stringify} = types;
3
+ const {S,O,isArray,stringify,def} = types;
4
4
 
5
5
  /**
6
6
  * A log representing the results of a test.
7
7
  *
8
+ * @alias module:@lumjs/tests/test~Log
9
+ *
8
10
  * @property {boolean} ok - Did the test pass.
9
11
  * @property {boolean} skipped - Was the test skipped?
10
12
  * @property {string} skippedReason - If the test was skipped, why?
@@ -19,7 +21,11 @@ const {F,S,O,isArray,stringify} = types;
19
21
  */
20
22
  class Log
21
23
  {
22
- constructor ()
24
+ /**
25
+ * (Internal Constructor)
26
+ * @param {module:@lumjs/tests/test} test - The parent `Test` instance.
27
+ */
28
+ constructor (test)
23
29
  {
24
30
  this.ok = false;
25
31
  this.skipped = false;
@@ -27,6 +33,8 @@ class Log
27
33
  this.desc = null;
28
34
  this.directive = null;
29
35
  this.details = {};
36
+ this.stringifyDepth = test.stringifyDepth;
37
+ def(this, '$test$', test);
30
38
  }
31
39
 
32
40
  /**
@@ -40,6 +48,8 @@ class Log
40
48
  */
41
49
  tap (num)
42
50
  {
51
+ const SD = this.stringifyDepth;
52
+
43
53
  var out;
44
54
  if (this.ok)
45
55
  out = 'ok ';
@@ -60,17 +70,21 @@ class Log
60
70
 
61
71
  out += "\n";
62
72
 
63
- if ('got' in this.details && 'wanted' in this.details)
73
+ if ('got' in this.details)
64
74
  {
65
- var got = this.details.got;
66
- var want = this.details.wanted;
67
- if (this.details.stringify)
75
+ const stringy = this.details.stringify;
76
+
77
+ let got = this.details.got;
78
+ if (stringy) got = stringify(got, SD);
79
+ out += `# got: ${got}\n`;
80
+
81
+ if ('wanted' in this.details)
68
82
  {
69
- got = stringify(got);
70
- want = stringify(want);
83
+ let want = this.details.wanted;
84
+ if (stringy) want = stringify(want, SD);
85
+ out += `# expected: ${want}\n`;
71
86
  }
72
- out += `# got: ${got}\n`;
73
- out += `# expected: ${want}\n`;
87
+
74
88
  if (typeof this.details.comparitor === S)
75
89
  {
76
90
  out += `# op: ${this.details.comparitor}\n`;
@@ -86,7 +100,7 @@ class Log
86
100
 
87
101
  for (const i in info)
88
102
  {
89
- const line = (typeof info[i] === S) ? info[i] : stringify(info[i]);
103
+ const line = (typeof info[i] === S) ? info[i] : stringify(info[i], SD);
90
104
  out += `## ${line}\n`;
91
105
  }
92
106
  }
package/lib/test.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const core = require('@lumjs/core');
2
- const types = core.types;
3
- const {F,S,N,isObj,isArray,def} = types;
2
+ const {types,obj} = core;
3
+ const {F,S,N,isObj,isArray,needs,def} = types;
4
4
 
5
5
  // We use a separate class to represent test logs.
6
6
  const Log = require('./log');
@@ -8,14 +8,14 @@ const Log = require('./log');
8
8
  // A list of Test methods that return Log objects.
9
9
  const TEST_METHODS =
10
10
  [
11
- 'ok', 'call', 'fail', 'pass', 'dies', 'diesWith', 'lives', 'cmp',
12
- 'is', 'isnt', 'isa', 'nota', 'isJSON', 'isntJSON', 'skip',
11
+ 'ok', 'call', 'callIs', 'fail', 'pass', 'dies', 'diesWith', 'lives', 'cmp',
12
+ 'is', 'isnt', 'isa', 'nota', 'isJSON', 'isntJSON', 'matches', 'skip',
13
13
  ];
14
14
 
15
15
  // A list of other methods to export that are not standard tests.
16
16
  const META_METHODS =
17
17
  [
18
- 'plan', 'diag', 'run', 'tap', 'output',
18
+ 'plan', 'diag', 'run', 'tap', 'output', 'done',
19
19
  ];
20
20
 
21
21
  // The function that powers `Test.call()` and friends.
@@ -39,6 +39,16 @@ function $call (testfunc, args)
39
39
  * Based on Lum.php's Test library.
40
40
  * Which itself was based on Perl 5's Test::More, and
41
41
  * Raku's Test libraries.
42
+ *
43
+ * @exports module:@lumjs/tests/test
44
+ *
45
+ * @property {number} planned - Number of tests planned, `0` if unplanned.
46
+ * @property {number} failed - Number of tests that failed.
47
+ * @property {number} skipped - Number of tests that were skipped.
48
+ * @property {number} ran - Number of tests ran (*calculated*).
49
+ * @property {string} id - Unique test id used by `Harness` libary.
50
+ * @property {boolean} isTop - Test module was loaded from the command line.
51
+ * @property {?object} harness - The top-level `Harness` if one was found.
42
52
  */
43
53
  class Test
44
54
  {
@@ -58,6 +68,9 @@ class Test
58
68
  * Also, if this is passed, and `opts.id` was not specified, and id
59
69
  * will be auto-generated based on the filename of the module.
60
70
  *
71
+ * @param {number} [opts.stringify=1] The depth `stringify()` should recurse
72
+ * objects and Arrays before switching to plain JSON stringification.
73
+ *
61
74
  */
62
75
  constructor (opts={})
63
76
  {
@@ -85,20 +98,72 @@ class Test
85
98
  this.id = null;
86
99
  }
87
100
 
101
+ this.stringifyDepth = opts.stringify ?? 1;
102
+
88
103
  this.failed = 0;
89
104
  this.skipped = 0;
90
105
  this.planned = 0;
91
106
  this.log = [];
92
107
 
108
+ // These three will be updated below if possible.
109
+ this.isTop = false;
110
+ this.harness = null;
111
+
112
+ // Methods that can be ran via run().
113
+ this.$testMethods = TEST_METHODS.slice();
114
+
93
115
  if (typeof opts.plan === N)
94
116
  {
95
117
  this.plan(opts.plan);
96
118
  }
97
119
 
98
120
  if (hasModule)
99
- { // Finally, if a module was passed, its going to export this test.
121
+ { // If a module was passed, its going to export this test.
100
122
  opts.module.exports = this;
123
+ // We'll also use the module to determine if we're Harnessed or not.
124
+ if (require.main === opts.module)
125
+ { // Was called directly.
126
+ this.isTop = true;
127
+ }
128
+ }
129
+
130
+ if (!this.isTop)
131
+ { // Try to find a Harness instance.
132
+ if (isObj(require.main) && require.main.exports instanceof Harness)
133
+ { // We found the Harness instance.
134
+ this.harness = require.main.exports;
135
+ }
101
136
  }
137
+
138
+ }
139
+
140
+ /**
141
+ * A static shortcut for creating a new instance.
142
+ *
143
+ * @param {object} [opts] Options to pass to constructor.
144
+ * @returns {module:@lumjs/tests/test}
145
+ */
146
+ static new(opts={})
147
+ {
148
+ return new this(opts);
149
+ }
150
+
151
+ /**
152
+ * A static shortcut for creating a `Functional` API object.
153
+ *
154
+ * @param {object} [opts] Options to pass to constructor.
155
+ * @returns {module:@lumjs/tests/functional.Functional}
156
+ */
157
+ static functional(opts={})
158
+ {
159
+ //console.debug('functional this', this);
160
+ return require('./functional')(opts, this);
161
+ }
162
+
163
+ // A wrapper around types.stringify()
164
+ stringify(what)
165
+ {
166
+ return types.stringify(what, this.stringifyDepth);
102
167
  }
103
168
 
104
169
  /**
@@ -142,7 +207,7 @@ class Test
142
207
  */
143
208
  ok (test, desc, directive, details)
144
209
  {
145
- const log = new Log();
210
+ const log = new Log(this);
146
211
 
147
212
  if (test)
148
213
  {
@@ -185,7 +250,7 @@ class Test
185
250
  * @param {...any} [args] Arguments to pass to the test function.
186
251
  * @returns {Log}
187
252
  */
188
- call(testfunc, desc, ...args)
253
+ call (testfunc, desc, ...args)
189
254
  {
190
255
  const ret = $call(testfunc, args);
191
256
  return this.ok(ret.val, desc, ret.err);
@@ -257,7 +322,7 @@ class Test
257
322
  * @param {...any} [args]
258
323
  * @returns {Log}
259
324
  */
260
- diesWith(testfunc, testerr, desc, ...args)
325
+ diesWith (testfunc, testerr, desc, ...args)
261
326
  {
262
327
  let ok = false, details = {}, err = null;
263
328
 
@@ -320,15 +385,22 @@ class Test
320
385
  * @param {string} comp - A comparitor to test with.
321
386
  *
322
387
  * - `===`, `is` (See also: `is()`)
323
- * - `!==`, `isnt` (See also: `isnt()`)
388
+ * - `!==`, `isnt`, `not` (See also: `isnt()`)
324
389
  * - `==`, `eq`
325
390
  * - `!=`, `ne`
326
391
  * - `>`, `gt`
327
392
  * - `<`, `lt`
328
393
  * - `>=`, `ge`, `gte`
329
394
  * - `<=`, `le`, `lte`
395
+ *
396
+ * A few special comparitors for *binary flag* testing:
397
+ *
398
+ * - `=&` → `((got & want) === want)`
399
+ * - `!&` → `((got & want) !== want)`
400
+ * - `+&` → `((got & want) !== 0)`
401
+ * - `-&` → `((got & want) === 0)`
330
402
  *
331
- * @param {string} desc
403
+ * @param {string} [desc]
332
404
  * @param {boolean} [stringify=true] Stringify values in TAP output?
333
405
  * @returns {Log}
334
406
  */
@@ -343,6 +415,7 @@ class Test
343
415
  break;
344
416
  case 'isnt':
345
417
  case '!==':
418
+ case 'not':
346
419
  test = (got !== want);
347
420
  break;
348
421
  case 'eq':
@@ -371,6 +444,16 @@ class Test
371
444
  case '>=':
372
445
  test = (got >= want);
373
446
  break;
447
+ case '=&':
448
+ test = ((got&want)===want);
449
+ break;
450
+ case '!&':
451
+ test = ((got&want)!==want)
452
+ case '+&':
453
+ test = ((got&want)!==0);
454
+ break;
455
+ case '-&':
456
+ test = ((got&want)===0);
374
457
  default:
375
458
  test = false;
376
459
  }
@@ -390,6 +473,39 @@ class Test
390
473
  return this.ok(test, desc, null, details);
391
474
  }
392
475
 
476
+ /**
477
+ * See if a string matches a value.
478
+ *
479
+ * @param {string} got
480
+ * @param {RegExp} want
481
+ * @param {string} [desc]
482
+ * @param {boolean} [stringify=true]
483
+ * @returns {Log}
484
+ */
485
+ matches(got, want, desc, stringify=true)
486
+ {
487
+ const no = {error: "matches 'got' value must be a string"};
488
+ needs(got, no, S);
489
+ no.error = "matches 'want' value must be a RegExp";
490
+ needs(want, no, RegExp);
491
+
492
+ const test = want.test(got);
493
+
494
+ let details = null;
495
+ if (!test)
496
+ { // The test failed, add the deets.
497
+ details =
498
+ {
499
+ got,
500
+ wanted: want,
501
+ stringify,
502
+ comparitor: 'matches',
503
+ };
504
+ }
505
+
506
+ return this.ok(test, desc, null, details);
507
+ }
508
+
393
509
  /**
394
510
  * See if two values are equal.
395
511
  *
@@ -486,13 +602,13 @@ class Test
486
602
  *
487
603
  * @param {*} got
488
604
  * @param {*} want
489
- * @param {string} desc
490
- * @returns
605
+ * @param {string} [desc]
606
+ * @returns {Log}
491
607
  */
492
608
  isJSON (got, want, desc)
493
609
  {
494
- got = types.stringify(got);
495
- want = types.stringify(want);
610
+ got = this.stringify(got);
611
+ want = this.stringify(want);
496
612
  return this.is(got, want, desc, false);
497
613
  }
498
614
 
@@ -503,16 +619,99 @@ class Test
503
619
  *
504
620
  * @param {*} got
505
621
  * @param {*} want
506
- * @param {string} desc
507
- * @returns
622
+ * @param {string} [desc]
623
+ * @returns {Log}
508
624
  */
509
625
  isntJSON (got, want, desc)
510
626
  {
511
- got = types.stringify(got);
512
- want = types.stringify(want);
627
+ got = this.stringify(got);
628
+ want = this.stringify(want);
513
629
  return this.isnt(got, want, desc, false);
514
630
  }
515
631
 
632
+ /**
633
+ * Run a function and see if it's return value is what we wanted.
634
+ *
635
+ * @param {function} testfunc - The function to run.
636
+ * The return value will be passed to `cmp()` or another appropriate
637
+ * testing method as determined by the options.
638
+ * How this handles error handling is determined by options as well.
639
+ *
640
+ * @param {*} want - The value we want.
641
+ * @param {(object|string)} [opts] Named options for further behaviour.
642
+ * If it is a string it's considered the `opts.desc` option.
643
+ * @param {string} [opts.desc] A description for `ok()`.
644
+ * @param {boolean} [opts.stringify=true]
645
+ * @param {Array} [opts.args] Arguments to pass to the test function.
646
+ * @param {string} [opts.comp="is"] - The comparitor to test with.
647
+ * In addition to all of the comparitors from `cmp()`, there are a few
648
+ * extra comparitors that will pass through to other methods:
649
+ * - `isa` → Use `isa()` to test return value.
650
+ * - `nota` → Use `nota()` to test return value.
651
+ * - `=json`, `isJSON` → Use `isJSON()` to test return value.
652
+ * - `!json`, `isntJSON` → Use `isntJSON()` to test return value.
653
+ * - `matches` → Use `matches()` to test return value.
654
+ *
655
+ * @param {boolean} [opts.thrown=false] How to handle thrown errors.
656
+ *
657
+ * If this is `true`, then anything thrown will be passed as if it was
658
+ * the return value from the function.
659
+ *
660
+ * If this is `false`, then any errors thrown will result in an immediate
661
+ * failure of the test without any further processing, and the error will
662
+ * be passed as the `directive` to the `ok()` method.
663
+ *
664
+ * @returns {Log}
665
+ */
666
+ callIs (testfunc, want, opts={})
667
+ {
668
+ const args = opts.args ?? [];
669
+ const ret = $call(testfunc, args);
670
+ const desc = opts.desc;
671
+
672
+ let got;
673
+
674
+ if (ret.err)
675
+ { // How to handle errors.
676
+ if (opts.thrown)
677
+ { // We're going to test the error.
678
+ got = ret.err;
679
+ }
680
+ else
681
+ { // This is an automatic failure.
682
+ return this.ok(false, desc, ret.err);
683
+ }
684
+ }
685
+ else
686
+ { // No errors, good, testing against the return value.
687
+ got = ret.val;
688
+ }
689
+
690
+ const CFUN =
691
+ {
692
+ 'matches': 'matches',
693
+ 'isa': 'isa',
694
+ 'nota': 'nota',
695
+ 'isJSON': 'isJSON',
696
+ '=json': 'isJSON',
697
+ 'isntJSON': 'isntJSON',
698
+ '!json': 'isntJSON',
699
+ };
700
+
701
+ const comp = opts.comp ?? 'is';
702
+ const stringify = opts.stringify ?? true;
703
+
704
+ if (typeof CFUN[comp] === S)
705
+ { // A function with a custom return value.
706
+ const meth = CFUN[comp];
707
+ return this[meth](got, want, desc, stringify);
708
+ }
709
+ else
710
+ { // We're going to use the cmp() method.
711
+ return this.cmp(got, want, comp, desc, stringify);
712
+ }
713
+ }
714
+
516
715
  /**
517
716
  * Skip a test.
518
717
  *
@@ -522,7 +721,7 @@ class Test
522
721
  */
523
722
  skip (reason, desc)
524
723
  {
525
- var log = this.ok(true, desc);
724
+ const log = this.ok(true, desc);
526
725
  log.skipped = true;
527
726
  if (typeof reason === S)
528
727
  log.skippedReason = reason;
@@ -554,35 +753,55 @@ class Test
554
753
  * methods in this class, it will be set as the *current* test method.
555
754
  *
556
755
  * If this is a `function`, it will be set as the *current* test method.
557
- * Function test methods are passed to `call()` with the test parameters.
756
+ * By default function test methods are passed to `call()` with the test
757
+ * parameters. However, if the *previous* test method was `callIs` then
758
+ * the `callIs()` method will be used as long as the custom function is
759
+ * the *current* test method. Likewise to switch back to `call()` simply
760
+ * set the *current* test method to `call` before setting it to a new custom
761
+ * test `function`.
558
762
  *
559
763
  * If this is an `Array` then it's the parameters for the *current* test
560
- * method. If a custom `function` is in use, remember that the *first*
561
- * parameter is **always** the `desc`, and any subsequent parameters will be
562
- * passed to the custom `function` call.
764
+ * method. If a custom `function` is in use, remember that it's the
765
+ * `call()` or `callIs()` methods that will be being called, with their
766
+ * first parameter always being the custom function.
563
767
  *
564
768
  * Any value other than one of those will throw a `TypeError`.
565
769
  *
566
770
  * @returns {Log[]} A `Log` item for each test that was ran.
567
771
  */
568
- run(...tests)
772
+ run (...tests)
569
773
  {
774
+ const CF = 'call';
775
+ const CI = 'callIs';
776
+
570
777
  const logs = [];
778
+ let funcall = CF;
571
779
  let current = 'ok';
572
780
 
573
781
  for (const test of tests)
574
782
  {
575
783
  const tt = typeof test;
576
- if (tt === F || (tt === S && TEST_METHODS.includes(test)))
577
- { // Set the current test.
784
+ if (tt === S && this.$testMethods.includes(test))
785
+ { // Set the current test to a built-in.
578
786
  current = test;
579
787
  }
788
+ else if (tt === F)
789
+ { // A custom test function for further tests.
790
+ if (current === CI)
791
+ { // Last test was `callIs` using that for the custom function.
792
+ funcall = CI;
793
+ }
794
+ else if (current === CF)
795
+ { // Last test was `call`, using that for the custom function.
796
+ funcall = CF;
797
+ }
798
+ }
580
799
  else if (isArray(test))
581
800
  { // A set of test parameters.
582
801
  let log;
583
802
  if (typeof current === F)
584
803
  { // A custom test function is in use.
585
- log = this.call(current, ...test);
804
+ log = this[funcall](current, ...test);
586
805
  }
587
806
  else
588
807
  { // A standard test is in use.
@@ -602,15 +821,14 @@ class Test
602
821
  */
603
822
  tap ()
604
823
  {
605
- var out = '';
824
+ let out = '';
606
825
  if (this.planned > 0)
607
826
  {
608
827
  out += '1..'+this.planned+"\n";
609
828
  }
610
- var t = 1;
611
- for (var i = 0; i < this.log.length; i++)
829
+ let t = 1;
830
+ for (const log of this.log)
612
831
  {
613
- var log = this.log[i];
614
832
  if (log instanceof Log)
615
833
  {
616
834
  out += log.tap(t++);
@@ -631,7 +849,7 @@ class Test
631
849
  out += ' out of '+this.planned;
632
850
  out += "\n";
633
851
  }
634
- var ran = t-1;
852
+ const ran = t-1;
635
853
  if (this.planned > 0 && this.planned != ran)
636
854
  {
637
855
  out += '# Looks like you planned '+this.planned+' but ran '+ran+" tests\n";
@@ -639,8 +857,38 @@ class Test
639
857
  return out;
640
858
  }
641
859
 
860
+ /**
861
+ * A read-only *accessor* property alias for `tap()`.
862
+ * @returns {string}
863
+ * @see {@link module:@lumjs/tests/test#tap}
864
+ */
865
+ get TAP()
866
+ {
867
+ return this.tap();
868
+ }
869
+
870
+ /**
871
+ * A calculated property of the number of tests that were ran.
872
+ * @type {int}
873
+ */
874
+ get ran ()
875
+ {
876
+ let ran = 0;
877
+ for (const log of this.log)
878
+ {
879
+ if (log instanceof Log)
880
+ {
881
+ ran++;
882
+ }
883
+ }
884
+ return ran;
885
+ }
886
+
642
887
  /**
643
888
  * Send the TAP output to the `console`.
889
+ *
890
+ * This is a low-level method and is no longer recommended for use.
891
+ * Instead call the `done()` method, which will *do the right thing*.
644
892
  */
645
893
  output ()
646
894
  {
@@ -648,6 +896,25 @@ class Test
648
896
  return this;
649
897
  }
650
898
 
899
+ /**
900
+ * We're done testing.
901
+ *
902
+ * This will mark the test-set as finished, so attempting to run further
903
+ * tests after will result in a `RangeError` being thrown.
904
+ *
905
+ * If no `Harness` is in use, this will also run `this.output()`.
906
+ */
907
+ done ()
908
+ {
909
+ if (this.$done)
910
+ {
911
+ throw new RangeError('Test set is already done');
912
+ }
913
+ this.$done = true;
914
+
915
+ return (this.harness ? this : this.output());
916
+ }
917
+
651
918
  } // class Test
652
919
 
653
920
  // Should never need this, but...
@@ -661,12 +928,17 @@ def(Test, '$METHODS',
661
928
  {
662
929
  test: TEST_METHODS,
663
930
  meta: META_METHODS,
931
+ $meta:
932
+ [ // A list of properties to skip in `all`.
933
+ '$meta', 'all', 'extend',
934
+ ],
664
935
  get all()
665
936
  {
666
937
  const list = [];
938
+ const skip = this.$meta;
667
939
  for (const name in this)
668
940
  {
669
- if (name === 'all') continue;
941
+ if (skip.includes(name)) continue;
670
942
  const prop = this[name];
671
943
  if (isArray(prop))
672
944
  {
@@ -675,7 +947,41 @@ def(Test, '$METHODS',
675
947
  }
676
948
  return list;
677
949
  },
678
- });
950
+ extend(newClass, opts={})
951
+ {
952
+ const mode = obj.CLONE.DEEP;
953
+ const clone = obj.clone(this, {mode});
954
+
955
+ //console.debug("extend:clone<pre>", clone);
956
+
957
+ if (isObj(opts.add))
958
+ { // Add in the extra properties.
959
+ //console.debug("opts.add", opts.add);
960
+ obj.copyProps(opts.add, clone, opts.addOpts);
961
+ }
962
+
963
+ if (isArray(opts.meta))
964
+ { // Added properties to skip in 'all'.
965
+ for (const val of opts.meta)
966
+ {
967
+ if (!clone.$meta.includes(val))
968
+ {
969
+ clone.$meta.push(val);
970
+ }
971
+ }
972
+ }
973
+
974
+ //console.debug("extend:clone<post>", clone);
975
+
976
+ // Add a new cloned and extended `$METHODS` property.
977
+ def(newClass, '$METHODS', {value: clone});
978
+ }, // extend()
979
+ }); // Test.$METHODS
679
980
 
680
981
  // Export the class
681
982
  module.exports = Test;
983
+
984
+ // Finally at the bottom after `module.exports` has been set, we will load
985
+ // the Harness class to avoid circular references failing.
986
+ const Harness = require('./harness');
987
+
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "@lumjs/tests",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
4
4
  "main": "lib/index.js",
5
+ "exports":
6
+ {
7
+ ".": "./lib/index.js",
8
+ "./test": "./lib/test.js",
9
+ "./functional": "./lib/functional.js",
10
+ "./harness": "./lib/harness.js",
11
+ "./package.json": "./package.json",
12
+ "./data/*": "./test/data/*.js"
13
+ },
5
14
  "license": "MIT",
6
15
  "repository":
7
16
  {
@@ -9,11 +18,12 @@
9
18
  "url": "https://github.com/supernovus/lum.tests.js.git"
10
19
  },
11
20
  "dependencies": {
12
- "@lumjs/core": "^1.0.0-beta.4"
21
+ "@lumjs/core": "^1.3.0"
13
22
  },
14
23
  "scripts":
15
24
  {
16
25
  "-TODO-1": "When Harness and bin/lumtest are done, use that for 'test'",
17
- "test": "prove -e node --ext js ./test"
26
+ "test": "prove -e node --ext js ./test",
27
+ "build-docs": "jsdoc -c ./jsdoc.json"
18
28
  }
19
29
  }
@@ -0,0 +1,58 @@
1
+ module.exports = function (opts={})
2
+ {
3
+ let people =
4
+ [
5
+ {
6
+ name: 'Bob',
7
+ age: 40,
8
+ },
9
+ {
10
+ name: 'Lisa',
11
+ age: 25,
12
+ },
13
+ {
14
+ name: 'Kevin',
15
+ age: 18,
16
+ },
17
+ {
18
+ name: 'Sarah',
19
+ age: 13,
20
+ },
21
+ ];
22
+
23
+ if (opts.withRecursion)
24
+ {
25
+ people[0].kids = [people[2],people[3]];
26
+ people[1].kids = [people[3]];
27
+ people[2].kids = [];
28
+ people[3].kids = [];
29
+ people[0].parents = [];
30
+ people[1].parents = [];
31
+ people[2].parents = [people[0]];
32
+ people[3].parents = [people[0],people[1]];
33
+ }
34
+ else if (opts.withReferences)
35
+ {
36
+ people[0].kids = [2,3];
37
+ people[1].kids = [3];
38
+ people[2].kids = [];
39
+ people[3].kids = [];
40
+ people[0].parents = [];
41
+ people[1].parents = [];
42
+ people[2].parents = [0];
43
+ people[3].parents = [0,1];
44
+ }
45
+ else if (opts.withNames)
46
+ {
47
+ people[0].kids = [people[2].name,people[3].name];
48
+ people[1].kids = [people[3].name];
49
+ people[2].kids = [];
50
+ people[3].kids = [];
51
+ people[0].parents = [];
52
+ people[1].parents = [];
53
+ people[2].parents = [people[0].name];
54
+ people[3].parents = [people[0].name,people[1].name];
55
+ }
56
+
57
+ return people;
58
+ }