tcompare
Version:
A comprehensive comparison library, for use in test frameworks
312 lines (262 loc) • 9.73 kB
Markdown
# 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.