risei 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -123,9 +123,9 @@ You can save a lot of time by putting repeated test properties into an object on
123
123
  { of: "countOf", for: "Returns the number present.", in: [ "b" ], out: 2 }
124
124
  ```
125
125
 
126
- - This _collapsing forward_ works for just about every test property, but is reset when you change the class in `.on`, when you give the property a new value, or you erase it with an empty array `[ ]`.
126
+ - This _collapsing forward_ is reset when you change the class in `.on`, when you give the property a new value, or you erase it with an empty array `[ ]`.
127
127
 
128
- - Collapsing forward doesn't reduce test isolation — for instance, with mutable args — because Risei always runs a copy of the test definition, not the original.
128
+ - Tests are isolated in general, but if the code you're testing mutates its arguments, you have to state them in each test so the mutations aren't carried forward.
129
129
 
130
130
 
131
131
 
@@ -153,9 +153,9 @@ When your tests need certain results from code dependencies, just _spoof_ what y
153
153
  in: [ "green" ], out: [ 7, 8, 9, 10, 10, 11, 12, { color: "green" } ] }
154
154
  ```
155
155
 
156
- - Spoofing collapses forward across tests, but not across the elements within `.plus`.
156
+ - It's just like _fakes_, _mocks_, or _test doubles_ in other test systems, but much easier to write and read.
157
157
 
158
- - Spoofing is also fully isolated within tests.
158
+ - Spoofed code is fully isolated within tests, even though spoof definitions collapse forward across tests (and within `.plus`).
159
159
 
160
160
 
161
161
 
@@ -254,10 +254,9 @@ There's plenty more to learn about Risei and how it makes testing easy...
254
254
  - Changing the tested class in `.on` wipes out all prior test properties, so the new class has a fresh start.
255
255
  - Changing the tested method in `.of` wipes out only test properties related to methods, to preserve class values you usually want.
256
256
 
257
- - Individual tests remain fully isolated:
258
- - Test args, including objects and functions in `.with`, `.plus`, and `.in`, are all copied for running in each test, so nothing is reused across tests.
259
- - Class instances, spoofs (covered later), and so on are all created anew for each test.
260
-
257
+ - Individual tests remain isolated, except for when args are mutated by the code under test.
258
+ - Restate args for each test when the code mutates them.
259
+ - This limitation is the result of limits on what can be copied reliably in JavaScript.
261
260
 
262
261
 
263
262
  ### Choosing test-only dependency inputs _AKA_ Spoofing
@@ -515,7 +514,7 @@ You set up the transpiler to output files to a directory with these settings:
515
514
 
516
515
  ### In test files:
517
516
 
518
- All `import` statements have to point to the `outDir` path or subfolders there, using relative-path syntax:
517
+ All `import` statements have to point to the Javascript (`.js`) files in the `outDir` path or subfolders there, using relative-path syntax:
519
518
 
520
519
  ```javascript
521
520
  import { TestedClass } from "../../dist/out-tsc/SubPath/TestedClass.js";
@@ -525,6 +524,7 @@ import { TestedClass } from "../../dist/out-tsc/SubPath/TestedClass.js";
525
524
 
526
525
 
527
526
 
527
+
528
528
  ## Limitations in Risei
529
529
 
530
530
  The following are not supported at present:
@@ -545,33 +545,39 @@ Some of these are on the tentative future-development list.  In the meanti
545
545
 
546
546
  Most problems with using Risei are minor mistakes in syntax, or omissions in test definitions:
547
547
 
548
- | Error condition or text | Probable cause / fix |
548
+ | Error condition or text | Probable cause |
549
549
  |---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
550
550
  | Gold bar appears below the summary, and the number of tests run drops | A syntax error caused a test file not to load; error output in the gold bar explains why |
551
551
  | `"Test loading failed for... SyntaxError: Unexpected token ':'"` | Missing comma in your tests in the named file |
552
552
  | `"Test loading failed for... SyntaxError: Unexpected token '.'"` | Using `this.tests = []` instead of `tests = []` in the file named |
553
553
  | Other `... Unexpected token ...` errors | Some other syntax error in the file named, most likely a missing or extra delimiter |
554
- | Unexpected actual values, or unexpected passes / fails in test runs | Spoofs, retrievals, or special conditions from previous tests not replaced or reset with `[ ]`<br>&mdash; or &mdash;<br>Pre-listed test properties produce extra tests: preventable by restating class in `.on` |
554
+ | Unexpected extra, failing test/s | Partial test properties in a mid-list object have produced extra tests |
555
+ | Unexpected actual values, or unexpected passes / fails in test runs | Test properties from previous tests not replaced or reset with `[ ]` <br>&mdash; or &mdash;<br> Args mutated by tested code were not restated in following tests to isolate them |
555
556
 
556
557
 
557
558
 
558
559
 
559
560
  ## Known issues and workarounds
560
561
 
561
- - No bugs are known to exist at present.
562
+ Two issues are known to exist:
563
+
564
+ - When code mutates its args in test definitions, the mutated forms are used in subsequent tests unless those args are restated.
565
+ - To avoid this problem, just restate the args in each test of this code.
566
+ - The workaround for this &mdash; copying and initing objects to bypass mutation &mdash; requires too many assumptions to be reliable.
562
567
 
563
- - One unexpected result is known to exist:
564
- - Changing one or more properties in objects by themselves in the middle of tests (perhaps to emphasize them) can create extra tests.
565
- - The extra tests usually fail, because they have an unintended mix of old and new properties.
566
- - In this case, Rs is actually working properly, running prior tests with the new properties swapped in.
567
- - To avoid this problem, include changed properties in full tests to run, or restate `.of` along with the changed properties.
568
568
 
569
- - This situation is rarely an issue, but ways to optionally prevent it are being considered.
569
+ - When a few properties are changed in separate objects, they are treated as whole new tests, which usually fail because of their unintended mix of old and new properties.
570
+ - To avoid this problem, just change properties as part of whole new tests, or else restate `.of` along with the changed properties.
571
+ - Rs is actually working as intended when this happens, but ways to optionally prevent it are being considered.
570
572
 
571
573
 
572
574
 
573
575
  ## What's new in Risei
574
576
 
577
+ - Release **1.3.2** (February, 2024) does not change the code at all, but improves this read-me, notably explaining the easiest way to deal with mutable args (which is just restating the args), and correcting a few mistakes.
578
+
579
+ - Release **1.3.1** (February, 2024) reverses some changes in 1.3.0 that were intended to fully isolate tests against mutable args, but which caused incomplete or inaccurate object contents in some cases.&nbsp; This sort of built-in total isolation is not feasible, and in fact is rarely needed.
580
+
575
581
  - Release **1.3.0** (February, 2024) moves the display of test-loading error messages to a new gold bar that appears at the bottom of the summary when there are any loading errors.&nbsp; A sorting error that occurs when no test files exist yet has also been fixed.
576
582
 
577
583
  - Release **1.2.0** (January, 2024) changes sorting of test files.&nbsp; Previously they were displayed in the order found.&nbsp; Now the order remains the same, except that the last-edited file is moved to the bottom, making it easy to see the latest test results.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "risei",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Risei is the framework that allows you to write unit tests as collections of values in JavaScript objects, so it's easy and fast, and tests don't serve as a drag on redesigns.",
5
5
  "keywords": [
6
6
  "unit test",
@@ -48,6 +48,6 @@
48
48
  "fs": "^0.0.1-security",
49
49
  "mocha": "^10.0.0",
50
50
  "morgan": "~1.9.1",
51
- "risei": "1.2.0"
51
+ "risei": "1.3.2"
52
52
  }
53
53
  }
@@ -53,10 +53,10 @@ export class SpoofClassMethodsFixture extends ASpoofingFixture {
53
53
  }
54
54
 
55
55
  // Converting to scope object type.
56
- let spoofDefinitionss = SpoofDefinition.fromNonceTuples(spoofNonces);
56
+ let spoofDefinitions = SpoofDefinition.fromNonceTuples(spoofNonces);
57
57
 
58
58
  // Storing spoofing definitions.
59
- this.#retainSpoofsByClass(type, spoofDefinitionss);
59
+ this.#retainSpoofsByClass(type, spoofDefinitions);
60
60
 
61
61
  // Storing originals for restoring later.
62
62
  this.#reserveOriginalsByClass();
@@ -67,9 +67,9 @@ export class SpoofClassMethodsFixture extends ASpoofingFixture {
67
67
 
68
68
  // region Dependencies of spoof()
69
69
 
70
- #retainSpoofsByClass(type, spoofDefinitionss) {
70
+ #retainSpoofsByClass(type, spoofDefinitions) {
71
71
  // Map of Maps: by type, then by method.
72
- for (let tuple of spoofDefinitionss) {
72
+ for (let tuple of spoofDefinitions) {
73
73
  // If no .target, use `type`.
74
74
  if (!tuple.target) {
75
75
  tuple.target = type;
@@ -299,8 +299,7 @@ export class TestDefinition {
299
299
  // Add (copy of) latest (for isolation),
300
300
  // only if it's ready to run as a test.
301
301
  if (latest.isRunnable) {
302
- let copy = latest.copy();
303
- tuples.push(copy);
302
+ tuples.push(latest);
304
303
  }
305
304
  }
306
305
 
@@ -14,6 +14,7 @@ export class TestFrame {
14
14
 
15
15
  testFrame(test) {
16
16
  console.log(`cruft : a`);
17
+
17
18
  // Outer try for fails of groundwork requested.
18
19
  try {
19
20
  console.log(`cruft : b`);
@@ -1,103 +1,106 @@
1
- /**/
2
-
3
- import { TypeIdentifier } from "./TypeIdentifier.js";
4
- import { Types } from "./Types.js";
5
-
6
- export class TotalCopier {
7
- #identifier = new TypeIdentifier();
8
-
9
- copy(original) /* passed */ {
10
- return this.recursiveCopy(original);
11
- }
12
-
13
- recursiveCopy(original) /* verified */ {
14
- /* &cruft, factor, possibly abstracting similar code,
15
- and/or return copy only once, or similar */
16
-
17
- /* Algorithm: First copied at current level by value if a value, by reference if
18
- not a value; then recursively replaced at next level, and so on. */
19
-
20
- let copy;
21
-
22
- let type = this.#identifier.identify(original);
23
-
24
- if (type === Types.isArray) {
25
- copy = [ ];
26
-
27
- // Traversal construction with recursion.
28
- for (let item of original) {
29
- let next = this.recursiveCopy(item);
30
- copy.push(next);
31
- }
32
-
33
- return copy;
34
- }
35
-
36
- if (type === Types.isMap) {
37
- copy = new Map();
38
-
39
- // Traversal construction with recursion.
40
- for (let entry of original.entries()) {
41
- let next = this.recursiveCopy(entry);
42
- copy.set(next[0], next[1]);
43
- }
44
-
45
- return copy;
46
- }
47
-
48
- if (type === Types.isSet) {
49
- copy = new Set();
50
-
51
- // Traversal construction with recursion.
52
- for (let value of original.values()) {
53
- let next = this.recursiveCopy(value);
54
- copy.add(next);
55
- }
56
-
57
- return copy;
58
- }
59
-
60
- if (type === Types.isDate) {
61
- copy = new Date(original);
62
- return copy;
63
- }
64
-
65
- // Actually copying isn't needed (or desirable) here,
66
- // since should always point to same thing regardless.
67
- if (type === Types.isClass) {
68
- copy = original;
69
- return copy;
70
- }
71
-
72
- // The workaround here makes a true independent copy of the
73
- // original function, regardless of its definition style.
74
- if (type === Types.isFunction) {
75
- let passer = [ original ];
76
- passer = [ ...passer ];
77
- copy = passer[0];
78
-
79
- return copy;
80
- }
81
-
82
- if (original instanceof Object) {
83
- copy = { };
84
-
85
- let keys = Object.keys(original);
86
-
87
- // Traversal construction with recursion.
88
- for (let key of Object.keys(original)) {
89
- let next = this.recursiveCopy(original[key]);
90
- copy[key] = next;
91
- }
92
-
93
- return copy;
94
- }
95
-
96
- /* &cruft, other object types here */
97
-
98
- // All object types exhausted, so this is a value,
99
- // which can be used directly for a copy.
100
- copy = original;
101
- return copy;
102
- }
103
- }
1
+ /**/
2
+
3
+ import { TypeIdentifier } from "./TypeIdentifier.js";
4
+ import { Types } from "./Types.js";
5
+
6
+ export class TotalCopier {
7
+ #identifier = new TypeIdentifier();
8
+
9
+ copy(original) /* passed */ {
10
+ return this.recursiveCopy(original);
11
+ }
12
+
13
+ recursiveCopy(original) /* verified */ {
14
+ /* &cruft, factor, possibly abstracting similar code,
15
+ and/or return copy only once, or similar */
16
+
17
+ /* Algorithm: First copied at current level by value if a value, by reference if
18
+ not a value; then recursively replaced at next level, and so on. */
19
+
20
+ let copy;
21
+
22
+ let type = this.#identifier.identify(original);
23
+
24
+ if (type === Types.isArray) {
25
+ copy = [ ];
26
+
27
+ // Traversal construction with recursion.
28
+ for (let item of original) {
29
+ let next = this.recursiveCopy(item);
30
+ copy.push(next);
31
+ }
32
+
33
+ return copy;
34
+ }
35
+
36
+ if (type === Types.isMap) {
37
+ copy = new Map();
38
+
39
+ // Traversal construction with recursion.
40
+ for (let entry of original.entries()) {
41
+ let next = this.recursiveCopy(entry);
42
+ copy.set(next[0], next[1]);
43
+ }
44
+
45
+ return copy;
46
+ }
47
+
48
+ if (type === Types.isSet) {
49
+ copy = new Set();
50
+
51
+ // Traversal construction with recursion.
52
+ for (let value of original.values()) {
53
+ let next = this.recursiveCopy(value);
54
+ copy.add(next);
55
+ }
56
+
57
+ return copy;
58
+ }
59
+
60
+ if (type === Types.isDate) {
61
+ copy = new Date(original);
62
+ return copy;
63
+ }
64
+
65
+ // Actually copying isn't needed (or desirable) here,
66
+ // since should always point to same thing regardless.
67
+ if (type === Types.isClass) {
68
+ copy = original;
69
+ return copy;
70
+ }
71
+
72
+ // The workaround here makes a true independent copy of the
73
+ // original function, regardless of its definition style.
74
+ if (type === Types.isFunction) {
75
+ let passer = [ original ];
76
+ passer = [ ...passer ];
77
+ copy = passer[0];
78
+
79
+ return copy;
80
+ }
81
+
82
+ if (original instanceof Object) {
83
+ // If original has a prototype, it's probably a class instance,
84
+ // so it's constructed, so it includes all methods and accessors.
85
+ let prototype = Object.getPrototypeOf(original);
86
+ copy = prototype !== null ? new prototype.constructor() : { };
87
+
88
+ let keys = Object.keys(original);
89
+
90
+ // Settable properties are added, with recursion.
91
+ for (let key of Object.keys(original)) {
92
+ let next = this.recursiveCopy(original[key]);
93
+ copy[key] = next;
94
+ }
95
+
96
+ return copy;
97
+ }
98
+
99
+ /* &cruft, other object types here */
100
+
101
+ // All object types exhausted, so this is a value,
102
+ // which can be used directly for a copy.
103
+ copy = original;
104
+ return copy;
105
+ }
106
+ }
@@ -1,103 +1,103 @@
1
- /**/
2
-
3
- /* Used to analyze classes / members of classes being tested. */
4
-
5
- export class TypeAnalyzer {
6
- // region Definitions
7
-
8
- static constructorName = "constructor";
9
- static getName = "get";
10
- static setName = "set";
11
-
12
- // endregion Definitions
13
-
14
- // region Fields
15
-
16
- #type;
17
-
18
- // endregion Fields
19
-
20
- constructor(type) {
21
- this.#type = type;
22
- }
23
-
24
- /* Returns true if instance or static member is a property (value or accessor). Returns false if member is a method. */
25
- memberIsProperty(name) /* passed */ {
26
- // Constructor is never a property and is irregular,
27
- // so other type-analysis code can't handle it.
28
- if (name === TypeAnalyzer.constructorName) {
29
- return false;
30
- }
31
-
32
- // Static methods and properties (value or accessor) are on type.
33
- if (name in this.#type) {
34
- return this.#staticMemberIsProperty(this.#type, name);
35
- }
36
-
37
- // Methods and accessor properties are on prototype.
38
- if (name in this.#type.prototype) {
39
- return this.#instanceMemberIsProperty(this.#type.prototype, name);
40
- }
41
-
42
- // Instance value properties are not on type or prototype.
43
- return true;
44
- }
45
-
46
- // region Dependencies of memberIsProperty()
47
-
48
- /* Algorithm specialized with function check for static members. */
49
- #staticMemberIsProperty(type, name) {
50
- let descriptor = Object.getOwnPropertyDescriptor(type, name);
51
-
52
- // If on type and has accessors, it's a static accessor property.
53
- if (this.#doesHaveAccessorProps(descriptor)) {
54
- return true;
55
- }
56
-
57
- // If on type and has non-Function .value, it's a static value property.
58
- if (this.#doesNotHaveFunctionValue(descriptor)) {
59
- return true;
60
- }
61
-
62
- // If on type but not a static property, it's a static method.
63
- return false;
64
- }
65
-
66
- /* Algorithm specialized for instance members, no function check. */
67
- #instanceMemberIsProperty(prototype, name) {
68
- let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
69
-
70
- // If on prototype and has accessors, it's an accessor property.
71
- if (this.#doesHaveAccessorProps(descriptor)) {
72
- return true;
73
- }
74
-
75
- // If on prototype but not an accessor property, it's a method.
76
- return false;
77
- }
78
-
79
- // region Internally reused dependencies of other memberIsProperty() dependencies
80
-
81
- #doesHaveAccessorProps(descriptor) /* verified */ {
82
- // Accessor properties have one or both of these in descriptor.
83
- if (TypeAnalyzer.getName in descriptor || TypeAnalyzer.setName in descriptor) {
84
- return true;
85
- }
86
-
87
- return false;
88
- }
89
-
90
- #doesNotHaveFunctionValue(descriptor) /* verified */ {
91
- // Simple inverter of other method for readability.
92
- return !this.#doesHaveFunctionValue(descriptor);
93
- }
94
-
95
- #doesHaveFunctionValue(descriptor) /* verified */ {
96
- // If no member, .value of undefined.
97
- return descriptor.value instanceof Function;
98
- }
99
-
100
- // endregion Internally reused dependencies of other memberIsProperty() dependencies
101
-
102
- // endregion Dependencies of memberIsProperty()
103
- }
1
+ /**/
2
+
3
+ /* Used to analyze classes / members of classes being tested. */
4
+
5
+ export class TypeAnalyzer {
6
+ // region Definitions
7
+
8
+ static constructorName = "constructor";
9
+ static getName = "get";
10
+ static setName = "set";
11
+
12
+ // endregion Definitions
13
+
14
+ // region Fields
15
+
16
+ #type;
17
+
18
+ // endregion Fields
19
+
20
+ constructor(type) {
21
+ this.#type = type;
22
+ }
23
+
24
+ /* Returns true if instance or static member is a property (value or accessor). Returns false if member is a method. */
25
+ memberIsProperty(name) /* passed */ {
26
+ // Constructor is never a property and is irregular,
27
+ // so other type-analysis code can't handle it.
28
+ if (name === TypeAnalyzer.constructorName) {
29
+ return false;
30
+ }
31
+
32
+ // Static methods and properties (value or accessor) are on type.
33
+ if (name in this.#type) {
34
+ return this.#staticMemberIsProperty(this.#type, name);
35
+ }
36
+
37
+ // Methods and accessor properties are on prototype.
38
+ if (name in this.#type.prototype) {
39
+ return this.#instanceMemberIsProperty(this.#type.prototype, name);
40
+ }
41
+
42
+ // Instance value properties are not on type or prototype.
43
+ return true;
44
+ }
45
+
46
+ // region Dependencies of memberIsProperty()
47
+
48
+ /* Algorithm specialized with function check for static members. */
49
+ #staticMemberIsProperty(type, name) {
50
+ let descriptor = Object.getOwnPropertyDescriptor(type, name);
51
+
52
+ // If on type and has accessors, it's a static accessor property.
53
+ if (this.#doesHaveAccessorProps(descriptor)) {
54
+ return true;
55
+ }
56
+
57
+ // If on type and has non-Function .value, it's a static value property.
58
+ if (this.#doesNotHaveFunctionValue(descriptor)) {
59
+ return true;
60
+ }
61
+
62
+ // If on type but not a static property, it's a static method.
63
+ return false;
64
+ }
65
+
66
+ /* Algorithm specialized for instance members, no function check. */
67
+ #instanceMemberIsProperty(prototype, name) {
68
+ let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
69
+
70
+ // If on prototype and has accessors, it's an accessor property.
71
+ if (this.#doesHaveAccessorProps(descriptor)) {
72
+ return true;
73
+ }
74
+
75
+ // If on prototype but not an accessor property, it's a method.
76
+ return false;
77
+ }
78
+
79
+ // region Internally reused dependencies of other memberIsProperty() dependencies
80
+
81
+ #doesHaveAccessorProps(descriptor) /* verified */ {
82
+ // Accessor properties have one or both of these in descriptor.
83
+ if (TypeAnalyzer.getName in descriptor || TypeAnalyzer.setName in descriptor) {
84
+ return true;
85
+ }
86
+
87
+ return false;
88
+ }
89
+
90
+ #doesNotHaveFunctionValue(descriptor) /* verified */ {
91
+ // Simple inverter of other method for readability.
92
+ return !this.#doesHaveFunctionValue(descriptor);
93
+ }
94
+
95
+ #doesHaveFunctionValue(descriptor) /* verified */ {
96
+ // If no member, .value of undefined.
97
+ return descriptor.value instanceof Function;
98
+ }
99
+
100
+ // endregion Internally reused dependencies of other memberIsProperty() dependencies
101
+
102
+ // endregion Dependencies of memberIsProperty()
103
+ }