alsatian-test/alsatian

View on GitHub
packages/alsatian/core/matchers/object-matcher.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { diff } from "./diff";
import { EmptyMatcher } from "./empty-matcher";
import { TypeMatcher } from "../spying";

export class ObjectMatcher<T extends object> extends EmptyMatcher<T> {

    /**
     * Checks that a value is equal to another (for objects the function will check for deep equality)
     * @param expectedValue - the value that will be used to match
     */
    public toEqual(expectedValue: T | TypeMatcher<T>) {
        if (Buffer.isBuffer(expectedValue) || Buffer.isBuffer(this.actualValue)) {
            this.checkTypeMatcherEqual(expectedValue, this.buffersEqual);
        }
        else {
            this.checkTypeMatcherEqual(expectedValue, this.objectsEqual);
        }
    }

   private buffersEqual(expectedValue: T) {
       this.registerMatcher(
           this.checkBuffersAreEqual(
               expectedValue,
               this.actualValue
           ) === this.shouldMatch,
           `Expected values ${!this.shouldMatch ? "not " : ""}to be equal`,
           expectedValue,
           {
               diff: diff(expectedValue, this.actualValue)
           }
       );
   }

   private checkBuffersAreEqual(buffer: any, other: any): boolean {
       // Buffer.from() only accepts of type string, Buffer, ArrayBuffer, Array, or Array-like Object.
       if (this.isBufferable(other)) {
           const otherBuffer = Buffer.isBuffer(other)
               ? other
               : Buffer.from(other as string); // Typings don't know that Buffer.from() can accept ArrayLike<T>

           return buffer.equals(otherBuffer);
       } else {
           return false;
       }
   }

   private isBufferable(
       obj: any
   ): obj is string | Buffer | Array<any> | ArrayBuffer | ArrayLike<any> {
       return (
           "string" === typeof obj ||
           Buffer.isBuffer(obj) ||
           Array.isArray(obj) ||
           obj instanceof ArrayBuffer ||
           // ArrayLike<any>
           (null != obj &&
               "object" === typeof obj &&
               obj.hasOwnProperty("length") &&
               "number" === typeof obj.length &&
               (obj.length === 0 || (obj.length > 0 && obj.length - 1 in obj)))
       );
   }

    private objectsEqual(expectedValue: T) {
        this.registerMatcher(
            this.checkObjectsAreDeepEqual(
                expectedValue,
                this.actualValue
            ) === this.shouldMatch,
            `Expected objects ${!this.shouldMatch ? "not " : ""}to be equal`,
            expectedValue,
            {
                diff: diff(expectedValue, this.actualValue)
            }
        );
    }

    private checkObjectsAreDeepEqual(objectA: any, objectB: any): boolean {
        // if one object is an array and the other is not then they are not equal
        if (Array.isArray(objectA) !== Array.isArray(objectB)) {
            return false;
        }

        // get all the property keys for each object
        const OBJECT_A_KEYS = Object.keys(objectA);
        const OBJECT_B_KEYS = Object.keys(objectB);

        // if they don't have the same amount of properties then clearly not
        if (OBJECT_A_KEYS.length !== OBJECT_B_KEYS.length) {
            return false;
        }

        // check all the properties of each object
        for (const objectAKey of OBJECT_A_KEYS) {
            // if the property values are not the same
            if (objectA[objectAKey] !== objectB[objectAKey]) {
                // and it's not an object or the objects are not equal
                if (
                    objectA[objectAKey] === null ||
                    typeof objectA[objectAKey] !== "object" ||
                    this.checkObjectsAreDeepEqual(
                        objectA[objectAKey],
                        objectB[objectAKey]
                    ) === false
                ) {
                    // then not deep equal
                    return false;
                }
            }
        }

        // all properties match so all is good
        return true;
    }
}