# 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.
