UNPKG

tcompare

Version:

A comprehensive comparison library, for use in test frameworks

312 lines (262 loc) 9.73 kB
# tcompare A comprehensive comparison library, for use in test frameworks. Walks an object once, generating both a simple true/false result, as well as a string representation of both the actual and expected values (highlighting just the parts that differ) and a patch-style diff string. ## USAGE ```ts // require() is fine too import { match, same, strict, has, hasStrict, matchStrict, matchOnly, matchOnlyStrict, } from 'tcompare' import type { Result } from 'tcompare' const result: Result = match(object, pattern) if (!result.match) { console.log(`item did not match pattern`) console.log(result.diff) } else { console.log(`it's a match!`) } // raw classes exported also import { MatchOnly } from 'tcompare' const mo = new MatchOnly({ a: 1, b: 2 }, { expect: { a: Number } }) const diff: string = mo.print() console.log(mo.match) // false console.log(diff) /* --- expected +++ actual @@ -1,2 +1,3 @@ Object { + "b": 2, } */ ``` - `indent` - String to indent each nested level. Defaults to `' '`.## METHODS Each method corresponds to an exported class. Except for `format()` (which returns a string), they all return a `Result` object. (That is, `{diff:string, match:boolean}`.) - `format(object, [options])` - No comparisons performed. Just print out the object. Returns just the formatted string. - `same(object, pattern, [options])` - Deep equivalence. Ensure that all items in the pattern are found in the object, and vice versa, matching loosely (so, for example `1` will match with `'1'`). - `strict(object, pattern, [options])` - Deep equality. Ensure that all items in the pattern are found in the object, and vice versa, matching strictly (so, for example `1` will not match with `'1'`). Objects must have the same constructors, and all fields will be matched recursively using the same `strict` test. - `has(object, pattern, [options])` - Ensure that all items in the pattern are found in the object, but ignore additional items found in the object, matching loosely. Classes only need to match loosely, so a plain JavaScript object can be used to check for fields on a class instance. - `hasStrict(object, pattern, [options])` - Ensure that all items in the pattern are found in the object, but ignore additional items found in the object, matching strictly. Constructors do _not_ have to match between objects, but if `constructor` is set as an ownProperty on the pattern object, then it will be checked for strict equality. - `match(object, pattern, [options])` - Verify that all items in `pattern` are found in `object`, and that they match in an extremely loose way. This is the loosest possible algorithm, allowing cases where we just want to verify that an object contains a few important properties. In summary: - If the object and pattern are loosely equal, then pass - If the object and the pattern are both Regular Expressions, Date objects or Buffers, then pass if they represent equivalent values. - If the pattern is a RegExp, cast object to a string, and test against the RegExp. - If both are Strings, pass if pattern appears in object. ( - If pattern is a function, and object is an instance of that function, then pass. (This also applies to Symbol, Number, String, etc.) - If pattern and object are collections (object, map, set, array or iterable), then compare their contents. Each type of collection can only match its same type, with the exception of non-Set iterables (including `arguments` objects), which are cast to Arrays. - `matchOnly(object, pattern, [options])` - Same comparison testing as `match()`, but will fail if the `object` has any properties that are not present in the `pattern`. - `matchStrict(object, pattern, [options])` - Same comparison testing as `match()`, but will fail when two values are equivalent but not strictly equal. (That is, when `a == b && !(a === b)`.) - `matchOnlyStrict(object, pattern, [options])` - Same comparison testing as `matchOnly()`, but will fail when two values are equivalent but not strictly equal. (That is, when `a == b && !(a === b)`.) There are classes exported to correspond to each of these. All of these are instantiated like `new Format(object, options)`. An `expect` option is required for all classes except `Format`. Call `obj.print()` on the resulting object to generate a diff. Once the diff (or format) is generated, it'll have a `match` boolean member. ## Classes The exported classes should usually not be used directly, and their implementation details are subject to change as needed between versions. The class heirarchy is: ``` Format +-- Same +-- Strict +-- Has | +-- HasStrict (uses Strict.prototype.test) | +-- Match | +-- MatchStrict (fails if a==b && a!==b) +-- MatchOnly (uses Match.prototype.test) +-- MatchOnlyStrict (uses MatchStrict.prototype.test) ``` In order to compare or print an object, instantiate one of the classes, and call then the `print()` method, which will return the diff or formatted value. The `match` boolean property will be set after calling `print()`. If the objects match, then the returned `diff` will also be an empty string. ## OPTIONS ### `FormatOptions` type Every method and class can take the following options. - `sort` - Set to `true` to sort object keys. This is important when serializing in a deterministic way. - `style` - Set to `pretty` for a very human-readable style of object printing. Set to `js` for a copy-and-paste friendly valid JavaScript output. Set to `tight` for a minimal white-space js format. Default is `pretty`. Example: ``` // pretty style Object { "myMap": Map { Object { "a": 1, } => Object { "b": 2, } } } // js style { "myMap": new Map([ [{ "a": 1, }, { "b": 2, }] ]) } // tight style {"myMap":new Map([[{"a":1,},{"b":2,}],]),} ``` Note that `tight` is not suitable for comparisons, only formatting. - `reactString` - Represent and compare React elements as JSX strings. Only supported in the `pretty` formatting style. Enabled by default, set `{ reactString: false }` in the options to disable it. When enabled, react elements are _first_ compared as react JSX strings, and if the strings match, treated as equivalent, even if they would not otherwise be treated as a match as plain objects (for example, if `children` is set to `'hello'` vs `['hello']`, these are considered identical, because they result in the same JSX). If they do not match, then they are still considered a match if their plain object represenatations would be considered a match. So for example, `<x a="b" />` would match `<x a={/b|c/} />` for functions where strings can match against regular expressions. - `bufferChunkSize` - The number of bytes to show per line when printing long `Buffer` objects. Defaults to 32. - `indent` - String to indent each nested level. Defaults to `' '`. - `includeEnumerable` - Set to `true` to walk over _all_ enumerable properties of a given object when comparing or formatting, rather than the default of only showing enumerable own-properties. Note that calling getter functions may be hazardous, as they may trigger side-effects. - `includeGetters` - Set to `true` to walk over all enumerable getters on an object's prototype (but not from further down the prototype chain), in addition to own-properties. This is useful in cases where you want to compare or print an object with enumerable getters that return internal values in a read-only manner. Note that calling getter functions can be hazardous, as they may trigger side-effects. ### `SameOptions` type Comparison classes also take the following options. - `expect` - required. The pattern object to compare against. - `diffContext` - Optional, default 10. Number of lines of context to show in diff output. ## Circular References Circular references are displayed using YAML-like references, in order to determine _which_ item is circularly referenced. When doing comparisons, a pattern and object will be considered matching if they contain the _same_ circularity. So, for example, if a pattern refers to itself, then an object should refer to itself as well. ```js const a = { list: [], b: {} } a.list.push(a) a.list.push(a.b) a.b.a = a console.log(format(a)) /* &ref_1 Object { "list": Array [ <*ref_1>, Object { "a": <*ref_1>, }, ], "b": Object { "a": <*ref_1>, }, } */ ``` Note that circular references are never going to be valid JavaScript, even when using the `js` style. ### Caveat: Circularity Between Pattern and Object Gets Weird It's possible to get strange output when an object and pattern refer to one another. ```js import { same } from 'tcompare' const a = {} a.o = a const b = { o: a } console.error(same(a, b).diff) // produces this confusing output: /* --- expected +++ actual @@ -1,5 +1,3 @@ &ref_1 Object { - "o": &ref_1 Object { - "o": <*ref_1>, - }, + "o": <*ref_1>, } */ ``` The more correct output would be something like: ``` --- expected +++ actual @@ -1,5 +1,3 @@ &ref_1 Object { - "o": &ref_2 Object { - "o": <*ref_2>, - }, + "o": <*ref_1>, } ``` However, this requires tracking IDs in a much more complicated way, being aware of whether the object is being read as an pattern object or test object when determining its reference ID. Since this is a relatively unusual thing to happen, and only affects the output (but still properly detects whether it should be treated as a match or not), it will likely not be addressed.