testClasses/SequencesAreEqual.js

/**
 * File: src/testClasses/SequenceAreEqual.js
 */

import { TestBase } from "../core.js";
import { InternalUtils } from "../utilClasses/_InternalUtils.js";

/**
 * @class SequencesAreEqual
 *
 * A test that compares two iterable sequences element-by-element for equality.
 * 
 * You can supply a custom item comparison function. Results include index mismatches.
 *
 * Inherits from {@link TestBase}.
 */
export class SequencesAreEqual extends TestBase {
	/** @type {Iterable<any>} */
	#_expected = null;

	/** @type {Iterable<any>} */
	#_actual = null;

	/** @type {function} */
	#_itemComparer = null;

	/** @type {boolean} */
	#_validIterables = true;

	/** @type {any} */
	#_thisArg = null;

	/**
	 * Compares two iterable sequences element by element.
	 * 
	 * @param {string} testName - Name of the test.
	 * @param {Iterable<any>} expected - Expected sequence.
	 * @param {Iterable<any>} actual - Actual sequence.
	 * @param {function(any, any):boolean} [itemComparer] - Optional custom comparison function to compare individual items ((expected, actual) => boolean).
	 * @param {any} [thisArg] - Optional `this` binding for the itemComparer.
	 */
	constructor(testName, expected, actual, itemComparer, thisArg) {
		// Call Assert constructor with all info
		super(testName, `Actual sequence equals to the expected sequence`, `Actual sequence does not equal to the expected sequence)`, null, thisArg);
		this.additionalData = {};

		this.#_expected = expected;
		this.#_actual = actual;
		this.#_itemComparer = itemComparer;
		this.#_thisArg = thisArg;

		// Validate that expected is iterable
		if (!InternalUtils.isIterable(expected)) {
			this.additionalData["expected"] = "ERROR: 'expected' argument is not iterable!"
			this.#_validIterables = false;
		}

		// Validate that actual is iterable
		if (!InternalUtils.isIterable(actual)) {
			this.additionalData["actual"] = "ERROR: 'actual' argument is not iterable!"
			this.#_validIterables = false;
		}
	}

	/**
	 * Runs the test without printing.
	 * 
	 * @returns {boolean} Whether the test passed.
	 * @override 
	 */
	runImpl() {
		if(!this.#_validIterables) {
			this.succeeded = false;
		}
		else {
			const t0 = InternalUtils.now();

			let expectedArr = this.additionalData["expected"] = Array.from(this.#_expected);
			let actualArr = this.additionalData["actual"] = Array.from(this.#_actual);
			let res;

			// Check lengths
			if (expectedArr.length === actualArr.length) {
				// Check individual items
				let indicesDifferent = [];
				for (var i = 0; i < expectedArr.length; i++) {
					let res = this.#_itemComparer != null ? this.#_itemComparer.call(this.#_thisArg ?? undefined, expectedArr[i], actualArr[i]) : expectedArr[i] === actualArr[i];
					if (res !== true) {
						indicesDifferent.push(i);
					}
				}

				if (indicesDifferent.length > 0)
					this.additionalData["Mismatch at indices"] = "Different element indices are: {" + indicesDifferent.join(", ") + "}";

				res = indicesDifferent.length === 0;
			}
			else{
				this.additionalData["Mismatch at indices"] = "expected.length !== actual.length";
				res = false;
			}

			const t1 = InternalUtils.now();
			this.elapsed = t1 - t0;

			this.succeeded = res;
		}
	}
}